/** * @file Varicode.cpp * @brief implementation of encoder * (C) 2018 Jordan Sherer - All Rights Reserved * */ #include "Varicode.h" #include "JS8_JSC/JSC.h" #include "JS8_Mode/DecodedText.h" #include #include #include #include #include #include #include #define CRCPP_INCLUDE_ESOTERIC_CRC_DEFINITIONS #define CRCPP_USE_CPP11 Q_DECLARE_LOGGING_CATEGORY(varicode_js8) const int nalphabet = 41; QString alphabet = { "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"}; // alphabet to encode _into_ // for FT8 freetext // transmission QString alphabet72 = { "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+/?."}; QString grid_pattern = { R"((?[A-X]{2}[0-9]{2}(?:[A-X]{2}(?:[0-9]{2})?)*)+)"}; QString orig_compound_callsign_pattern = { R"((?(\d|[A-Z])+\/?((\d|[A-Z]){2,})(\/(\d|[A-Z])+)?(\/(\d|[A-Z])+)?))"}; QString base_callsign_pattern = { R"((?\b(?([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?[/][P])?\b))"}; // QString compound_callsign_pattern = // {R"((?\b(?[A-Z0-9]{1,4}\/)?(?([0-9A-Z])?([0-9A-Z])([0-9])([A-Z])?([A-Z])?([A-Z])?)(?\/[A-Z0-9]{1,4})?)\b)"}; QString compound_callsign_pattern = { R"((?(?:[@]?|\b)(?[A-Z0-9\/@][A-Z0-9\/]{0,2}[\/]?[A-Z0-9\/]{0,3}[\/]?[A-Z0-9\/]{0,3})\b))"}; QString pack_callsign_pattern = { R"(([0-9A-Z ])([0-9A-Z])([0-9])([A-Z ])([A-Z ])([A-Z ]))"}; QString alphanumeric = { "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ /@"}; // callsign and grid alphabet QMap directed_cmds = { // any changes here need to be made also in the directed regular xpression // for parsing // ?*^&@ {" HEARTBEAT", -1}, // this is my heartbeat (unused except for faux // processing of HBs as directed commands) {" HB", -1}, // this is my heartbeat (unused except for faux processing of // HBs as directed commands) {" CQ", -1}, // this is my cq (unused except for faux processing of CQs as // directed commands) {" SNR?", 0}, // query snr {"?", 0}, // compat {" DIT DIT", 1}, // two bits {" HEARING?", 3}, // query station calls heard {" GRID?", 4}, // query grid {">", 5}, // relay message {" STATUS?", 6}, // query idle message {" STATUS", 7}, // this is my status {" HEARING", 8}, // these are the stations i'm hearing {" MSG", 9}, // this is a complete message {" MSG TO:", 10}, // store message at a station {" QUERY", 11}, // generic query {" QUERY MSGS", 12}, // do you have any stored messages? {" QUERY MSGS?", 12}, // do you have any stored messages? {" QUERY CALL", 13}, // can you transmit a ping to callsign? // {" ", 14 }, // reserved {" GRID", 15}, // this is my current grid locator {" INFO?", 16}, // what is your info message? {" INFO", 17}, // this is my info message {" FB", 18}, // fine business {" HW CPY?", 19}, // how do you copy? {" SK", 20}, // end of contact {" RR", 21}, // roger roger {" QSL?", 22}, // do you copy? {" QSL", 23}, // i copy {" CMD", 24}, // command {" SNR", 25}, // seen a station at the provided snr {" NO", 26}, // negative confirm {" YES", 27}, // confirm {" 73", 28}, // best regards, end of contact {" NACK", 2}, // negative acknowledge {" ACK", 14}, // acknowledge (was reserved in 2.1) {" HEARTBEAT SNR", 29}, // (was ACK in 2.1, now deprecated) {" AGN?", 30}, // repeat message {" ", 31}, // send freetext (weird artifact) {" ", 31}, // send freetext }; // commands allowed to be processed QSet allowed_cmds = {-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; // commands that result in an autoreply (which can be relayed) QSet autoreply_cmds = {0, 2, 3, 4, 6, 9, 10, 11, 12, 13, 14, 16, 30}; // commands that should be buffered QSet buffered_cmds = {5, 9, 10, 11, 12, 13, 15, 24}; // commands that may include an SNR value QSet snr_cmds = {25, 29}; // commands that are checksummed and their crc size QMap checksum_cmds = {{5, 16}, {9, 16}, {10, 16}, {11, 16}, {12, 16}, {13, 16}, {15, 0}, {24, 16}}; QString callsign_pattern = QString("(?[@]?[A-Z0-9/]+)"); QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|QSL[?]|HW CPY[?]|MSG " "TO[:]|SNR[?]|INFO[?]|GRID[?]|STATUS[?]|QUERY " "MSGS[?]|HEARING[?]|(?:(?:STATUS|HEARING|QUERY CALL|QUERY " "MSGS|QUERY|CMD|MSG|NACK|ACK|73|YES|NO|HEARTBEAT " "SNR|SNR|QSL|RR|SK|FB|INFO|GRID|DIT DIT)(?=[ ]|$))|[?> ]))?"); QString optional_grid_pattern = QString("(?\\s?[A-R]{2}[0-9]{2})?"); QString optional_extended_grid_pattern = QString("^(?\\s?(?:[A-R]{2}[0-9]{2}(?:[A-X]{2}(?:[0-9]{2})?)*))?"); QString optional_num_pattern = QString("(?(?<=SNR)\\s?[-+]?(?:3[01]|[0-2]?[0-9]))?"); QRegularExpression directed_re("^" + callsign_pattern + optional_cmd_pattern + optional_num_pattern); QRegularExpression heartbeat_re( R"(^\s*(?[@](?:ALLCALL|HB)\s+)?(?CQ CQ CQ|CQ DX|CQ QRP|CQ CONTEST|CQ FIELD|CQ FD|CQ CQ|CQ|HB|HEARTBEAT(?!\s+SNR))(?:\s(?[A-R]{2}[0-9]{2}))?\b)"); QRegularExpression compound_re("^\\s*[`]" + callsign_pattern + "(?" + optional_grid_pattern + // there's a reason this is first (see: // buildMessageFrames) optional_cmd_pattern + optional_num_pattern + ")"); QMap hufftable = { // char code weight {" ", "01"}, // 1.0 {"E", "100"}, // 0.5 {"T", "1101"}, // 0.333333333333 {"A", "0011"}, // 0.25 {"O", "11111"}, // 0.2 {"I", "11100"}, // 0.166666666667 {"N", "10111"}, // 0.142857142857 {"S", "10100"}, // 0.125 {"H", "00011"}, // 0.111111111111 {"R", "00000"}, // 0.1 {"D", "111011"}, // 0.0909090909091 {"L", "110011"}, // 0.0833333333333 {"C", "110001"}, // 0.0769230769231 {"U", "101101"}, // 0.0714285714286 {"M", "101011"}, // 0.0666666666667 {"W", "001011"}, // 0.0625 {"F", "001001"}, // 0.0588235294118 {"G", "000101"}, // 0.0555555555556 {"Y", "000011"}, // 0.0526315789474 {"P", "1111011"}, // 0.05 {"B", "1111001"}, // 0.047619047619 {".", "1110100"}, // 0.0434782608696 {"V", "1100101"}, // 0.0416666666667 {"K", "1100100"}, // 0.04 {"-", "1100001"}, // 0.0384615384615 {"+", "1100000"}, // 0.037037037037 {"?", "1011001"}, // 0.0344827586207 {"!", "1011000"}, // 0.0333333333333 {"\"", "1010101"}, // 0.0322580645161 {"X", "1010100"}, // 0.03125 {"0", "0010101"}, // 0.030303030303 {"J", "0010100"}, // 0.0294117647059 {"1", "0010001"}, // 0.0285714285714 {"Q", "0010000"}, // 0.0277777777778 {"2", "0001001"}, // 0.027027027027 {"Z", "0001000"}, // 0.0263157894737 {"3", "0000101"}, // 0.025641025641 {"5", "0000100"}, // 0.025 {"4", "11110101"}, // 0.0243902439024 {"9", "11110100"}, // 0.0238095238095 {"8", "11110001"}, // 0.0232558139535 {"6", "11110000"}, // 0.0227272727273 {"7", "11101011"}, // 0.0222222222222 {"/", "11101010"}, // 0.0217391304348 }; QChar ESC = '\\'; // Escape char QChar EOT = '\x04'; // EOT char quint32 nbasecall = 37 * 36 * 10 * 27 * 27 * 27; quint16 nbasegrid = 180 * 180; quint16 nusergrid = nbasegrid + 10; quint16 nmaxgrid = (1 << 15) - 1; QMap basecalls = { {"<....>", nbasecall + 1}, // incomplete callsign {"@ALLCALL", nbasecall + 2}, // ALLCALL group {"@JS8NET", nbasecall + 3}, // JS8NET group // continental dx {"@DX/NA", nbasecall + 4}, // North America DX group {"@DX/SA", nbasecall + 5}, // South America DX group {"@DX/EU", nbasecall + 6}, // Europe DX group {"@DX/AS", nbasecall + 7}, // Asia DX group {"@DX/AF", nbasecall + 8}, // Africa DX group {"@DX/OC", nbasecall + 9}, // Oceania DX group {"@DX/AN", nbasecall + 10}, // Antarctica DX group // itu regions {"@REGION/1", nbasecall + 11}, // ITU Region 1 {"@REGION/2", nbasecall + 12}, // ITU Region 2 {"@REGION/3", nbasecall + 13}, // ITU Region 3 // generic {"@GROUP/0", nbasecall + 14}, // Generic group {"@GROUP/1", nbasecall + 15}, // Generic group {"@GROUP/2", nbasecall + 16}, // Generic group {"@GROUP/3", nbasecall + 17}, // Generic group {"@GROUP/4", nbasecall + 18}, // Generic group {"@GROUP/5", nbasecall + 19}, // Generic group {"@GROUP/6", nbasecall + 20}, // Generic group {"@GROUP/7", nbasecall + 21}, // Generic group {"@GROUP/8", nbasecall + 22}, // Generic group {"@GROUP/9", nbasecall + 23}, // Generic group // ops {"@COMMAND", nbasecall + 24}, // Command group {"@CONTROL", nbasecall + 25}, // Control group {"@NET", nbasecall + 26}, // Net group {"@NTS", nbasecall + 27}, // NTS group // reserved groups {"@RESERVE/0", nbasecall + 28}, // Reserved {"@RESERVE/1", nbasecall + 29}, // Reserved {"@RESERVE/2", nbasecall + 30}, // Reserved {"@RESERVE/3", nbasecall + 31}, // Reserved {"@RESERVE/4", nbasecall + 32}, // Reserved // special groups {"@APRSIS", nbasecall + 33}, // APRS GROUP {"@RAGCHEW", nbasecall + 34}, // RAGCHEW GROUP {"@JS8", nbasecall + 35}, // JS8 GROUP {"@EMCOMM", nbasecall + 36}, // EMCOMM GROUP {"@ARES", nbasecall + 37}, // ARES GROUP {"@MARS", nbasecall + 38}, // MARS GROUP {"@AMRRON", nbasecall + 39}, // AMRRON GROUP {"@RACES", nbasecall + 40}, // RACES GROUP {"@RAYNET", nbasecall + 41}, // RAYNET GROUP {"@RADAR", nbasecall + 42}, // RADAR GROUP {"@SKYWARN", nbasecall + 43}, // SKYWARN GROUP {"@CQ", nbasecall + 44}, // CQ GROUP {"@HB", nbasecall + 45}, // HB GROUP {"@QSO", nbasecall + 46}, // QSO GROUP {"@QSOPARTY", nbasecall + 47}, // QSO PARTY GROUP {"@CONTEST", nbasecall + 48}, // CONTEST GROUP {"@FIELDDAY", nbasecall + 49}, // FIELD DAY GROUP {"@SOTA", nbasecall + 50}, // SOTA GROUP {"@IOTA", nbasecall + 51}, // IOTA GROUP {"@POTA", nbasecall + 52}, // POTA GROUP {"@QRP", nbasecall + 53}, // QRP GROUP {"@QRO", nbasecall + 54}, // QRO GROUP }; QMap cqs = { {0, "CQ CQ CQ"}, {1, "CQ DX"}, {2, "CQ QRP"}, {3, "CQ CONTEST"}, {4, "CQ FIELD"}, {5, "CQ FD"}, {6, "CQ CQ"}, {7, "CQ"}, }; // status flags in HB messages are deprecated as of 2.2, later versions will // likely repurpose these flags keep in mind if you change any of these to not // start with HB you'll have to address the packHeartbeatMessage and how the // function computes the isAlt flag. QMap hbs = { {0, "HB"}, // HB {1, "HB"}, // HB AUTO {2, "HB"}, // HB AUTO RELAY {3, "HB"}, // HB AUTO RELAY SPOT {4, "HB"}, // HB RELAY {5, "HB"}, // HB RELAY SPOT {6, "HB"}, // HB SPOT {7, "HB"}, // HB AUTO SPOT }; QMap dbm2mw = { {0, 1}, // 1mW {3, 2}, // 2mW {7, 5}, // 5mW {10, 10}, // 10mW {13, 20}, // 20mW {17, 50}, // 50mW {20, 100}, // 100mW {23, 200}, // 200mW {27, 500}, // 500mW {30, 1000}, // 1W {33, 2000}, // 2W {37, 5000}, // 5W {40, 10000}, // 10W {43, 20000}, // 20W {47, 50000}, // 50W {50, 100000}, // 100W {53, 200000}, // 200W {57, 500000}, // 500W {60, 1000000}, // 1000W }; /* * UTILITIES */ int mwattsToDbm(int mwatts) { int dbm = 0; auto values = dbm2mw.values(); std::sort(values.begin(), values.end()); foreach (auto mw, values) { if (mw < mwatts) { continue; } dbm = dbm2mw.key(mw); break; } return dbm; } int dbmTomwatts(int dbm) { if (dbm2mw.contains(dbm)) { return dbm2mw[dbm]; } auto iter = dbm2mw.lowerBound(dbm); if (iter == dbm2mw.end()) { return dbm2mw.last(); } return iter.value(); } QString Varicode::extendedChars() { static QString c; if (c.size() == 0) { for (quint32 i = 0; i < JSC::prefixSize; i++) { if (JSC::prefix[i].size != 1) { continue; } c.append(QLatin1String(JSC::prefix[i].str, 1)); } } return c; } QString Varicode::escape(const QString &text) { static const int size = 6; QString escaped; escaped.reserve(size * text.size()); for (QString::const_iterator it = text.begin(); it != text.end(); ++it) { QChar ch = *it; ushort code = ch.unicode(); if (code < 0x80) { escaped += ch; } else { #if JS8_USE_ESCAPE_SUB_CHAR // escaped += "\x1A"; // substitute char #else escaped += "\\U"; // "U+"; // substitute char #endif escaped += QString::number(code, 16).rightJustified(4, '0'); } } return escaped; } QString Varicode::unescape(const QString &text) { QString unescaped(text); #if JS8_USE_ESCAPE_SUB_CHAR static const int size = 5; QRegularExpression r("([\\x1A][0-9a-fA-F]{4})"); #else static const int size = 6; QRegularExpression r("(([uU][+]|\\\\[uU])[0-9a-fA-F]{4})"); #endif qsizetype pos = 0; QRegularExpressionMatch match; while ((pos = unescaped.indexOf(r, pos, &match)) != -1) { unescaped.replace(pos++, size, QChar(match.captured(1).right(4).toUShort(0, 16))); } return unescaped; } QString Varicode::rstrip(const QString &str) { qsizetype n = str.size() - 1; for (; n >= 0; --n) { if (str.at(n).isSpace()) { continue; } return str.left(n + 1); } return ""; } QString Varicode::lstrip(const QString &str) { qsizetype len = str.size(); for (int n = 0; n < len; n++) { if (str.at(n).isSpace()) { continue; } return str.mid(n); } return ""; } /* * VARICODE */ QMap Varicode::defaultHuffTable() { return hufftable; } QString Varicode::cqString(int number) { if (!cqs.contains(number)) { return QString{}; } return cqs[number]; } QString Varicode::hbString(int number) { if (!hbs.contains(number)) { return QString{}; } return hbs[number]; } bool Varicode::startsWithCQ(QString text) { foreach (auto cq, cqs.values()) { if (text.startsWith(cq)) { return true; } } return false; } bool Varicode::startsWithHB(QString text) { foreach (auto hb, hbs.values()) { if (text.startsWith(hb)) { return true; } } return false; } QString Varicode::formatSNR(int snr) { if (snr < -60 || snr > 60) { return QString(); } return QString("%1%2") .arg(snr >= 0 ? "+" : "") .arg(snr, snr < 0 ? 3 : 2, 10, QChar('0')); } QString Varicode::checksum16(QString const &input) { auto fromBytes = input.toLocal8Bit(); auto crc = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_16_KERMIT()); auto checksum = Varicode::pack16bits(crc); if (checksum.length() < 3) { checksum += QString(" ").repeated(3 - checksum.length()); } return checksum; } bool Varicode::checksum16Valid(QString const &checksum, QString const &input) { auto fromBytes = input.toLocal8Bit(); auto crc = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_16_KERMIT()); return Varicode::pack16bits(crc) == checksum; } QString Varicode::checksum32(QString const &input) { auto fromBytes = input.toLocal8Bit(); auto crc = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_32_BZIP2()); auto checksum = Varicode::pack32bits(crc); if (checksum.length() < 6) { checksum += QString(" ").repeated(6 - checksum.length()); } return checksum; } bool Varicode::checksum32Valid(QString const &checksum, QString const &input) { auto fromBytes = input.toLocal8Bit(); auto crc = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_32_BZIP2()); return Varicode::pack32bits(crc) == checksum; } QStringList Varicode::parseCallsigns(QString const &input) { QStringList callsigns; QRegularExpression re(compound_callsign_pattern); QRegularExpressionMatchIterator iter = re.globalMatch(input); while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); if (!match.hasMatch()) { continue; } QString callsign = match.captured("callsign").trimmed(); if (!Varicode::isValidCallsign(callsign, nullptr)) { continue; } QRegularExpression m(grid_pattern); if (m.match(callsign).hasMatch()) { continue; } callsigns.append(callsign); } return callsigns; } QStringList Varicode::parseGrids(const QString &input) { QStringList grids; QRegularExpression re(grid_pattern); QRegularExpressionMatchIterator iter = re.globalMatch(input); while (iter.hasNext()) { QRegularExpressionMatch match = iter.next(); if (!match.hasMatch()) { continue; } auto grid = match.captured("grid"); if (grid == "RR73") { continue; } grids.append(grid); } return grids; } QList>> Varicode::huffEncode(const QMap &huff, QString const &text) { QList>> out; int i = 0; auto keys = huff.keys(); std::sort(keys.begin(), keys.end(), [](QString const &a, QString const &b) { auto alen = a.length(); auto blen = b.length(); if (blen < alen) { return true; } if (alen < blen) { return false; } return b < a; }); while (i < text.length()) { bool found = false; foreach (auto ch, keys) { if (QStringView(text.begin() + i, text.end()).startsWith(ch)) { out.append({ch.length(), Varicode::strToBits(huff[ch])}); i += ch.length(); found = true; break; } } if (!found) { i++; } } return out; } QString Varicode::huffDecode(QMap const &huff, QVector const &bitvec) { QString text; QString bits = Varicode::bitsToStr(bitvec); // TODO: jsherer - this is naive... while (bits.length() > 0) { bool found = false; foreach (auto key, huff.keys()) { if (bits.startsWith(huff[key])) { if (key == EOT) { text.append(" "); found = false; break; } text.append(key); bits = bits.mid(huff[key].length()); found = true; } } if (!found) { break; } } return text; } QSet Varicode::huffValidChars(const QMap &huff) { auto const keys = huff.keys(); return QSet(keys.begin(), keys.end()); } // convert char* array of 0 bytes and 1 bytes to bool vector QVector Varicode::bytesToBits(char *bitvec, int n) { QVector bits; for (int i = 0; i < n; i++) { bits.append(bitvec[i] == 0x01); } return bits; } // convert string of 0s and 1s to bool vector QVector Varicode::strToBits(QString const &bitvec) { QVector bits; foreach (auto ch, bitvec) { bits.append(ch == '1'); } return bits; } QString Varicode::bitsToStr(QVector const &bitvec) { QString bits; foreach (auto bit, bitvec) { bits.append(bit ? "1" : "0"); } return bits; } QVector Varicode::intToBits(quint64 value, int expected) { QVector bits; while (value) { bits.prepend((bool)(value & 1)); value = value >> 1; } if (expected) { while (bits.count() < expected) { bits.prepend((bool)0); } } return bits; } quint64 Varicode::bitsToInt(QVector const value) { quint64 v = 0; foreach (bool bit, value) { v = (v << 1) + (int)(bit); } return v; } quint64 Varicode::bitsToInt(QVector::ConstIterator start, int n) { quint64 v = 0; for (int i = 0; i < n; i++) { int bit = (int)(*start); v = (v << 1) + (int)(bit); start++; } return v; } QVector Varicode::bitsListToBits(QList> &list) { QVector out; foreach (auto vec, list) { out += vec; } return out; } quint8 Varicode::unpack5bits(QString const &value) { return alphabet.indexOf(value.at(0)); } // pack a 5-bit value from 0 to 31 into a single character QString Varicode::pack5bits(quint8 packed) { return alphabet.at(packed % 32); } quint8 Varicode::unpack6bits(QString const &value) { return alphabet.indexOf(value.at(0)); } // pack a 6-bit value from 0 to 40 into a single character QString Varicode::pack6bits(quint8 packed) { return alphabet.at(packed % 41); } quint16 Varicode::unpack16bits(QString const &value) { int a = alphabet.indexOf(value.at(0)); int b = alphabet.indexOf(value.at(1)); int c = alphabet.indexOf(value.at(2)); int unpacked = (nalphabet * nalphabet) * a + nalphabet * b + c; if (unpacked > (1 << 16) - 1) { // BASE-41 can produce a value larger than 16 bits... ala "???" == 70643 return 0; } return unpacked & ((1 << 16) - 1); } // pack a 16-bit value into a three character sequence QString Varicode::pack16bits(quint16 packed) { QString out; quint16 tmp = packed / (nalphabet * nalphabet); out.append(alphabet.at(tmp)); tmp = (packed - (tmp * (nalphabet * nalphabet))) / nalphabet; out.append(alphabet.at(tmp)); tmp = packed % nalphabet; out.append(alphabet.at(tmp)); return out; } quint32 Varicode::unpack32bits(QString const &value) { return (quint32)(unpack16bits(value.left(3))) << 16 | unpack16bits(value.right(3)); } QString Varicode::pack32bits(quint32 packed) { quint16 a = (packed & 0xFFFF0000) >> 16; quint16 b = packed & 0xFFFF; return pack16bits(a) + pack16bits(b); } quint64 Varicode::unpack64bits(QString const &value) { return (quint64)(unpack32bits(value.left(6))) << 32 | unpack32bits(value.right(6)); } QString Varicode::pack64bits(quint64 packed) { quint32 a = (packed & 0xFFFFFFFF00000000) >> 32; quint32 b = packed & 0xFFFFFFFF; return pack32bits(a) + pack32bits(b); } // returns the first 64 bits and sets the last 8 bits in pRem quint64 Varicode::unpack72bits(QString const &text, quint8 *pRem) { quint64 value = 0; quint8 rem = 0; quint8 mask2 = ((1 << 2) - 1); for (int i = 0; i < 10; i++) { value |= (quint64)(alphabet72.indexOf(text.at(i))) << (58 - 6 * i); } quint8 remHigh = alphabet72.indexOf(text.at(10)); value |= remHigh >> 2; quint8 remLow = alphabet72.indexOf(text.at(11)); rem = ((remHigh & mask2) << 6) | remLow; if (pRem) *pRem = rem; return value; } QString Varicode::pack72bits(quint64 value, quint8 rem) { QChar packed[12]; // 12 x 6bit characters quint8 mask4 = ((1 << 4) - 1); quint8 mask6 = ((1 << 6) - 1); quint8 remHigh = ((value & mask4) << 2) | (rem >> 6); quint8 remLow = rem & mask6; value = value >> 4; packed[11] = alphabet72.at(remLow); packed[10] = alphabet72.at(remHigh); for (int i = 0; i < 10; i++) { packed[9 - i] = alphabet72.at(value & mask6); value = value >> 6; } return QString(packed, 12); } // // // --- // // // // pack a 4-digit alpha-numeric + space into a 22 bit value // 21 bits for the data + 1 bit for a flag indicator // giving us a total of 5.5 bits per character quint32 Varicode::packAlphaNumeric22(QString const &value, bool isFlag) { QString word = QString(value).replace(QRegularExpression("[^A-Z0-9/ ]"), ""); if (word.length() < 4) { word = word + QString(" ").repeated(4 - word.length()); } quint32 a = 38 * 38 * 38 * alphanumeric.indexOf(word.at(0)); quint32 b = 38 * 38 * alphanumeric.indexOf(word.at(1)); quint32 c = 38 * alphanumeric.indexOf(word.at(2)); quint32 d = alphanumeric.indexOf(word.at(3)); quint32 packed = a + b + c + d; packed = (packed << 1) + (int)isFlag; return packed; } QString Varicode::unpackAlphaNumeric22(quint32 packed, bool *isFlag) { QChar word[4]; if (isFlag) *isFlag = packed & 1; packed = packed >> 1; quint32 tmp = packed % 38; word[3] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[2] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[1] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[0] = alphanumeric.at(tmp); packed = packed / 38; return QString(word, 4); } // pack a 10-digit alpha-numeric + space + forward-slash into a 50 bit value // optionally start with an @ // // [39][38][38][02][38][38][38][02][38][38][38] // [K] [N] [4] [ ] [C] [R] [D] [/] [Q] [R] [P] // [V] [E] [3] [/] [L] [B] [9] [ ] [Y] [H] [X] // [@] [R] [A] [ ] [C] [E] [S] [ ] [ ] [ ] [ ] // // giving us a total of 4.5-5.55 bits per character quint64 Varicode::packAlphaNumeric50(QString const &value) { QString word = QString(value).replace(QRegularExpression("[^A-Z0-9 /@]"), ""); if (word.length() > 3 && word.at(3) != '/') { word.insert(3, ' '); } if (word.length() > 7 && word.at(7) != '/') { word.insert(7, ' '); } if (word.length() < 11) { word = word + QString(" ").repeated(11 - word.length()); } quint64 a = (quint64)38 * 38 * 38 * 2 * 38 * 38 * 38 * 2 * 38 * 38 * alphanumeric.indexOf(word.at(0)); quint64 b = (quint64)38 * 38 * 38 * 2 * 38 * 38 * 38 * 2 * 38 * alphanumeric.indexOf(word.at(1)); quint64 c = (quint64)38 * 38 * 38 * 2 * 38 * 38 * 38 * 2 * alphanumeric.indexOf(word.at(2)); quint64 d = (quint64)38 * 38 * 38 * 2 * 38 * 38 * 38 * (int)(word.at(3) == '/'); quint64 e = (quint64)38 * 38 * 38 * 2 * 38 * 38 * alphanumeric.indexOf(word.at(4)); quint64 f = (quint64)38 * 38 * 38 * 2 * 38 * alphanumeric.indexOf(word.at(5)); quint64 g = (quint64)38 * 38 * 38 * 2 * alphanumeric.indexOf(word.at(6)); quint64 h = (quint64)38 * 38 * 38 * (int)(word.at(7) == '/'); quint64 i = (quint64)38 * 38 * alphanumeric.indexOf(word.at(8)); quint64 j = (quint64)38 * alphanumeric.indexOf(word.at(9)); quint64 k = (quint64)alphanumeric.indexOf(word.at(10)); quint64 packed = a + b + c + d + e + f + g + h + i + j + k; return packed; } QString Varicode::unpackAlphaNumeric50(quint64 packed) { QChar word[11]; quint64 tmp = packed % 38; word[10] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[9] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[8] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 2; word[7] = tmp ? '/' : ' '; packed = packed / 2; tmp = packed % 38; word[6] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[5] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[4] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 2; word[3] = tmp ? '/' : ' '; packed = packed / 2; tmp = packed % 38; word[2] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 38; word[1] = alphanumeric.at(tmp); packed = packed / 38; tmp = packed % 39; word[0] = alphanumeric.at(tmp); packed = packed / 39; auto value = QString(word, 11); return value.replace(" ", ""); } // pack a callsign into a 28-bit value and a boolean portable flag quint32 Varicode::packCallsign(QString const &value, bool *pPortable) { quint32 packed = 0; QString callsign = value.toUpper().trimmed(); if (basecalls.contains(callsign)) { return basecalls[callsign]; } // strip /P if (callsign.endsWith("/P")) { callsign = callsign.left(callsign.length() - 2); if (pPortable) *pPortable = true; } // workaround for swaziland if (callsign.startsWith("3DA0")) { callsign = "3D0" + callsign.mid(4); } // workaround for guinea if (callsign.startsWith("3X") && 'A' <= callsign.at(2) && callsign.at(2) <= 'Z') { callsign = "Q" + callsign.mid(2); } int slen = callsign.length(); if (slen < 2) { return packed; } if (slen > 6) { return packed; } QStringList permutations = {callsign}; if (slen == 2) { permutations.append(" " + callsign + " "); } if (slen == 3) { permutations.append(" " + callsign + " "); permutations.append(callsign + " "); } if (slen == 4) { permutations.append(" " + callsign + " "); permutations.append(callsign + " "); } if (slen == 5) { permutations.append(" " + callsign); permutations.append(callsign + " "); } QString matched; QRegularExpression m(pack_callsign_pattern); foreach (auto permutation, permutations) { auto match = m.match(permutation); if (match.hasMatch()) { matched = match.captured(0); } } if (matched.isEmpty()) { return packed; } if (matched.length() < 6) { return packed; } packed = alphanumeric.indexOf(matched.at(0)); packed = 36 * packed + alphanumeric.indexOf(matched.at(1)); packed = 10 * packed + alphanumeric.indexOf(matched.at(2)); packed = 27 * packed + alphanumeric.indexOf(matched.at(3)) - 10; packed = 27 * packed + alphanumeric.indexOf(matched.at(4)) - 10; packed = 27 * packed + alphanumeric.indexOf(matched.at(5)) - 10; return packed; } QString Varicode::unpackCallsign(quint32 value, bool portable) { foreach (auto key, basecalls.keys()) { if (basecalls[key] == value) { return key; } } QChar word[6]; quint32 tmp = value % 27 + 10; word[5] = alphanumeric.at(tmp); value = value / 27; tmp = value % 27 + 10; word[4] = alphanumeric.at(tmp); value = value / 27; tmp = value % 27 + 10; word[3] = alphanumeric.at(tmp); value = value / 27; tmp = value % 10; word[2] = alphanumeric.at(tmp); value = value / 10; tmp = value % 36; word[1] = alphanumeric.at(tmp); value = value / 36; tmp = value; word[0] = alphanumeric.at(tmp); QString callsign(word, 6); if (callsign.startsWith("3D0")) { callsign = "3DA0" + callsign.mid(3); } if (callsign.startsWith("Q") and 'A' <= callsign.at(1) && callsign.at(1) <= 'Z') { callsign = "3X" + callsign.mid(1); } if (portable) { callsign = callsign.trimmed() + "/P"; } return callsign.trimmed(); } QString Varicode::deg2grid(float dlong, float dlat) { QChar grid[6]; if (dlong < -180) { dlong += 360; } if (dlong > 180) { dlong -= 360; } int nlong = int(60.0 * (180.0 - dlong) / 5); int n1 = nlong / 240; int n2 = (nlong - 240 * n1) / 24; int n3 = (nlong - 240 * n1 - 24 * n2); grid[0] = QChar('A' + n1); grid[2] = QChar('0' + n2); grid[4] = QChar('a' + n3); int nlat = int(60.0 * (dlat + 90) / 2.5); n1 = nlat / 240; n2 = (nlat - 240 * n1) / 24; n3 = (nlat - 240 * n1 - 24 * n2); grid[1] = QChar('A' + n1); grid[3] = QChar('0' + n2); grid[5] = QChar('a' + n3); return QString(grid, 6); } QPair Varicode::grid2deg(QString const &grid) { QPair longLat; QString g = grid; if (g.length() < 6) { g = grid.left(4) + "mm"; } g = g.left(4).toUpper() + g.right(2).toLower(); int nlong = 180 - 20 * (g.at(0).toLatin1() - 'A'); int n20d = 2 * (g.at(2).toLatin1() - '0'); float xminlong = 5 * (g.at(4).toLatin1() - 'a' + 0.5); float dlong = nlong - n20d - xminlong / 60.0; int nlat = -90 + 10 * (g.at(1).toLatin1() - 'A') + g.at(3).toLatin1() - '0'; float xminlat = 2.5 * (g.at(5).toLatin1() - 'a' + 0.5); float dlat = nlat + xminlat / 60.0; longLat.first = dlong; longLat.second = dlat; return longLat; } // pack a 4-digit maidenhead grid locator into a 15-bit value quint16 Varicode::packGrid(QString const &value) { QString grid = QString(value).trimmed(); if (grid.length() < 4) { return (1 << 15) - 1; } auto pair = Varicode::grid2deg(grid.left(4)); int ilong = pair.first; int ilat = pair.second + 90; return ((ilong + 180) / 2) * 180 + ilat; } QString Varicode::unpackGrid(quint16 value) { if (value > nbasegrid) { return ""; } float dlat = value % 180 - 90; float dlong = value / 180 * 2 - 180 + 2; return Varicode::deg2grid(dlong, dlat).left(4); } // pack a number or snr into an integer between 0 & 62 quint8 Varicode::packNum(QString const &num, bool *ok) { int inum = 0; if (num.isEmpty()) { if (ok) *ok = false; return inum; } inum = qMax(-30, qMin(num.toInt(ok, 10), 31)); return inum + 30 + 1; } // pack a reduced fidelity command and a number into the extra bits provided // between nbasegrid and nmaxgrid quint8 Varicode::packCmd(quint8 cmd, quint8 num, bool *pPackedNum) { // quint8 allowed = nmaxgrid - nbasegrid - 1; // if cmd == snr quint8 value = 0; auto cmdStr = directed_cmds.key(cmd); if (Varicode::isSNRCommand(cmdStr)) { // 8 bits - 1 bit flag + 1 bit type + 6 bit number // [1][X][6] // X = 0 == SNR // X = 1 == DEADBEEF value = ((1 << 1) | (int)(cmdStr == " HEARTBEAT SNR")) << 6; value = value + (num & ((1 << 6) - 1)); if (pPackedNum) *pPackedNum = true; } else { value = cmd & ((1 << 7) - 1); if (pPackedNum) *pPackedNum = false; } return value; } quint8 Varicode::unpackCmd(quint8 value, quint8 *pNum) { // if the first bit is 1, this is an SNR with a number encoded in the lower // 6 bits if (value & (1 << 7)) { if (pNum) *pNum = value & ((1 << 6) - 1); auto cmd = directed_cmds[" SNR"]; // sending digits with ACKS this way was deprecated in 2.2 (for reasons) // so we zero them out when unpacking so we don't display them even if // they were encoded that way. if (value & (1 << 6)) { cmd = directed_cmds[" HEARTBEAT SNR"]; } return cmd; } else { if (pNum) *pNum = 0; return value & ((1 << 7) - 1); } } bool Varicode::isSNRCommand(const QString &cmd) { return directed_cmds.contains(cmd) && snr_cmds.contains(directed_cmds[cmd]); } bool Varicode::isCommandAllowed(const QString &cmd) { return directed_cmds.contains(cmd) && allowed_cmds.contains(directed_cmds[cmd]); } bool Varicode::isCommandBuffered(const QString &cmd) { return directed_cmds.contains(cmd) && (cmd.contains(" ") || buffered_cmds.contains(directed_cmds[cmd])); } int Varicode::isCommandChecksumed(const QString &cmd) { if (!directed_cmds.contains(cmd) || !checksum_cmds.contains(directed_cmds[cmd])) { return 0; } return checksum_cmds[directed_cmds[cmd]]; } bool Varicode::isCommandAutoreply(const QString &cmd) { return directed_cmds.contains(cmd) && (autoreply_cmds.contains(directed_cmds[cmd])); } bool isValidCompoundCallsign(QStringView callsign) { // compound calls cannot be > 9 characters after removing the / if (callsign.length() - callsign.count('/') > 9) { return false; } // compound is valid when it is: // 1) a group call (starting with @) // 2) an actual compound call (containing /) that is not a base call // 3) is greater than two characters and has an alphanumeric character // sequence // // this is so arbitrary words < 10 characters in length don't end up coded // as callsigns if (const auto index = callsign.indexOf('/'); index != -1) { return !basecalls.contains(callsign.first(index).toString()); } if (callsign.startsWith('@')) { return true; } if (callsign.length() > 2 && QRegularExpression("[0-9][A-Z]|[A-Z][0-9]") #if (QT_VERSION < QT_VERSION_CHECK(6, 5, 0)) .match(callsign) #else .matchView(callsign) #endif .hasMatch()) { return true; } return false; } bool Varicode::isValidCallsign(const QString &callsign, bool *pIsCompound) { if (basecalls.contains(callsign)) { if (pIsCompound) *pIsCompound = false; return true; } auto match = QRegularExpression(base_callsign_pattern).match(callsign); if (match.hasMatch() && (match.capturedLength() == callsign.length())) { if (pIsCompound) *pIsCompound = false; return callsign.length() > 2 && QRegularExpression("[0-9][A-Z]|[A-Z][0-9]") .match(callsign) .hasMatch(); } match = QRegularExpression("^" + compound_callsign_pattern).match(callsign); if (match.hasMatch() && (match.capturedLength() == callsign.length())) { bool isValid = isValidCompoundCallsign(match.capturedView(0)); if (pIsCompound) *pIsCompound = isValid; return isValid; } if (pIsCompound) *pIsCompound = false; return false; } bool Varicode::isCompoundCallsign(const QString &callsign) { if (basecalls.contains(callsign) && !callsign.startsWith("@")) { return false; } auto match = QRegularExpression(base_callsign_pattern).match(callsign); if (match.hasMatch() && (match.capturedLength() == callsign.length())) { return false; } match = QRegularExpression("^" + compound_callsign_pattern).match(callsign); if (!match.hasMatch() || (match.capturedLength() != callsign.length())) { return false; } bool isValid = isValidCompoundCallsign(match.capturedView(0)); qCDebug(varicode_js8) << "is valid compound?" << match.capturedView(0) << isValid; return isValid; } bool Varicode::isGroupAllowed(const QString &group) { const QSet disallowed = { "@APRSIS", "@JS8NET", }; return !disallowed.contains(group); } // CQCQCQ EM73 // CQ DX EM73 // CQ QRP EM73 // HB EM73 QString Varicode::packHeartbeatMessage(QString const &text, const QString &callsign, int *n) { QString frame; auto parsedText = heartbeat_re.match(text); if (!parsedText.hasMatch()) { if (n) *n = 0; return frame; } auto extra = parsedText.captured("grid"); // Heartbeat Alt Type // --------------- // 1 0 HB // 1 1 CQ auto type = parsedText.captured("type"); auto isAlt = type.startsWith("CQ"); if (callsign.isEmpty()) { if (n) *n = 0; return frame; } quint16 packed_extra = nmaxgrid; // which will display an empty string if (extra.length() == 4 && QRegularExpression(grid_pattern).match(extra).hasMatch()) { packed_extra = Varicode::packGrid(extra); } quint8 cqNumber = hbs.key(type, 0); if (isAlt) { packed_extra |= (1 << 15); cqNumber = cqs.key(type, 0); } frame = packCompoundFrame(callsign, Varicode::FrameHeartbeat, packed_extra, cqNumber); if (frame.isEmpty()) { if (n) *n = 0; return frame; } if (n) *n = parsedText.captured(0).length(); return frame; } QStringList Varicode::unpackHeartbeatMessage(const QString &text, quint8 *pType, bool *isAlt, quint8 *pBits3) { quint8 type = Varicode::FrameHeartbeat; quint16 num = nmaxgrid; quint8 bits3 = 0; QStringList unpacked = unpackCompoundFrame(text, &type, &num, &bits3); if (unpacked.isEmpty() || type != Varicode::FrameHeartbeat) { return QStringList{}; } unpacked.append(Varicode::unpackGrid(num & ((1 << 15) - 1))); if (isAlt) *isAlt = (num & (1 << 15)); if (pType) *pType = type; if (pBits3) *pBits3 = bits3; return unpacked; } // KN4CRD/XXXX EM73 // XXXX/KN4CRD EM73 // KN4CRD/P QString Varicode::packCompoundMessage(QString const &text, int *n) { QString frame; qCDebug(varicode_js8) << "trying to pack compound message" << text; auto parsedText = compound_re.match(text); if (!parsedText.hasMatch()) { qCDebug(varicode_js8) << "no match for compound message" << text; if (n) *n = 0; return frame; } qCDebug(varicode_js8) << parsedText.capturedTexts(); QString callsign = parsedText.captured("callsign"); QString grid = parsedText.captured("grid"); QString cmd = parsedText.captured("cmd"); QString num = parsedText.captured("num").trimmed(); if (callsign.isEmpty()) { if (n) *n = 0; return frame; } quint8 type = Varicode::FrameCompound; quint16 extra = nmaxgrid; qCDebug(varicode_js8) << "try pack cmd" << cmd << directed_cmds.contains(cmd) << Varicode::isCommandAllowed(cmd); if (!cmd.isEmpty() && directed_cmds.contains(cmd) && Varicode::isCommandAllowed(cmd)) { bool packedNum = false; quint8 inum = Varicode::packNum(num, nullptr); extra = nusergrid + Varicode::packCmd(directed_cmds[cmd], inum, &packedNum); type = Varicode::FrameCompoundDirected; } else if (!grid.isEmpty()) { extra = Varicode::packGrid(grid); } frame = Varicode::packCompoundFrame(callsign, type, extra, 0); if (n) *n = parsedText.captured(0).length(); return frame; } QStringList Varicode::unpackCompoundMessage(const QString &text, quint8 *pType, quint8 *pBits3) { quint8 type = Varicode::FrameCompound; quint16 extra = nmaxgrid; quint8 bits3 = 0; QStringList unpacked = unpackCompoundFrame(text, &type, &extra, &bits3); if (unpacked.isEmpty() || (type != Varicode::FrameCompound && type != Varicode::FrameCompoundDirected)) { return QStringList{}; } if (extra <= nbasegrid) { unpacked.append(" " + Varicode::unpackGrid(extra)); } else if (nusergrid <= extra && extra < nmaxgrid) { // if this is a grid that is higer than the usergrid reference, treat // this as an SNR command quint8 num = 0; auto cmd = Varicode::unpackCmd(extra - nusergrid, &num); auto cmdStr = directed_cmds.key(cmd); unpacked.append(cmdStr); if (Varicode::isSNRCommand(cmdStr)) { unpacked.append(Varicode::formatSNR(num - 31)); } } if (pType) *pType = type; if (pBits3) *pBits3 = bits3; return unpacked; } QString Varicode::packCompoundFrame(const QString &callsign, quint8 type, quint16 num, quint8 bits3) { QString frame; // needs to be a compound type... if (type == Varicode::FrameData || type == Varicode::FrameDirected) { return frame; } quint8 packed_flag = type; quint64 packed_callsign = Varicode::packAlphaNumeric50(callsign); if (packed_callsign == 0) { return frame; } quint16 mask11 = ((1 << 11) - 1) << 5; quint8 mask5 = (1 << 5) - 1; quint16 packed_11 = (num & mask11) >> 5; quint8 packed_5 = num & mask5; quint8 packed_8 = (packed_5 << 3) | bits3; // [3][50][11],[5][3] = 72 auto bits = (Varicode::intToBits(packed_flag, 3) + Varicode::intToBits(packed_callsign, 50) + Varicode::intToBits(packed_11, 11)); return Varicode::pack72bits(Varicode::bitsToInt(bits), packed_8); } QStringList Varicode::unpackCompoundFrame(const QString &text, quint8 *pType, quint16 *pNum, quint8 *pBits3) { QStringList unpacked; if (text.length() < 12 || text.contains(" ")) { return unpacked; } // [3][50][11],[5][3] = 72 quint8 packed_8 = 0; auto bits = Varicode::intToBits(Varicode::unpack72bits(text, &packed_8), 64); quint8 packed_5 = packed_8 >> 3; quint8 packed_3 = packed_8 & ((1 << 3) - 1); quint8 packed_flag = Varicode::bitsToInt(bits.mid(0, 3)); // needs to be a ping type... if (packed_flag == Varicode::FrameData || packed_flag == Varicode::FrameDirected) { return unpacked; } quint64 packed_callsign = Varicode::bitsToInt(bits.mid(3, 50)); quint16 packed_11 = Varicode::bitsToInt(bits.mid(53, 11)); QString callsign = Varicode::unpackAlphaNumeric50(packed_callsign); quint16 num = (packed_11 << 5) | packed_5; if (pType) *pType = packed_flag; if (pNum) *pNum = num; if (pBits3) *pBits3 = packed_3; unpacked.append(callsign); unpacked.append(""); return unpacked; } // J1Y ACK // J1Y? // KN4CRD: J1Y$ // KN4CRD: J1Y! HELLO WORLD QString Varicode::packDirectedMessage(const QString &text, const QString &mycall, QString *pTo, bool *pToCompound, QString *pCmd, QString *pNum, int *n) { QString frame; auto match = directed_re.match(text); if (!match.hasMatch()) { if (n) *n = 0; return frame; } QString from = mycall; bool isFromCompound = Varicode::isCompoundCallsign(from); if (isFromCompound) { from = "<....>"; } QString to = match.captured("callsign"); QString cmd = match.captured("cmd"); QString num = match.captured("num"); // ensure we have a directed command if (cmd.isEmpty()) { if (n) *n = 0; return frame; } // ensure we have a valid callsign bool isToCompound = false; bool validToCallsign = (to != mycall) && Varicode::isValidCallsign(to, &isToCompound); if (!validToCallsign) { qCDebug(varicode_js8) << "to" << to << "is not a valid callsign"; if (n) *n = 0; return frame; } // return back the parsed "to" field if (pTo) *pTo = to; if (pToCompound) *pToCompound = isToCompound; // then replace the current processing with a placeholder that we _can_ pack // into a directed command, because later we'll send the "to" field in a // compound frame using the results of this directed pack if (isToCompound) { to = "<....>"; } qCDebug(varicode_js8) << "directed" << validToCallsign << isToCompound << to; // validate command if (!Varicode::isCommandAllowed(cmd) && !Varicode::isCommandAllowed(cmd.trimmed())) { if (n) *n = 0; return frame; } // packing general number... bool numOK = false; quint8 inum = Varicode::packNum(num.trimmed(), &numOK); if (numOK) { if (pNum) *pNum = num; } bool portable_from = false; quint32 packed_from = Varicode::packCallsign(from, &portable_from); bool portable_to = false; quint32 packed_to = Varicode::packCallsign(to, &portable_to); if (packed_from == 0 || packed_to == 0) { if (n) *n = 0; return frame; } QString cmdOut; quint8 packed_cmd = 0; if (directed_cmds.contains(cmd)) { cmdOut = cmd; packed_cmd = directed_cmds[cmdOut]; } if (directed_cmds.contains(cmd.trimmed())) { cmdOut = cmd.trimmed(); packed_cmd = directed_cmds[cmdOut]; } quint8 packed_flag = Varicode::FrameDirected; quint8 packed_extra = ((((int)portable_from) << 7) + (((int)portable_to) << 6) + inum); // [3][28][28][5],[2][6] = 72 auto bits = (Varicode::intToBits(packed_flag, 3) + Varicode::intToBits(packed_from, 28) + Varicode::intToBits(packed_to, 28) + Varicode::intToBits(packed_cmd % 32, 5)); if (pCmd) *pCmd = cmdOut; if (n) *n = match.captured(0).length(); return Varicode::pack72bits(Varicode::bitsToInt(bits), packed_extra); } QStringList Varicode::unpackDirectedMessage(const QString &text, quint8 *pType) { QStringList unpacked; if (text.length() < 12 || text.contains(" ")) { return unpacked; } // [3][28][22][11],[2][6] = 72 quint8 extra = 0; auto bits = Varicode::intToBits(Varicode::unpack72bits(text, &extra), 64); quint8 packed_flag = Varicode::bitsToInt(bits.mid(0, 3)); if (packed_flag != Varicode::FrameDirected) { return unpacked; } quint32 packed_from = Varicode::bitsToInt(bits.mid(3, 28)); quint32 packed_to = Varicode::bitsToInt(bits.mid(31, 28)); quint8 packed_cmd = Varicode::bitsToInt(bits.mid(59, 5)); bool portable_from = ((extra >> 7) & 1) == 1; bool portable_to = ((extra >> 6) & 1) == 1; extra = extra % 64; QString from = Varicode::unpackCallsign(packed_from, portable_from); QString to = Varicode::unpackCallsign(packed_to, portable_to); QString cmd = directed_cmds.key(packed_cmd % 32); unpacked.append(from); unpacked.append(to); unpacked.append(cmd); if (extra != 0) { if (Varicode::isSNRCommand(cmd)) { unpacked.append(Varicode::formatSNR((int)extra - 31)); } else { unpacked.append(QString("%1").arg(extra - 31)); } } if (pType) *pType = packed_flag; return unpacked; } QString packHuffMessage(const QString &input, const QVector prefix, int *n) { static const int frameSize = 72; QString frame; // [1][1][70] = 72 // The first bit is a flag that indicates this is a data frame, technically // encoded as [100] but, since none of the other frame types start with a 0, // we can drop the two zeros and use them for encoding the first two bits of // the actuall data sent. boom! The second bit is a flag that indicates this // is not compressed frame (huffman coding) QVector frameBits; if (!prefix.isEmpty()) { frameBits << prefix; } int i = 0; // only pack huff messages that only contain valid chars QString::const_iterator it; QSet validChars = Varicode::huffValidChars(Varicode::defaultHuffTable()); for (it = input.constBegin(); it != input.constEnd(); it++) { auto ch = (*it).toUpper(); if (!validChars.contains(ch)) { if (n) *n = 0; return frame; } } // pack using the default huff table foreach (auto pair, Varicode::huffEncode(Varicode::defaultHuffTable(), input)) { auto charN = pair.first; auto charBits = pair.second; if (frameBits.length() + charBits.length() < frameSize) { frameBits += charBits; i += charN; continue; } break; } qCDebug(varicode_js8) << "Huff bits" << frameBits.length() << "chars" << i; int pad = frameSize - frameBits.length(); if (pad) { // the way we will pad is this... // set the bit after the frame to 0 and every bit after that a 1 // to unpad, seek from the end of the bits until you hit a zero... the // rest is the actual frame. for (int i = 0; i < pad; i++) { frameBits.append(i == 0 ? (bool)0 : (bool)1); } } quint64 value = Varicode::bitsToInt(frameBits.constBegin(), 64); quint8 rem = (quint8)Varicode::bitsToInt(frameBits.constBegin() + 64, 8); frame = Varicode::pack72bits(value, rem); if (n) *n = i; return frame; } QString packCompressedMessage(const QString &input, QVector prefix, int *n) { static const int frameSize = 72; QString frame; // [1][1][70] = 72 // The first bit is a flag that indicates this is a data frame, technically // encoded as [100] but, since none of the other frame types start with a 1, // we can drop the two zeros and use them for encoding the first two bits of // the actuall data sent. boom! The second bit is a flag that indicates this // is a compressed frame (dense coding) For fast modes, we don't use the // prefix since it is indicated by the JS8CallData flag. QVector frameBits; if (!prefix.isEmpty()) { frameBits << prefix; } int i = 0; foreach (auto pair, JSC::compress(input)) { auto bits = pair.first; auto chars = pair.second; if (frameBits.length() + bits.length() < frameSize) { frameBits.append(bits); i += chars; continue; } break; } qCDebug(varicode_js8) << "Compressed bits" << frameBits.length() << "chars" << i; int pad = frameSize - frameBits.length(); if (pad) { // the way we will pad is this... // set the bit after the frame to 0 and every bit after that a 1 // to unpad, seek from the end of the bits until you hit a zero... the // rest is the actual frame. for (int i = 0; i < pad; i++) { frameBits.append(i == 0 ? (bool)0 : (bool)1); } } quint64 value = Varicode::bitsToInt(frameBits.constBegin(), 64); quint8 rem = (quint8)Varicode::bitsToInt(frameBits.constBegin() + 64, 8); frame = Varicode::pack72bits(value, rem); if (n) *n = i; return frame; } // TODO: DEPRECATED in 2.2 (we will eventually stop transmitting these frames) // pack data message using 70 bits available flagged as data by the first 2 bits QString Varicode::packDataMessage(const QString &input, int *n) { QString huffFrame; int huffChars = 0; huffFrame = packHuffMessage(input, {true, false}, &huffChars); QString compressedFrame; int compressedChars = 0; compressedFrame = packCompressedMessage(input, {true, true}, &compressedChars); if (huffChars > compressedChars) { if (n) *n = huffChars; return huffFrame; } else { if (n) *n = compressedChars; return compressedFrame; } } // TODO: DEPRECATED in 2.2 (still available for decoding legacy frames, but will // eventually no longer be decodable) unpack data message using 70 bits // available flagged as data by the first 2 bits QString Varicode::unpackDataMessage(const QString &text) { QString unpacked; if (text.length() < 12 || text.contains(" ")) { return unpacked; } quint8 rem = 0; quint64 value = Varicode::unpack72bits(text, &rem); auto bits = Varicode::intToBits(value, 64) + Varicode::intToBits(rem, 8); bool isData = bits.at(0); if (!isData) { return unpacked; } bits = bits.mid(1); bool compressed = bits.at(0); int n = bits.lastIndexOf(0); // trim off the pad bits bits = bits.mid(1, n - 1); if (compressed) { // partial word (s,c)-dense coding with code tables unpacked = JSC::decompress(bits); } else { // huff decode the bits (without escapes) unpacked = Varicode::huffDecode(Varicode::defaultHuffTable(), bits); } return unpacked; } #define JS8_FAST_DATA_CAN_USE_HUFF 0 // pack data message using the full 72 bits available (with the data flag in the // i3bit header) QString Varicode::packFastDataMessage(const QString &input, int *n) { #if JS8_FAST_DATA_CAN_USE_HUFF QString huffFrame; int huffChars = 0; huffFrame = packHuffMessage(input, {false}, &huffChars); QString compressedFrame; int compressedChars = 0; compressedFrame = packCompressedMessage(input, {true}, &compressedChars); if (huffChars > compressedChars) { if (n) *n = huffChars; return huffFrame; } else { if (n) *n = compressedChars; return compressedFrame; } #else QString compressedFrame; int compressedChars = 0; compressedFrame = packCompressedMessage(input, {}, &compressedChars); if (n) *n = compressedChars; return compressedFrame; #endif } // unpack data message using the full 72 bits available (with the data flag in // the i3bit header) QString Varicode::unpackFastDataMessage(const QString &text) { QString unpacked; if (text.length() < 12 || text.contains(" ")) { return unpacked; } quint8 rem = 0; quint64 value = Varicode::unpack72bits(text, &rem); auto bits = Varicode::intToBits(value, 64) + Varicode::intToBits(rem, 8); #if JS8_FAST_DATA_CAN_USE_HUFF bool compressed = bits.at(0); int n = bits.lastIndexOf(0); // trim off the pad bits bits = bits.mid(1, n - 1); if (compressed) { // partial word (s,c)-dense coding with code tables unpacked = JSC::decompress(bits); } else { // huff decode the bits (without escapes) unpacked = Varicode::huffDecode(Varicode::defaultHuffTable(), bits); } #else int n = bits.lastIndexOf(0); // trim off the pad bits bits = bits.mid(0, n); // partial word (s,c)-dense coding with code tables unpacked = JSC::decompress(bits); #endif return unpacked; } // TODO: remove the dependence on providing all this data? QList> Varicode::buildMessageFrames(QString const &mycall, QString const &mygrid, QString const &selectedCall, QString const &text, bool forceIdentify, bool forceData, int submode, MessageInfo *pInfo) { #define ALLOW_SEND_COMPOUND 1 #define ALLOW_SEND_COMPOUND_DIRECTED 1 #define AUTO_PREPEND_DIRECTED 1 #define AUTO_REMOVE_MYCALL 1 #define AUTO_PREPEND_DIRECTED_ALLOW_TEXT_CALLSIGNS 1 #define ALLOW_FORCE_IDENTIFY 1 bool mycallCompound = Varicode::isCompoundCallsign(mycall); QList> allFrames; #if JS8_NO_MULTILINE // auto lines = text.split(QRegExp("[\\r\\n]"), QString::SkipEmptyParts); #else QStringList lines = {text}; #endif foreach (QString line, lines) { QList> lineFrames; // once we find a directed call, data encode the rest of the line. bool hasDirected = false; // do the same for when we have sent data... bool hasData = false; // or if we're forcing data to be sent... if (forceData) { forceIdentify = false; hasData = true; } #if AUTO_REMOVE_MYCALL // remove our callsign from the start of the line... if (line.startsWith(mycall + ":") || line.startsWith(mycall + " ")) { line = lstrip(line.mid(mycall.length() + 1)); } #endif #if AUTO_RSTRIP_WHITESPACE // remove trailing whitespace as long as there are characters left // afterwards auto rline = rstrip(line); if (!rline.isEmpty()) { line = rline; } #endif #if AUTO_PREPEND_DIRECTED // see if we need to prepend the directed call to the line... // if we have a selected call and the text doesn't start with that // call... and if this isn't a raw message (starting with "`")... // then... if (!selectedCall.isEmpty() && !line.startsWith(selectedCall) && !line.startsWith("`") && !forceData) { bool lineStartsWithBaseCall = (line.startsWith("@ALLCALL") || Varicode::startsWithCQ(line) || Varicode::startsWithHB(line)); #if AUTO_PREPEND_DIRECTED_ALLOW_TEXT_CALLSIGNS auto calls = Varicode::parseCallsigns(line); bool lineStartsWithStandardCall = !calls.isEmpty() && line.startsWith(calls.first()) && calls.first().length() > 3; #else bool lineStartsWithStandardCall = false; #endif if (lineStartsWithBaseCall || lineStartsWithStandardCall) { // pass } else { // if the message doesn't start with a base call // and if there are no other callsigns in this message // or if the first callsign in the message isn't at the // beginning... then we should be auto-prefixing this line with // the selected call auto sep = line.startsWith(" ") ? "" : " "; line = QString("%1%2%3").arg(selectedCall).arg(sep).arg(line); } } #endif while (line.size() > 0) { QString frame; bool useBcn = false; #if ALLOW_SEND_COMPOUND bool useCmp = false; #endif bool useDir = false; bool useDat = false; int l = 0; QString bcnFrame = Varicode::packHeartbeatMessage(line, mycall, &l); #if ALLOW_SEND_COMPOUND int o = 0; QString cmpFrame = Varicode::packCompoundMessage(line, &o); #endif int n = 0; QString dirCmd; QString dirTo; QString dirNum; bool dirToCompound = false; QString dirFrame = Varicode::packDirectedMessage( line, mycall, &dirTo, &dirToCompound, &dirCmd, &dirNum, &n); if (dirToCompound) { qCDebug(varicode_js8) << "directed message to field is compound" << dirTo; } #if ALLOW_FORCE_IDENTIFY // if we're sending a data message, then ensure our callsign is // included automatically bool isLikelyDataFrame = lineFrames.isEmpty() && selectedCall.isEmpty() && dirTo.isEmpty() && l == 0 && o == 0; if (forceIdentify && isLikelyDataFrame && !line.contains(mycall)) { line = QString("%1: %2").arg(mycall).arg(line); } #endif int m = 0; bool fastDataFrame = false; QString datFrame; // TODO: DEPRECATED in 2.2 (the following release will remove // transmission of these frames) if (submode == Varicode::JS8CallNormal) { datFrame = Varicode::packDataMessage(line, &m); fastDataFrame = false; } else { datFrame = Varicode::packFastDataMessage(line, &m); fastDataFrame = true; } // if this parses to a standard FT8 free text message // but it can be parsed as a directed message, then we // should send the directed version. if we've already sent // a directed message or a data frame, we will only follow it // with more data frames. if (!hasDirected && !hasData && l > 0) { useBcn = true; hasDirected = false; frame = bcnFrame; } #if ALLOW_SEND_COMPOUND else if (!hasDirected && !hasData && o > 0) { useCmp = true; hasDirected = false; frame = cmpFrame; } #endif else if (!hasDirected && !hasData && n > 0) { useDir = true; hasDirected = true; frame = dirFrame; } else if (m > 0) { useDat = true; hasData = true; frame = datFrame; } if (useBcn) { lineFrames.append({frame, Varicode::JS8Call}); line = line.mid(l); } #if ALLOW_SEND_COMPOUND if (useCmp) { lineFrames.append({frame, Varicode::JS8Call}); line = line.mid(o); } #endif if (useDir) { bool shouldUseStandardFrame = true; #if ALLOW_SEND_COMPOUND_DIRECTED /** * We have a few special cases when we are sending to a compound *call, or our call is a compound call, or both. CASE 0: *Non-compound: KN4CRD: J1Y ACK * -> One standard directed message frame * * CASE 1: Compound From: KN4CRD/P: J1Y ACK * -> One standard compound frame, followed by a standard *directed message frame with placeholder * -> The second standard directed frame _could_ be replaced *with a compound directed frame * -> then <....>: J1Y ACK * -> then * * CASE 2: Compound To: KN4CRD: J1Y/P ACK * -> One standard compound frame, followed by a compound *directed frame * -> then * * CASE 3: Compound From & To: KN4CRD/P: J1Y/P ACK * -> One standard compound frame, followed by a compound *directed frame * -> then **/ if (mycallCompound || dirToCompound) { qCDebug(varicode_js8) << "compound?" << mycallCompound << dirToCompound; // Cases 1, 2, 3 all send a standard compound frame first... QString deCompoundMessage = QString("`%1 %2").arg(mycall).arg(mygrid); QString deCompoundFrame = Varicode::packCompoundMessage( deCompoundMessage, nullptr); if (!deCompoundFrame.isEmpty()) { lineFrames.append({deCompoundFrame, Varicode::JS8Call}); } // Followed, by a standard OR compound directed message... QString dirCompoundMessage = QString("`%1%2%3").arg(dirTo).arg(dirCmd).arg(dirNum); QString dirCompoundFrame = Varicode::packCompoundMessage( dirCompoundMessage, nullptr); if (!dirCompoundFrame.isEmpty()) { lineFrames.append( {dirCompoundFrame, Varicode::JS8Call}); } shouldUseStandardFrame = false; } #endif if (shouldUseStandardFrame) { // otherwise, just send the standard directed frame lineFrames.append({frame, Varicode::JS8Call}); } line = line.mid(n); // generate a checksum for buffered commands with line data if (Varicode::isCommandBuffered(dirCmd) && !line.isEmpty()) { qCDebug(varicode_js8) << "generating checksum for line" << line << line.mid(1); // strip leading whitespace after a buffered directed // command line = lstrip(line); qCDebug(varicode_js8) << "before:" << line; #if 1 bool skipAprsChecksum = (dirTo.compare("@APRSIS", Qt::CaseInsensitive) == 0); int checksumSize = skipAprsChecksum ? 0 : Varicode::isCommandChecksumed(dirCmd); #else int checksumSize = 0; #endif if (checksumSize == 32) { line = line + " " + Varicode::checksum32(line); } else if (checksumSize == 16) { line = line + " " + Varicode::checksum16(line); } else if (checksumSize == 0) { // pass qCDebug(varicode_js8) << "no checksum required for cmd" << dirCmd; } qCDebug(varicode_js8) << "after:" << line; } if (pInfo) { pInfo->dirCmd = dirCmd; pInfo->dirTo = dirTo; pInfo->dirNum = dirNum; } } if (useDat) { // use the standard data frame lineFrames.append({frame, fastDataFrame ? Varicode::JS8CallData : Varicode::JS8Call}); line = line.mid(m); } } if (!lineFrames.isEmpty()) { lineFrames.first().second |= Varicode::JS8CallFirst; lineFrames.last().second |= Varicode::JS8CallLast; } allFrames.append(lineFrames); } return allFrames; } BuildMessageFramesThread::BuildMessageFramesThread( const QString &mycall, const QString &mygrid, const QString &selectedCall, const QString &text, bool forceIdentify, bool forceData, int submode, QObject *parent) : QThread(parent), m_mycall{mycall}, m_mygrid{mygrid}, m_selectedCall{selectedCall}, m_text{text}, m_forceIdentify{forceIdentify}, m_forceData{forceData}, m_submode{submode} {} void BuildMessageFramesThread::run() { auto results = Varicode::buildMessageFrames(m_mycall, m_mygrid, m_selectedCall, m_text, m_forceIdentify, m_forceData, m_submode); // TODO: jsherer - we wouldn't normally use DecodedText.h here... but it's // useful for computing the actual frames transmitted. QStringList textList; qCDebug(varicode_js8) << "frames:"; foreach (auto frame, results) { auto dt = DecodedText(frame.first, frame.second, m_submode); qCDebug(varicode_js8) << "->" << frame << dt.message() << Varicode::frameTypeString(dt.frameType()) << "submode:" << m_submode; textList.append(dt.message()); } auto transmitText = textList.join(""); emit resultReady(transmitText, results.length()); } Varicode::SubmodeType Varicode::intToSubmode(int sm) { switch (sm) { case Varicode::JS8CallNormal: return Varicode::JS8CallNormal; case Varicode::JS8CallFast: return Varicode::JS8CallFast; case Varicode::JS8CallTurbo: return Varicode::JS8CallTurbo; case Varicode::JS8CallSlow: return Varicode::JS8CallSlow; case Varicode::JS8CallUltra: return Varicode::JS8CallUltra; }; throw std::invalid_argument{ boost::str(boost::format("Unexpected JS8 submode %1%") % sm)}; } Q_LOGGING_CATEGORY(varicode_js8, "varicode.js8", QtWarningMsg)