/** * 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 #define CRCPP_INCLUDE_ESOTERIC_CRC_DEFINITIONS #include "crc.h" #include "varicode.h" const int nalphabet = 41; QString alphabet = {"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ+-./?"}; QString grid_pattern = {R"((?[A-R]{2}[0-9]{2})+)"}; QString callsign_pattern1 = {R"((?[A-Z0-9/]{2,}))"}; QString callsign_pattern2 = {R"((?(\d|[A-Z])+\/?((\d|[A-Z]){3,})(\/(\d|[A-Z])+)?(\/(\d|[A-Z])+)?))"}; QString callsign_pattern3 = {R"(([0-9A-Z ])([0-9A-Z])([0-9])([A-Z ])([A-Z ])([A-Z ]))"}; QString callsign_alphabet = {"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ "}; QString directed_cmds("?$@&| "); QRegularExpression directed_re(R"((?[A-Z0-9/]+):(?[A-Z0-9/]+)(?[?$@&| ]))"); QMap huff = { // char code weight {' ' , "001" }, // 1300 {'E' , "000" }, // 1270.2 {'T' , "1100" }, // 905.6 {'A' , "1010" }, // 816.7 {'O' , "0111" }, // 750.7 {'I' , "0101" }, // 696.6 {'N' , "0100" }, // 674.9 {'S' , "11111" }, // 632.7 {'H' , "11110" }, // 609.4 {'R' , "11101" }, // 598.7 {'D' , "10111" }, // 425.3 {'L' , "10110" }, // 402.5 {'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 {'!' , "0110111" }, // 100 {'.' , "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 <- shift {'V' , "0110110" }, // 97.8 {'K' , "11010111" }, // 77.2 {'J' , "1101011010" }, // 15.3 {'X' , "1101011001" }, // 15.0 {'Z' , "11010110110" }, // 7.4 {':' , "11010110000" }, // 5 {'+' , "110101100011" }, // 5 {'-' , "110101101110" }, // 5 {'/' , "110101101111" }, // 5 {'\x04' , "110101100010" }, // 1 <- eot }; QChar huffeot = '\x04'; quint32 nbasecall = 37 * 36 * 10 * 27 * 27 * 27; QMap basecalls = { { "CQ DX", nbasecall + 1 }, { "CQCQCQ", nbasecall + 2 }, { "ALLCALL", nbasecall + 3 }, }; QStringList Varicode::parseCallsigns(QString const &input){ QStringList callsigns; QRegularExpression re(callsign_pattern2); 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(QString const& text){ QList> out; foreach(auto ch, text){ if(!huff.contains(ch)){ continue; } out.append(strToBits(huff[ch])); } return out; } QVector Varicode::huffFlatten(QList> &list){ QVector out; foreach(auto vec, list){ out += vec; } return out; } QString Varicode::huffDecode(QVector const& bitvec){ QString out; QString bits = 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 == huffeot){ break; } out.append(key); bits = bits.mid(huff[key].length()); found = true; } } if(!found){ break; } } return out; } 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; } quint8 Varicode::unpack5bits(QString const& value){ return alphabet.indexOf(value.at(0)); } QString Varicode::pack5bits(quint8 packed){ return alphabet.at(packed % nalphabet); } 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)); return (nalphabet*nalphabet) * a + nalphabet*b + c; } 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); } 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(callsign_pattern3); 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 = callsign_alphabet.indexOf(matched.at(0)); packed = 36*packed + callsign_alphabet.indexOf(matched.at(1)); packed = 10*packed + callsign_alphabet.indexOf(matched.at(2)); packed = 27*packed + callsign_alphabet.indexOf(matched.at(3)) - 10; packed = 27*packed + callsign_alphabet.indexOf(matched.at(4)) - 10; packed = 27*packed + callsign_alphabet.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] = callsign_alphabet.at(tmp); value = value/27; tmp = value % 27 + 10; word[4] = callsign_alphabet.at(tmp); value = value/27; tmp = value % 27 + 10; word[3] = callsign_alphabet.at(tmp); value = value/27; tmp = value % 10; word[2] = callsign_alphabet.at(tmp); value = value/10; tmp = value % 36; word[1] = callsign_alphabet.at(tmp); value = value/36; tmp = value; word[0] = callsign_alphabet.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 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 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; } quint16 Varicode::packGrid(QString const& grid){ // TODO: validate grid... // TODO: encode non-grid data... auto pair = 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 > 180*180){ // TODO: decode non-grid data... return ""; } float dlat = value % 180 - 90; float dlong = value / 180 * 2 - 180 + 2; return deg2grid(dlong, dlat).left(4); } QString Varicode::packDirectedMessage(const QString &text, int *n){ QString frame; auto match = directed_re.match(text); if(match.hasMatch()){ QString from = match.captured("from"); QString to = match.captured("to"); QString cmd = match.captured("cmd"); if(cmd.at(0) != '?'){ // this is the only allowed one at this point... *n = 0; return frame; } auto fromBytes = from.toLocal8Bit(); auto fromCRC = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_5_ITU()); quint8 packed_flag = 0; quint32 packed_from = Varicode::packCallsign(from); quint32 packed_to = Varicode::packCallsign(to); quint8 packed_cmd = directed_cmds.indexOf(cmd.at(0)); quint8 packed_extra = fromCRC; // 3 + 28 + 28 + 5 = 64 auto bits = ( Varicode::intToBits(packed_flag, 3) + Varicode::intToBits(packed_from, 28) + Varicode::intToBits(packed_to, 28) + Varicode::intToBits(packed_cmd & 5, 5) ); frame = Varicode::pack64bits(Varicode::bitsToInt(bits)) + Varicode::pack5bits(packed_extra & 31); *n = match.captured(0).length(); } return frame; } QStringList Varicode::unpackDirectedMessage(const QString &text){ QStringList unpacked; if(text.length() < 13){ return unpacked; } auto bits = Varicode::bitsToStr(Varicode::intToBits(Varicode::unpack64bits(text.left(12)), 64)); quint8 extra = Varicode::unpack5bits(text.right(1)); quint8 flag = Varicode::bitsToInt(Varicode::strToBits(bits.left(3))); quint32 packed_from = Varicode::bitsToInt(Varicode::strToBits(bits.mid(3, 28))); quint32 packed_to = Varicode::bitsToInt(Varicode::strToBits(bits.mid(31, 28))); quint8 packed_cmd = Varicode::bitsToInt(Varicode::strToBits(bits.mid(59, 5))); QString from = Varicode::unpackCallsign(packed_from).trimmed(); auto fromBytes = from.toLocal8Bit(); auto fromCRC = CRC::Calculate(fromBytes.data(), fromBytes.length(), CRC::CRC_5_ITU()); if(fromCRC != extra){ return unpacked; } unpacked.append(from); unpacked.append(Varicode::unpackCallsign(packed_to).trimmed()); unpacked.append(QString(directed_cmds.at(packed_cmd & 5))); return unpacked; }