/** * This file is part of FT8Call. * * 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, either 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * (C) 2018 Jordan Sherer - All Rights Reserved * **/ #include #include #include #define CRCPP_INCLUDE_ESOTERIC_CRC_DEFINITIONS #include "crc.h" #include "varicode.h" 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 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 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 // directed queries {"?", 0 }, // query snr {"@", 1 }, // query qth {"&", 2 }, // query station message {"$", 3 }, // query station(s) heard {"^", 4 }, // query grid {">", 5 }, // relay message //{"|", 6 }, // retransmit message //{"!", 7 }, // alert message {"#", 8 }, // all or nothing message // {"=", 9 }, // unused // {"/", 10 }, // unused // {"/", 11 }, // unused // {"/", 12 }, // unused // {"/", 13 }, // unused // directed responses {" QSO", 13 }, // can you communicate with? i can communicate with {" APRS:", 14 }, // send an aprs packet {" GRID", 15 }, // this is my current grid locator {" QTC", 16 }, // this is my qtc message {" QTH", 17 }, // this is my qth message {" FB", 18 }, // fine business {" HW CPY?", 19 }, // how do you copy? {" HEARING", 20 }, // i am hearing the following stations {" RR", 21 }, // roger roger {" QSL?", 22 }, // do you copy? {" QSL", 23 }, // i copy // {"", 24 }, // unused {" SNR", 25 }, // seen a station at the provided snr {" NO", 26 }, // negative confirm {" YES", 27 }, // confirm {" 73", 28 }, // best regards, end of contact {" ACK", 29 }, // acknowledge {" AGN?", 30 }, // repeat message {" ", 31 }, // send freetext }; QSet allowed_cmds = {0, 1, 2, 3, 4, 5, /*6,*/ /*7,*/ 8, /*...*/ 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, /*24,*/ 25, 26, 27, 28, 29, 30, 31}; QSet buffered_cmds = {5, /*6,*/ /*7,*/ 8, 13, 14, 15}; QMap checksum_cmds = { { 5, 16 }, /*{ 6, 16 },*/ /*{ 7, 16 },*/ { 8, 32 }, { 13, 16 }, { 14, 16 }, { 15, 0 } }; QString callsign_pattern = QString("(?[A-Z0-9/]+)"); QString optional_cmd_pattern = QString("(?\\s?(?:AGN[?]|ACK|73|YES|NO|SNR|QSL[?]?|RR|HEARING|HW CPY[?]|FB|QTH|QTC|GRID|APRS[:]|QSO|[?@&$%#^> ]))?"); 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|HEARING)\\s?[-+]?(?:3[01]|[0-2]?[0-9]))?"); QRegularExpression directed_re("^" + callsign_pattern + optional_cmd_pattern + optional_num_pattern); QRegularExpression beacon_re(R"(^\s*(?CQCQCQ|CQ QRP|CQ DX|CQ TEST|CQ( CQ){0,2}|BEACON)(?:\s(?[A-R]{2}[0-9]{2}))?\b)"); QRegularExpression compound_re("^\\s*[<]" + callsign_pattern + "(?" + optional_grid_pattern + optional_cmd_pattern + optional_num_pattern + ")[>]"); QMap hufftable = { // char code weight // 3 bits { " " , "000" }, // 1300 { "E" , "001" }, // 1270.2 // 4 bits { "T" , "1100" }, // 905.6 { "A" , "1010" }, // 816.7 { "O" , "0111" }, // 750.7 { "I" , "0101" }, // 696.6 { "N" , "0100" }, // 674.9 // 5 bits { "S" , "11111" }, // 632.7 { "H" , "11110" }, // 609.4 { "R" , "11101" }, // 598.7 { "D" , "10111" }, // 425.3 { "L" , "10110" }, // 402.5 // 6 bits { "C" , "111001" }, // 278.2 { "U" , "111000" }, // 275.8 { "M" , "110111" }, // 240.6 { "W" , "110110" }, // 236.0 { "F" , "110100" }, // 222.8 { "G" , "100111" }, // 201.5 { "Q" , "100110" }, // 200 { "Y" , "011010" }, // 197.4 { "P" , "011001" }, // 192.9 { "B" , "011000" }, // 149.2 // 7 bits { "\\" , "0110111" }, // 100 <- escape { "." , "1000000" }, // 100 { "0" , "1000001" }, // 100 { "1" , "1000010" }, // 100 { "2" , "1000011" }, // 100 { "3" , "1000100" }, // 100 { "4" , "1000101" }, // 100 { "5" , "1000110" }, // 100 { "6" , "1000111" }, // 100 { "7" , "1001000" }, // 100 { "8" , "1001001" }, // 100 { "9" , "1001010" }, // 100 { "?" , "1001011" }, // 100 { "/" , "1101010" }, // 100 { "V" , "0110110" }, // 97.8 // 8 bits { "K" , "11010111" }, // 77.2 // 10 bits { "J" , "1101011010" }, // 15.3 { "X" , "1101011001" }, // 15.0 // 11 bits { "Z" , "11010110110" }, // 7.4 { ":" , "11010110000" }, // 5 // 12 bits { "+" , "110101100011" }, // 5 { "-" , "110101101110" }, // 5 { "!" , "110101101111" }, // 5 { "\x04" , "110101100010" }, // 1 <- eot /* A-Z 0-9 Space \\ ? / : - + ! special chars that are escaped will be added here too... */ }; /* via https://www3.nd.edu/~busiforc/handouts/cryptography/Letter%20Frequencies.html#Most_common_trigrams_.28in_order.29 most common trigrams: the = 12 bits and = 13 bits tha = 13 bits ent = 11 bits ing = 14 bits ** ion = 12 bits tio = 12 bits for = 15 bits ** nde = 12 bits has = 14 bits nce = 13 bits edt = 12 bits tis = 13 bits oft = 14 bits sth = 14 bits men = 13 bits her = 13 bits hat = 13 bits his = 14 bits ere = 11 bits ter = 12 bits was = 15 bits you = 16 bits ** ith = 13 bits ver = 15 bits all = 14 bits wit = 14 bits thi = 13 bits most common quadgrams: that = 17 bits ther = 17 bits with = 18 bits tion = 16 bits here = 16 bits ould = 20 bits ** ight = 19 bits have = 19 bits hich = 20 bits ** whic = 21 bits ** this = 18 bits thin = 18 bits they = 18 bits atio = 16 bits ever = 18 bits from = 21 bits ** ough = 21 bits ** were = 17 bits hing = 18 bits ment = 17 bits potential contenders: _DE_ = 14 bits BTU = 16 bits ... = 21 bits 599 = 21 bits FT8 = 17 bits BAND = 19 bits FT8CALL = 37 bits DIPOLE = 27 bits VERT = 19 bits BEAM = 19 bits */ /* original: Space \\ ? / : - + ! needed: ^,&@#$%'"()<>|*[]{}=;_~` */ QMap huffescapes = { // 10 bits { "\\ ", " DE " }, // 14 bits - 4 bit savings { "\\E", "," }, // 11 bits { "\\T", "&" }, { "\\A", "@" }, { "\\O", "#" }, { "\\I", "$" }, { "\\N", "%" }, // 12 bits { "\\S", "\'" }, { "\\H", "\"" }, { "\\R", "(" }, { "\\D", ")" }, { "\\L", "|" }, // 13 bits // trigram / quadgram efficiency { "\\C", "YOU" }, // 16 bits - 3 bit savings { "\\U", "THAT" }, // 17 bits - 4 bit savings { "\\M", "THER" }, // 17 bits - 4 bit savings { "\\W", "WITH" }, // 18 bits - 5 bit savings { "\\F", "TION" }, // 16 bits - 3 bit savings { "\\G", "HERE" }, // 16 bits - 3 bit savings { "\\Q", "OULD" }, // 20 bits - 7 bit savings { "\\Y", "IGHT" }, // 19 bits - 6 bit savings { "\\P", "HAVE" }, // 19 bits - 6 bit savings { "\\B", "HICH" }, // 20 bits - 7 bit savings #if 0 // 14 bits { "\\.", "" }, { "\\0", "" }, #endif { "\\1", "<" }, { "\\2", ">" }, { "\\3", "[" }, { "\\4", "]" }, { "\\5", "{" }, { "\\6", "}" }, { "\\7", "*" }, { "\\8", "=" }, { "\\9", ";" }, { "\\?", "WHIC" }, // 21 bits - 7 bit savings { "\\/", "THIS" }, // 18 bits - 4 bit savings { "\\V", "FROM" }, // 21 bits - 7 bit savings // 15 bits // quadgram efficiency { "\\K" , "OUGH" }, // 21 bits - 6 bit savings // 17 bits #if 0 { "\\J" , "" }, { "\\X" , "" }, #endif // 18 bits { "\\Z" , "^" }, { "\\:" , "~" }, // 19 bits { "\\+" , "`" }, { "\\-" , "_" }, // special case :) { "\\!" , "FT8CALL" }, // 37 bits - 18 bit savings }; 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 }, }; QMap cqs = { { 0, "CQCQCQ" }, { 1, "CQ DX" }, { 2, "CQ QRP" }, { 3, "CQ TEST" }, { 4, "CQ"}, { 5, "CQ CQ"}, { 6, "CQ CQ CQ"}, }; 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 }; QMap initializeEscapes(QMap huff, QMap escapes){ QMap newhuff(huff); foreach(auto escapeString, escapes.keys()){ auto ch = escapes[escapeString]; auto encoded = Varicode::huffEncode(huff, escapeString); QList> e; foreach(auto pair, encoded){ e.append(pair.second); } auto bits = Varicode::bitsListToBits(e); newhuff[ch] = Varicode::bitsToStr(bits); } #if PRINT_VARICODE_ALPHABET auto keys = newhuff.keys(); qSort(keys.begin(), keys.end(), [newhuff](QChar a, QChar b){ return newhuff[a].length() < newhuff[b].length(); }); foreach(auto ch, keys){ qDebug() << ch << newhuff[ch] << newhuff[ch].length(); } #endif return newhuff; } QMap hufftableescaped = initializeEscapes(hufftable, huffescapes); /* * UTILITIES */ int mwattsToDbm(int mwatts){ int dbm = 0; auto values = dbm2mw.values(); qSort(values); 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(); } /* * VARICODE */ QString Varicode::cqString(int number){ if(!cqs.contains(number)){ return QString{}; } return cqs[number]; } bool Varicode::startsWithCQ(QString text){ foreach(auto cq, cqs.values()){ if(text.startsWith(cq)){ 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"); 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(); qSort(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(text.midRef(i).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); //.mid(0, bitvec.length()-pad); // 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; } QString Varicode::huffUnescape(QString const &input){ QString text = input; // unescape alternate alphabet foreach(auto escaped, huffescapes.keys()){ text = text.replace(escaped, huffescapes[escaped]); } return text; } QString Varicode::huffEscape(QString const &input){ QString text = input; // escape alternate alphabet foreach(auto unescaped, huffescapes.values()){ text = text.replace(unescaped, huffescapes.key(unescaped)); } return text; } QSet Varicode::huffValidChars(){ return QSet::fromList(hufftableescaped.keys()); } bool Varicode::huffShouldEscape(QString const &input){ foreach(auto ch, huffescapes.values()){ if(input.contains(ch)){ return true; } } return false; } // 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(QRegExp("[^A-Z0-9]"), ""); if(word.length() < 4){ word = word + QString(" ").repeated(4-word.length()); } quint32 a = 37 * 37 * 37 * alphanumeric.indexOf(word.at(0)); quint32 b = 37 * 37 * alphanumeric.indexOf(word.at(1)); quint32 c = 37 * 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 % 37; word[3] = alphanumeric.at(tmp); packed = packed / 37; tmp = packed % 37; word[2] = alphanumeric.at(tmp); packed = packed / 37; tmp = packed % 37; word[1] = alphanumeric.at(tmp); packed = packed / 37; tmp = packed % 37; word[0] = alphanumeric.at(tmp); packed = packed / 37; return QString(word, 4); } // pack a callsign into a 28-bit value quint32 Varicode::packCallsign(QString const& value){ quint32 packed = 0; QString callsign = value.toUpper().trimmed(); if(basecalls.contains(callsign)){ return basecalls[callsign]; } // 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){ 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); } return callsign; } 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; if(cmd == directed_cmds[" SNR"]){ // 8 bits - 1 bit flag + 1 bit type + 6 bit number // [1][X][6] // X = 0 == SNR value = (1 << 1) << 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); return directed_cmds[" SNR"]; } else { if(pNum) *pNum = 0; return value & ((1<<7)-1); } } 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) && 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]]; } // CQCQCQ EM73 // CQ DX EM73 // CQ QRP EM73 // BEACON EM73 QString Varicode::packBeaconMessage(QString const &text, const QString &callsign, int *n){ QString frame; auto parsedText = beacon_re.match(text); if(!parsedText.hasMatch()){ if(n) *n = 0; return frame; } auto extra = parsedText.captured("grid"); // Beacon Alt Type // --------------- // 1 0 BEACON // 1 1 CQCQCQ auto type = parsedText.captured("type"); auto isAlt = type.startsWith("CQ"); auto parsedCall = QRegularExpression(compound_callsign_pattern).match(callsign); if(!parsedCall.hasMatch()){ if(n) *n = 0; return frame; } QString base = parsedCall.captured("base"); bool isPrefix = false; QString fix = parsedCall.captured("prefix"); if(!fix.isEmpty()){ isPrefix = true; } if(!isPrefix){ fix = parsedCall.captured("suffix"); } 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); } if(isAlt){ packed_extra |= (1<<15); } quint8 cqNumber = cqs.key(type, 0); frame = packCompoundFrame(base, fix, isPrefix, FrameBeacon, packed_extra, cqNumber); if(frame.isEmpty()){ if(n) *n = 0; return frame; } if(n) *n = parsedText.captured(0).length(); return frame; } QStringList Varicode::unpackBeaconMessage(const QString &text, quint8 *pType, bool * isAlt, quint8 * pBits3){ quint8 type = FrameBeacon; quint16 num = nmaxgrid; quint8 bits3 = 0; QStringList unpacked = unpackCompoundFrame(text, &type, &num, &bits3); if(unpacked.isEmpty() || type != FrameBeacon){ 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; qDebug() << "trying to pack compound message" << text; auto parsedText = compound_re.match(text); if(!parsedText.hasMatch()){ if(n) *n = 0; return frame; } qDebug() << parsedText.capturedTexts(); QString callsign = parsedText.captured("callsign"); QString grid = parsedText.captured("grid"); QString cmd = parsedText.captured("cmd"); QString num = parsedText.captured("num").trimmed(); QString base; QString fix; bool isPrefix = false; if(basecalls.contains(callsign)){ // if it's a basecall, use it verbatim with no prefix/suffix base = callsign; fix = ""; } else { // otherwise, parse the callsign for prefix/suffix auto parsedCall = QRegularExpression(compound_callsign_pattern).match(callsign); if(!parsedCall.hasMatch()){ if(n) *n = 0; return frame; } base = parsedCall.captured("base"); fix = parsedCall.captured("prefix"); if(!fix.isEmpty()){ isPrefix = true; } if(!isPrefix){ fix = parsedCall.captured("suffix"); } } quint8 type = FrameCompound; quint16 extra = nmaxgrid; if (!cmd.isEmpty() && directed_cmds.contains(cmd) && Varicode::isCommandAllowed(cmd)){ bool packedNum = false; qint8 inum = Varicode::packNum(num, nullptr); extra = nusergrid + Varicode::packCmd(directed_cmds[cmd], inum, &packedNum); type = FrameCompoundDirected; } else if(!grid.isEmpty()){ extra = Varicode::packGrid(grid); } frame = Varicode::packCompoundFrame(base, fix, isPrefix, type, extra, 0); if(n) *n = parsedText.captured(0).length(); return frame; } QStringList Varicode::unpackCompoundMessage(const QString &text, quint8 *pType, quint8 *pBits3){ quint8 type = FrameCompound; quint16 extra = nmaxgrid; quint8 bits3 = 0; QStringList unpacked = unpackCompoundFrame(text, &type, &extra, &bits3); if(unpacked.isEmpty() || (type != FrameCompound && type != FrameCompoundDirected)){ return QStringList {}; } if(extra <= nbasegrid){ unpacked.append(" " + Varicode::unpackGrid(extra)); } else if (nusergrid <= extra && extra < nmaxgrid) { quint8 num = 0; auto cmd = Varicode::unpackCmd(extra - nusergrid, &num); unpacked.append(directed_cmds.key(cmd)); if(cmd == directed_cmds[" SNR"]){ unpacked.append(Varicode::formatSNR(num - 31)); } } if(pType) *pType = type; if(pBits3) *pBits3 = bits3; return unpacked; } QString Varicode::packCompoundFrame(const QString &baseCallsign, const QString &fix, bool isPrefix, quint8 type, quint16 num, quint8 bits3){ QString frame; // needs to be a compound type... if(type == FrameDataPadded || type == FrameDataUnpadded || type == FrameDirected){ return frame; } quint8 packed_flag = type; quint32 packed_base = Varicode::packCallsign(baseCallsign); quint32 packed_fix = Varicode::packAlphaNumeric22(fix, isPrefix); if(packed_base == 0 || packed_fix == 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][28][22][11],[5][3] = 72 auto bits = ( Varicode::intToBits(packed_flag, 3) + Varicode::intToBits(packed_base, 28) + Varicode::intToBits(packed_fix, 22) + 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][28][22][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 beacon type... if(packed_flag == FrameDataPadded || packed_flag == FrameDataUnpadded || packed_flag == FrameDirected){ return unpacked; } quint32 packed_base = Varicode::bitsToInt(bits.mid(3, 28)); quint32 packed_fix = Varicode::bitsToInt(bits.mid(31, 22)); quint16 packed_11 = Varicode::bitsToInt(bits.mid(53, 11)); QString base = Varicode::unpackCallsign(packed_base).trimmed(); bool isPrefix = false; QString fix = Varicode::unpackAlphaNumeric22(packed_fix, &isPrefix).trimmed(); quint16 num = (packed_11 << 5) | packed_5; if(pType) *pType = packed_flag; if(pNum) *pNum = num; if(pBits3) *pBits3 = packed_3; if(isPrefix){ unpacked.append(fix); } unpacked.append(base); if(!isPrefix){ unpacked.append(fix); } return unpacked; } // J1Y ACK // J1Y? // KN4CRD: J1Y$ // KN4CRD: J1Y! HELLO WORLD QString Varicode::packDirectedMessage(const QString &text, const QString &callsign, QString *pTo, 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 = callsign; QString to = match.captured("callsign"); QString cmd = match.captured("cmd"); QString num = match.captured("num").trimmed(); // ensure we have a directed command if(cmd.isEmpty()){ if(n) *n = 0; return frame; } // validate "to" callsign auto parsedTo = QRegularExpression(compound_callsign_pattern).match(to); bool validToCallsign = (to != callsign) && (basecalls.contains(to) || parsedTo.hasMatch()); if(!validToCallsign){ if(n) *n = 0; return frame; } if(basecalls.contains(to)){ if(pTo) *pTo = to; } else if(parsedTo.hasMatch()){ if(pTo) *pTo = parsedTo.captured(0); auto parsedBase = parsedTo.captured("base"); if(parsedBase.length() != to.length()){ to = "<....>"; // parsedBase; } } // validate command if(!Varicode::isCommandAllowed(cmd) && !Varicode::isCommandAllowed(cmd.trimmed())){ if(n) *n = 0; return frame; } // packing general number... quint8 inum = Varicode::packNum(num, nullptr); if(pNum) *pNum = num; quint32 packed_from = Varicode::packCallsign(from); quint32 packed_to = Varicode::packCallsign(to); if(packed_from == 0 || packed_to == 0){ if(n) *n = 0; return frame; } quint8 packed_cmd = 0; if(directed_cmds.contains(cmd)){ packed_cmd = directed_cmds[cmd]; } if(directed_cmds.contains(cmd.trimmed())){ packed_cmd = directed_cmds[cmd.trimmed()]; } quint8 packed_flag = FrameDirected; quint8 packed_extra = 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 = cmd; if(n) *n = match.captured(0).length(); return Varicode::pack72bits(Varicode::bitsToInt(bits), packed_extra % 64); } 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 != 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)); QString from = Varicode::unpackCallsign(packed_from).trimmed(); QString to = Varicode::unpackCallsign(packed_to).trimmed(); QString cmd = directed_cmds.key(packed_cmd % 32); unpacked.append(from); unpacked.append(to); unpacked.append(cmd); if(extra != 0){ // TODO: jsherer - should we decide which format to use on the command, or something else? if(packed_cmd == directed_cmds[" SNR"]) { unpacked.append(Varicode::formatSNR((int)extra-31)); } else { unpacked.append(QString("%1").arg(extra-31)); } } if(pType) *pType = packed_flag; return unpacked; } QString Varicode::packDataMessage(const QString &input, int *n){ static const int frameSize = 72; QString frame; // [3][69] = 72 QVector frameDataBits; QVector frameHeaderBits = Varicode::intToBits(FrameDataUnpadded, 3); int i = 0; // we use the escaped table here, so they the escapes and the characters are packed together... foreach(auto pair, Varicode::huffEncode(hufftableescaped, input)){ auto charN = pair.first; auto charBits = pair.second; if(frameHeaderBits.length() + frameDataBits.length() + charBits.length() <= frameSize){ frameDataBits += charBits; i += charN; continue; } break; } QVector framePadBits; int pad = frameSize - frameHeaderBits.length() - frameDataBits.length(); if(pad){ frameHeaderBits = Varicode::intToBits(FrameDataPadded, 3); // 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++){ framePadBits.append(i == 0 ? (bool)0 : (bool)1); } } QVector allBits = frameHeaderBits + frameDataBits + framePadBits; quint64 value = Varicode::bitsToInt(allBits.constBegin(), 64); quint8 rem = (quint8)Varicode::bitsToInt(allBits.constBegin() + 64, 8); frame = Varicode::pack72bits(value, rem); *n = i; return frame; } QString Varicode::unpackDataMessage(const QString &text, quint8 *pType){ 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); quint8 type = Varicode::bitsToInt(bits.mid(0, 3)); if(type == FrameDataUnpadded){ bits = bits.mid(3); } else if(type == FrameDataPadded) { int n = bits.lastIndexOf(0); bits = bits.mid(3, n-3); } else { return unpacked; } // huff decode the bits (without escapes) unpacked = Varicode::huffDecode(hufftable, bits); // then... unescape special characters unpacked = Varicode::huffUnescape(unpacked); if(pType) *pType = type; return unpacked; }