diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5761abc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..835d98a --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +CXXFLAGS = -std=c++14 +LDFLAGS = -lm + +test: test.o encode.o pack.o + $(CXX) $(LDFLAGS) -o $@ $^ diff --git a/README.md b/README.md index aef0672..b4bc2a4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ -# ft8_lib -FT8 library +# FT8 library + +A C++ implementation of FT8 protocol, mostly intended for experimentation with microcontrollers. At the moment only encoding is implemented. + +The encoding process is relatively light on resources, and an Arduino should be perfectly capable of running this code. + +The intent of this library is to foster experimentation with e.g. automated beacons. FT8 already supports free-text messages and the upcoming new version will support raw telemetry data (71 bits). + +Work is in progress (possibly taking forever) to explore decoding options. On a fast 32-bit microcontroller decoding might be possible, perhaps with some tradeoffs. + +# References and credits + +Thanks to Robert Morris, AB1HL, whose Python code (https://github.com/rtmrtmrtmrtm/weakmon) inspired this and helped to test various parts of the code. + +This would not of course be possible without the original WSJT-X code, which is mostly written in Fortran (http://physics.princeton.edu/pulsar/K1JT/wsjtx.html). I believe that is the only 'documentation' of the FT8 protocol available, and the source code was used as such in this project. diff --git a/encode.cpp b/encode.cpp new file mode 100644 index 0000000..e96b224 --- /dev/null +++ b/encode.cpp @@ -0,0 +1,262 @@ +#include "encode.h" + +constexpr int N = 174, K = 87, M = N-K; + +constexpr uint16_t POLYNOMIAL = 0xC06; // CRC-12 polynomial without the leading (MSB) 1 + +// Parity generator matrix for (174,87) code, stored in bitpacked format (MSB first) +constexpr uint8_t G[87][11] = { + { 0x23, 0xbb, 0xa8, 0x30, 0xe2, 0x3b, 0x6b, 0x6f, 0x50, 0x98, 0x2e }, + { 0x1f, 0x8e, 0x55, 0xda, 0x21, 0x8c, 0x5d, 0xf3, 0x30, 0x90, 0x52 }, + { 0xca, 0x7b, 0x32, 0x17, 0xcd, 0x92, 0xbd, 0x59, 0xa5, 0xae, 0x20 }, + { 0x56, 0xf7, 0x83, 0x13, 0x53, 0x7d, 0x0f, 0x43, 0x82, 0x96, 0x4e }, + { 0x29, 0xc2, 0x9d, 0xba, 0x9c, 0x54, 0x5e, 0x26, 0x77, 0x62, 0xfe }, + { 0x6b, 0xe3, 0x96, 0xb5, 0xe2, 0xe8, 0x19, 0xe3, 0x73, 0x34, 0x0c }, + { 0x29, 0x35, 0x48, 0xa1, 0x38, 0x85, 0x83, 0x28, 0xaf, 0x42, 0x10 }, + { 0xcb, 0x6c, 0x6a, 0xfc, 0xdc, 0x28, 0xbb, 0x3f, 0x7c, 0x6e, 0x86 }, + { 0x3f, 0x2a, 0x86, 0xf5, 0xc5, 0xbd, 0x22, 0x5c, 0x96, 0x11, 0x50 }, + { 0x84, 0x9d, 0xd2, 0xd6, 0x36, 0x73, 0x48, 0x18, 0x60, 0xf6, 0x2c }, + { 0x56, 0xcd, 0xae, 0xc6, 0xe7, 0xae, 0x14, 0xb4, 0x3f, 0xee, 0xee }, + { 0x04, 0xef, 0x5c, 0xfa, 0x37, 0x66, 0xba, 0x77, 0x8f, 0x45, 0xa4 }, + { 0xc5, 0x25, 0xae, 0x4b, 0xd4, 0xf6, 0x27, 0x32, 0x0a, 0x39, 0x74 }, + { 0xfe, 0x37, 0x80, 0x29, 0x41, 0xd6, 0x6d, 0xde, 0x02, 0xb9, 0x9c }, + { 0x41, 0xfd, 0x95, 0x20, 0xb2, 0xe4, 0xab, 0xeb, 0x2f, 0x98, 0x9c }, + { 0x40, 0x90, 0x7b, 0x01, 0x28, 0x0f, 0x03, 0xc0, 0x32, 0x39, 0x46 }, + { 0x7f, 0xb3, 0x6c, 0x24, 0x08, 0x5a, 0x34, 0xd8, 0xc1, 0xdb, 0xc4 }, + { 0x40, 0xfc, 0x3e, 0x44, 0xbb, 0x7d, 0x2b, 0xb2, 0x75, 0x6e, 0x44 }, + { 0xd3, 0x8a, 0xb0, 0xa1, 0xd2, 0xe5, 0x2a, 0x8e, 0xc3, 0xbc, 0x76 }, + { 0x3d, 0x0f, 0x92, 0x9e, 0xf3, 0x94, 0x9b, 0xd8, 0x4d, 0x47, 0x34 }, + { 0x45, 0xd3, 0x81, 0x4f, 0x50, 0x40, 0x64, 0xf8, 0x05, 0x49, 0xae }, + { 0xf1, 0x4d, 0xbf, 0x26, 0x38, 0x25, 0xd0, 0xbd, 0x04, 0xb0, 0x5e }, + { 0xf0, 0x8a, 0x91, 0xfb, 0x2e, 0x1f, 0x78, 0x29, 0x06, 0x19, 0xa8 }, + { 0x7a, 0x8d, 0xec, 0x79, 0xa5, 0x1e, 0x8a, 0xc5, 0x38, 0x80, 0x22 }, + { 0xca, 0x41, 0x86, 0xdd, 0x44, 0xc3, 0x12, 0x15, 0x65, 0xcf, 0x5c }, + { 0xdb, 0x71, 0x4f, 0x8f, 0x64, 0xe8, 0xac, 0x7a, 0xf1, 0xa7, 0x6e }, + { 0x8d, 0x02, 0x74, 0xde, 0x71, 0xe7, 0xc1, 0xa8, 0x05, 0x5e, 0xb0 }, + { 0x51, 0xf8, 0x15, 0x73, 0xdd, 0x40, 0x49, 0xb0, 0x82, 0xde, 0x14 }, + { 0xd0, 0x37, 0xdb, 0x82, 0x51, 0x75, 0xd8, 0x51, 0xf3, 0xaf, 0x00 }, + { 0xd8, 0xf9, 0x37, 0xf3, 0x18, 0x22, 0xe5, 0x7c, 0x56, 0x23, 0x70 }, + { 0x1b, 0xf1, 0x49, 0x06, 0x07, 0xc5, 0x40, 0x32, 0x66, 0x0e, 0xde }, + { 0x16, 0x16, 0xd7, 0x80, 0x18, 0xd0, 0xb4, 0x74, 0x5c, 0xa0, 0xf2 }, + { 0xa9, 0xfa, 0x8e, 0x50, 0xbc, 0xb0, 0x32, 0xc8, 0x5e, 0x33, 0x04 }, + { 0x83, 0xf6, 0x40, 0xf1, 0xa4, 0x8a, 0x8e, 0xbc, 0x04, 0x43, 0xea }, + { 0xec, 0xa9, 0xaf, 0xa0, 0xf6, 0xb0, 0x1d, 0x92, 0x30, 0x5e, 0xdc }, + { 0x37, 0x76, 0xaf, 0x54, 0xcc, 0xfb, 0xae, 0x91, 0x6a, 0xfd, 0xe6 }, + { 0x6a, 0xbb, 0x21, 0x2d, 0x97, 0x39, 0xdf, 0xc0, 0x25, 0x80, 0xf2 }, + { 0x05, 0x20, 0x9a, 0x0a, 0xbb, 0x53, 0x0b, 0x9e, 0x7e, 0x34, 0xb0 }, + { 0x61, 0x2f, 0x63, 0xac, 0xc0, 0x25, 0xb6, 0xab, 0x47, 0x6f, 0x7c }, + { 0x0a, 0xf7, 0x72, 0x31, 0x61, 0xec, 0x22, 0x30, 0x80, 0xbe, 0x86 }, + { 0xa8, 0xfc, 0x90, 0x69, 0x76, 0xc3, 0x56, 0x69, 0xe7, 0x9c, 0xe0 }, + { 0x45, 0xb7, 0xab, 0x62, 0x42, 0xb7, 0x74, 0x74, 0xd9, 0xf1, 0x1a }, + { 0xb2, 0x74, 0xdb, 0x8a, 0xbd, 0x3c, 0x6f, 0x39, 0x6e, 0xa3, 0x56 }, + { 0x90, 0x59, 0xdf, 0xa2, 0xbb, 0x20, 0xef, 0x7e, 0xf7, 0x3a, 0xd4 }, + { 0x3d, 0x18, 0x8e, 0xa4, 0x77, 0xf6, 0xfa, 0x41, 0x31, 0x7a, 0x4e }, + { 0x8d, 0x90, 0x71, 0xb7, 0xe7, 0xa6, 0xa2, 0xee, 0xd6, 0x96, 0x5e }, + { 0xa3, 0x77, 0x25, 0x37, 0x73, 0xea, 0x67, 0x83, 0x67, 0xc3, 0xf6 }, + { 0xec, 0xbd, 0x7c, 0x73, 0xb9, 0xcd, 0x34, 0xc3, 0x72, 0x0c, 0x8a }, + { 0xb6, 0x53, 0x7f, 0x41, 0x7e, 0x61, 0xd1, 0xa7, 0x08, 0x53, 0x36 }, + { 0x6c, 0x28, 0x0d, 0x2a, 0x05, 0x23, 0xd9, 0xc4, 0xbc, 0x59, 0x46 }, + { 0xd3, 0x6d, 0x66, 0x2a, 0x69, 0xae, 0x24, 0xb7, 0x4d, 0xcb, 0xd8 }, + { 0xd7, 0x47, 0xbf, 0xc5, 0xfd, 0x65, 0xef, 0x70, 0xfb, 0xd9, 0xbc }, + { 0xa9, 0xfa, 0x2e, 0xef, 0xa6, 0xf8, 0x79, 0x6a, 0x35, 0x57, 0x72 }, + { 0xcc, 0x9d, 0xa5, 0x5f, 0xe0, 0x46, 0xd0, 0xcb, 0x3a, 0x77, 0x0c }, + { 0xf6, 0xad, 0x48, 0x24, 0xb8, 0x7c, 0x80, 0xeb, 0xfc, 0xe4, 0x66 }, + { 0xcc, 0x6d, 0xe5, 0x97, 0x55, 0x42, 0x09, 0x25, 0xf9, 0x0e, 0xd2 }, + { 0x16, 0x4c, 0xc8, 0x61, 0xbd, 0xd8, 0x03, 0xc5, 0x47, 0xf2, 0xac }, + { 0xc0, 0xfc, 0x3e, 0xc4, 0xfb, 0x7d, 0x2b, 0xb2, 0x75, 0x66, 0x44 }, + { 0x0d, 0xbd, 0x81, 0x6f, 0xba, 0x15, 0x43, 0xf7, 0x21, 0xdc, 0x72 }, + { 0xa0, 0xc0, 0x03, 0x3a, 0x52, 0xab, 0x62, 0x99, 0x80, 0x2f, 0xd2 }, + { 0xbf, 0x4f, 0x56, 0xe0, 0x73, 0x27, 0x1f, 0x6a, 0xb4, 0xbf, 0x80 }, + { 0x57, 0xda, 0x6d, 0x13, 0xcb, 0x96, 0xa7, 0x68, 0x9b, 0x27, 0x90 }, + { 0x81, 0xcf, 0xc6, 0xf1, 0x8c, 0x35, 0xb1, 0xe1, 0xf1, 0x71, 0x14 }, + { 0x48, 0x1a, 0x2a, 0x0d, 0xf8, 0xa2, 0x35, 0x83, 0xf8, 0x2d, 0x6c }, + { 0x1a, 0xc4, 0x67, 0x2b, 0x54, 0x9c, 0xd6, 0xdb, 0xa7, 0x9b, 0xcc }, + { 0xc8, 0x7a, 0xf9, 0xa5, 0xd5, 0x20, 0x6a, 0xbc, 0xa5, 0x32, 0xa8 }, + { 0x97, 0xd4, 0x16, 0x9c, 0xb3, 0x3e, 0x74, 0x35, 0x71, 0x8d, 0x90 }, + { 0xa6, 0x57, 0x3f, 0x3d, 0xc8, 0xb1, 0x6c, 0x9d, 0x19, 0xf7, 0x46 }, + { 0x2c, 0x41, 0x42, 0xbf, 0x42, 0xb0, 0x1e, 0x71, 0x07, 0x6a, 0xcc }, + { 0x08, 0x1c, 0x29, 0xa1, 0x0d, 0x46, 0x8c, 0xcd, 0xbc, 0xec, 0xb6 }, + { 0x5b, 0x0f, 0x77, 0x42, 0xbc, 0xa8, 0x6b, 0x80, 0x12, 0x60, 0x9a }, + { 0x01, 0x2d, 0xee, 0x21, 0x98, 0xeb, 0xa8, 0x2b, 0x19, 0xa1, 0xda }, + { 0xf1, 0x62, 0x77, 0x01, 0xa2, 0xd6, 0x92, 0xfd, 0x94, 0x49, 0xe6 }, + { 0x35, 0xad, 0x3f, 0xb0, 0xfa, 0xeb, 0x5f, 0x1b, 0x0c, 0x30, 0xdc }, + { 0xb1, 0xca, 0x4e, 0xa2, 0xe3, 0xd1, 0x73, 0xba, 0xd4, 0x37, 0x9c }, + { 0x37, 0xd8, 0xe0, 0xaf, 0x92, 0x58, 0xb9, 0xe8, 0xc5, 0xf9, 0xb2 }, + { 0xcd, 0x92, 0x1f, 0xdf, 0x59, 0xe8, 0x82, 0x68, 0x37, 0x63, 0xf6 }, + { 0x61, 0x14, 0xe0, 0x84, 0x83, 0x04, 0x3f, 0xd3, 0xf3, 0x8a, 0x8a }, + { 0x2e, 0x54, 0x7d, 0xd7, 0xa0, 0x5f, 0x65, 0x97, 0xaa, 0xc5, 0x16 }, + { 0x95, 0xe4, 0x5e, 0xcd, 0x01, 0x35, 0xac, 0xa9, 0xd6, 0xe6, 0xae }, + { 0xb3, 0x3e, 0xc9, 0x7b, 0xe8, 0x3c, 0xe4, 0x13, 0xf9, 0xac, 0xc8 }, + { 0xc8, 0xb5, 0xdf, 0xfc, 0x33, 0x50, 0x95, 0xdc, 0xdc, 0xaf, 0x2a }, + { 0x3d, 0xd0, 0x1a, 0x59, 0xd8, 0x63, 0x10, 0x74, 0x3e, 0xc7, 0x52 }, + { 0x14, 0xcd, 0x0f, 0x64, 0x2f, 0xc0, 0xc5, 0xfe, 0x3a, 0x65, 0xca }, + { 0x3a, 0x0a, 0x1d, 0xfd, 0x7e, 0xee, 0x29, 0xc2, 0xe8, 0x27, 0xe0 }, + { 0x8a, 0xbd, 0xb8, 0x89, 0xef, 0xbe, 0x39, 0xa5, 0x10, 0xa1, 0x18 }, + { 0x3f, 0x23, 0x1f, 0x21, 0x20, 0x55, 0x37, 0x1c, 0xf3, 0xe2, 0xa2 } +}; + +// Column order (permutation) in which the bits in codeword are stored +const uint8_t colorder[174] = { + 0, 1, 2, 3, 30, 4, 5, 6, 7, 8, 9, 10, 11, 32, 12, 40, 13, 14, 15, 16, + 17, 18, 37, 45, 29, 19, 20, 21, 41, 22, 42, 31, 33, 34, 44, 35, 47, 51, 50, 43, + 36, 52, 63, 46, 25, 55, 27, 24, 23, 53, 39, 49, 59, 38, 48, 61, 60, 57, 28, 62, + 56, 58, 65, 66, 26, 70, 64, 69, 68, 67, 74, 71, 54, 76, 72, 75, 78, 77, 80, 79, + 73, 83, 84, 81, 82, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119, + 120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139, + 140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159, + 160,161,162,163,164,165,166,167,168,169,170,171,172,173 +}; + +// Costas 7x7 tone pattern +const uint8_t ICOS7[] = { 2,5,6,0,4,1,3 }; + + +// Returns 1 if an odd number of bits are set in x, zero otherwise +uint8_t parity8(uint8_t x) { + x ^= x >> 4; // a b c d ae bf cg dh + x ^= x >> 2; // a b ac bd cae dbf aecg bfdh + x ^= x >> 1; // a ab bac acbd bdcae caedbf aecgbfdh + return (x) & 1; +} + + +// Encode an 87-bit message and return a 174-bit codeword. +// The generator matrix has dimensions (87,87). +// The code is a (174,87) regular ldpc code with column weight 3. +// The code was generated using the PEG algorithm. +// After creating the codeword, the columns are re-ordered according to +// "colorder" to make the codeword compatible with the parity-check matrix +// Arguments: +// * message - array of 87 bits stored as 11 bytes (MSB first) +// * codeword - array of 174 bits stored as 22 bytes (MSB first) +void encode174(const uint8_t *message, uint8_t *codeword) { + static bool first = true; + + // Here we don't generate the generator bit matrix as in WSJT-X implementation + // Instead we access the generator bits straight from the binary representation in G + + // Also we don't use the itmp temporary buffer, instead filling codeword bit by bit + // in the reordered order as we compute the result. + + // For reference: + // itmp(1:M)=pchecks + // itmp(M+1:N)=message(1:K) + // codeword(colorder+1)=itmp(1:N) + + int colidx = 0; // track the current column in codeword + + // Fill the codeword with zeroes, as we will only update binary ones later + for (int i = 0; i < (N + 7) / 8; i++) { + codeword[i] = 0; + } + + // Compute the first part of itmp (1:M) and store the result in codeword + for (int i = 0; i < M; ++i) { // do i=1,M + // Fast implementation of bitwise multiplication and parity checking + // Normally nsum would contain the result of dot product between message and G[i], + // but we only compute the sum modulo 2. + uint8_t nsum = 0; + for (int j = 0; j < 11; ++j) { + uint8_t bits = message[j] & G[i][j]; // bitwise AND (bitwise multiplication) + nsum ^= parity8(bits); // bitwise XOR (addition modulo 2) + } + // Check if we need to set a bit in codeword + if (nsum % 2) { // pchecks(i)=mod(nsum,2) + uint8_t col = colorder[colidx]; // Index of the bit to set + codeword[col/8] |= (1 << (7 - col%8)); + } + ++colidx; + } + + // Compute the second part of itmp (M+1:N) and store the result in codeword + uint8_t mask = 0x80; // Rolling mask starting with the MSB + for (int j = 0; j < K; ++j) { + // Copy the j-th bit from message to codeword + if (message[j/8] & mask) { + uint8_t col = colorder[colidx]; // Index of the bit to set + codeword[col/8] |= (1 << (7 - col%8)); + } + ++colidx; + + // Roll the bitmask to the right + mask >>= 1; + if (mask == 0) mask = 0x80; + } +} + + +uint16_t ft8_crc(uint8_t *message, int nBits) { + const int WIDTH = 12; + const uint16_t TOPBIT = (1 << (WIDTH - 1)); + + uint16_t remainder = 0; + int iByte = 0; + // Perform modulo-2 division, a bit at a time. + for (int iBit = 0; iBit < nBits; ++iBit) { + if (iBit % 8 == 0) { + // Bring the next byte into the remainder. + remainder ^= (message[iByte] << (WIDTH - 8)); + ++iByte; + } + + // Try to divide the current data bit. + if (remainder & TOPBIT) { + remainder = (remainder << 1) ^ POLYNOMIAL; + } + else { + remainder = (remainder << 1); + } + } + return remainder & ((1 << WIDTH) - 1); +} + + +// Generate FT8 tone sequence from payload data +// [IN] payload - 9 byte array consisting of 72 bit payload +// [IN] i3 - 3 bits containing message type (zero?) +// [OUT] itone - array of NN (79) bytes to store the generated tones (encoded as 0..7) +void genft8(const uint8_t *payload, uint8_t i3, uint8_t *itone) { + uint8_t a87[11]; // Store 72 bits of payload + 3 bits i3 + 12 bits CRC + + for (int i = 0; i < 9; i++) + a87[i] = payload[i]; + + // Append 3 bits of i3 at the end of 72 bit payload + a87[9] = ((i3 & 0x07) << 5); + + // Calculate CRC of 76 bits (yes, 72 + 3 + 1 zero bit), see WSJT-X code + uint16_t checksum = ft8_crc(a87, 76); + + // Store the CRC at the end of 75 bit message (yes, 72 + 3) + uint16_t tmp = (checksum << 1); + a87[9] |= (uint8_t)(tmp >> 8); + a87[10] = (uint8_t)tmp; + + // a87 contains 72 bits of payload + 3 bits of i3 + 12 bits of CRC + uint8_t codeword[22]; + encode174(a87, codeword); + + // Message structure: S7 D29 S7 D29 S7 + for (int i = 0; i < 7; ++i) { + itone[i] = ICOS7[i]; + itone[36 + i] = ICOS7[i]; + itone[72 + i] = ICOS7[i]; + } + + int k = 7; // Skip over the first set of Costas symbols + for (int j = 0; j < ND; ++j) { // do j=1,ND + if (j == 29) { + k += 7; // Skip over the second set of Costas symbols + } + // Extract 3 bits from codeword at i-th position + itone[k] = 0; + int i = 3*j; + if (codeword[i/8] & (1 << (7 - i%8))) itone[k] |= 4; + ++i; + if (codeword[i/8] & (1 << (7 - i%8))) itone[k] |= 2; + ++i; + if (codeword[i/8] & (1 << (7 - i%8))) itone[k] |= 1; + ++k; + } +} diff --git a/encode.h b/encode.h new file mode 100644 index 0000000..0d81773 --- /dev/null +++ b/encode.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +constexpr int ND = 58; // Data symbols +constexpr int NS = 21; // Sync symbols (3 @ Costas 7x7) +constexpr int NN = NS+ND; // Total channel symbols (79) + + +// Generate FT8 tone sequence from payload data +// [IN] payload - 9 byte array consisting of 72 bit payload +// [IN] i3 - 3 bits containing message type (zero?) +// [OUT] itone - array of NN (79) bytes to store the generated tones (encoded as 0..7) +void genft8(const uint8_t *payload, uint8_t i3, uint8_t *itone); + + +// Encode an 87-bit message and return a 174-bit codeword. +// The generator matrix has dimensions (87,87). +// The code is a (174,87) regular ldpc code with column weight 3. +// The code was generated using the PEG algorithm. +// After creating the codeword, the columns are re-ordered according to +// "colorder" to make the codeword compatible with the parity-check matrix +// Arguments: +// * message - array of 87 bits stored as 11 bytes (MSB first) +// * codeword - array of 174 bits stored as 22 bytes (MSB first) +void encode174(const uint8_t *message, uint8_t *codeword); + +uint16_t ft8_crc(uint8_t *message, int nBits); diff --git a/pack.cpp b/pack.cpp new file mode 100644 index 0000000..fa8944d --- /dev/null +++ b/pack.cpp @@ -0,0 +1,401 @@ +#include +#include + +#include "pack.h" + +constexpr int NBASE = 37*36*10*27*27*27; +constexpr int NGBASE = 180*180; + +char to_upper(char c) { + return (c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c; +} + +bool is_digit(char c) { + return (c >= '0') && (c <= '9'); +} + +bool is_letter(char c) { + return ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')); +} + +bool is_space(char c) { + return (c == ' '); +} + +bool starts_with(const char *string, const char *prefix) { + return 0 == memcmp(string, prefix, strlen(prefix)); +} + +bool equals(const char *string1, const char *string2) { + return 0 == strcmp(string1, string2); +} + + +// Message formatting: +// - replaces lowercase letters with uppercase +// - merges consecutive spaces into single space +void fmtmsg(char *msg_out, const char *msg_in) { + char c; + char last_out = 0; + while ( (c = *msg_in) ) { + if (c != ' ' || last_out != ' ') { + last_out = to_upper(c); + *msg_out = last_out; + ++msg_out; + } + ++msg_in; + } + *msg_out = 0; // Add zero termination +} + + +// Returns integer encoding of a character (number/digit/space). +// * Digits are encoded as 0..9 +// * Letters a..z are encoded as 10..35 (case insensitive) +// * Space is encoded as 36 +uint8_t nchar(char c) { + if (is_digit(c)) { + return (c - '0'); + } + if (is_letter(c)) { + return (to_upper(c) - 'A') + 10; + } + if (c == ' ') { + return 36; + } + // we should never reach here + return 36; +} + + +// Pack FT8 source/destination and grid data into 72 bits (stored as 9 bytes) +// [IN] nc1 - first callsign data (28 bits) +// [IN] nc2 - second callsign data (28 bits) +// [IN] ng - grid data (16 bits) +// [OUT] payload - 9 byte array to store the 72 bit payload (MSB first) +void pack3_8bit(uint32_t nc1, uint32_t nc2, uint16_t ng, uint8_t *payload) { + payload[0] = (uint8_t)(nc1 >> 20); + payload[1] = (uint8_t)(nc1 >> 12); + payload[2] = (uint8_t)(nc1 >> 4); + payload[3] = (uint8_t)(nc1 << 4) | (uint8_t)(nc2 >> 24); + payload[4] = (uint8_t)(nc2 >> 16); + payload[5] = (uint8_t)(nc2 >> 8); + payload[6] = (uint8_t)(nc2); + payload[7] = (uint8_t)(ng >> 8); + payload[8] = (uint8_t)(ng); +} + + +// Pack FT8 source/destination and grid data into 72 bits (stored as 12 bytes of 6-bit values) +// (For compatibility with WSJT-X and testing) +// [IN] nc1 - first callsign data (28 bits) +// [IN] nc2 - second callsign data (28 bits) +// [IN] ng - grid data (16 bits) +// [OUT] payload - 12 byte array to store the 72 bit payload (MSB first) +void pack3_6bit(uint32_t nc1, uint32_t nc2, uint16_t ng, uint8_t *payload) { + payload[0] = (nc1 >> 22) & 0x3f; // 6 bits + payload[1] = (nc1 >> 16) & 0x3f; // 6 bits + payload[2] = (nc1 >> 10) & 0x3f; // 6 bits + payload[3] = (nc1 >> 4) & 0x3f; // 6 bits + payload[4] = ((nc1 & 0xf) << 2) | ((nc2 >> 26) & 0x3); // 4+2 bits + payload[5] = (nc2 >> 20) & 0x3f; // 6 bits + payload[6] = (nc2 >> 14) & 0x3f; // 6 bits + payload[7] = (nc2 >> 8) & 0x3f; // 6 bits + payload[8] = (nc2 >> 2) & 0x3f; // 6 bits + payload[9] = ((nc2 & 0x3) << 4) | ((ng >> 12) & 0xf); // 2+4 bits + payload[10] = (ng >> 6) & 0x3f; // 6 bits + payload[11] = (ng >> 0) & 0x3f; // 6 bits +} + + +// Parse a 2 digit integer from string +int dd_to_int(const char *str, int length) { + int result = 0; + bool negative; + int i; + if (str[0] == '-') { + negative = true; + i = 1; + } + else { + negative = false; + i = (str[0] == '+') ? 1 : 0; + } + + while (i < length) { + if (str[i] == 0) break; + if (!is_digit(str[i])) break; + result *= 10; + result += (str[i] - '0'); + ++i; + } + + return negative ? -result : result; +} + + +void int_to_dd(char *str, int value, int width) { + if (value < 0) { + *str = '-'; + str++; + value = -value; + } + + int divisor = 1; + for (int i = 0; i < width; i++) { + divisor *= 10; + } + + while (divisor > 1) { + int digit = value / divisor; + + *str = '0' + digit; + str++; + + value -= digit * divisor; + divisor /= 10; + } + *str = 0; +} + + +// Pack a valid callsign into a 28-bit integer. +int32_t packcall(const char *callsign) { + printf("Callsign = [%s]\n", callsign); + if (equals(callsign, "CQ")) { + // TODO: support 'CQ nnn' frequency specification + //if (callsign(4:4).ge.'0' .and. callsign(4:4).le.'9' .and. & + // callsign(5:5).ge.'0' .and. callsign(5:5).le.'9' .and. & + // callsign(6:6).ge.'0' .and. callsign(6:6).le.'9') then + // read(callsign(4:6),*) nfreq + // ncall=NBASE + 3 + nfreq + //endif + return NBASE + 1; + } + if (equals(callsign, "QRZ")) { + return NBASE + 2; + } + if (equals(callsign, "DE")) { + return 267796945; + } + + int len = strlen(callsign); + if (len > 6) { + return -1; + } + + char callsign2[7] = {' ', ' ', ' ', ' ', ' ', ' ', 0}; // 6 spaces with zero terminator + + // Work-around for Swaziland prefix (see WSJT-X code): + if (starts_with(callsign, "3DA0")) { + // callsign='3D0'//callsign(5:6) + memcpy(callsign2, "3D0", 3); + memcpy(callsign2 + 3, callsign + 4, 2); + } + // Work-around for Guinea prefixes (see WSJT-X code): + else if (starts_with(callsign, "3X") && is_letter(callsign[2])) { + //callsign='Q'//callsign(3:6) + memcpy(callsign2, "Q", 1); + memcpy(callsign2 + 1, callsign + 2, 4); + } + else { + // Just copy, no modifications needed + // Check for callsigns with 1 symbol prefix + if (!is_digit(callsign[2]) && is_digit(callsign[1])) { + if (len > 5) { + return -1; + } + // Leave one space at the beginning as padding + memcpy(callsign2 + 1, callsign, len); + } + else { + memcpy(callsign2, callsign, len); + } + } + + // Check if the callsign consists of valid characters + if (!is_digit(callsign2[0]) && !is_letter(callsign2[0]) && !is_space(callsign2[0])) + return -3; + if (!is_digit(callsign2[1]) && !is_letter(callsign2[1])) + return -3; + if (!is_digit(callsign2[2])) + return -3; + if (!is_letter(callsign2[3]) && !is_space(callsign2[3])) + return -3; + if (!is_letter(callsign2[4]) && !is_space(callsign2[4])) + return -3; + if (!is_letter(callsign2[5]) && !is_space(callsign2[5])) + return -3; + + // Form a 28 bit integer from callsign parts + int32_t ncall = nchar(callsign2[0]); + ncall = 36*ncall + nchar(callsign2[1]); + ncall = 10*ncall + nchar(callsign2[2]); + ncall = 27*ncall + nchar(callsign2[3]) - 10; + ncall = 27*ncall + nchar(callsign2[4]) - 10; + ncall = 27*ncall + nchar(callsign2[5]) - 10; + + return ncall; +} + + +// Pack a valid grid locator into an integer. +int16_t packgrid(const char *grid) { + printf("Grid = [%s]\n", grid); + int len = strlen(grid); + + if (len == 0) { + // Blank grid is OK + return NGBASE + 1; + } + // Check for RO, RRR, or 73 in the message field normally used for grid + if (equals(grid, "RO")) { + return NGBASE + 62; + } + if (equals(grid, "RRR")) { + return NGBASE + 63; + } + if (equals(grid, "73")) { + return NGBASE + 64; + } + + // TODO: + char c1 = grid[0]; + int n; + if (c1 == 'R') { + n = dd_to_int(grid + 1, 3); // read(grid(2:4),*,err=30,end=30) n + } + else { + n = dd_to_int(grid, 3); // read(grid,*,err=20,end=20) n + } + + // First, handle signal reports in the original range, -01 to -30 dB + if (n >= -30 && n <= -1) { + if (c1 == 'R') { + return NGBASE + 31 + (-n); + } + else { + return NGBASE + 1 + (-n); + } + } + + char grid4[4]; + memcpy(grid4, grid, 4); + + // Check for extended-range signal reports: -50 to -31, and 0 to +49 + // if (n >= -50 && n <= 49) { + // if (c1 == 'R') { + // // write(grid,1002) n+50 1002 format('LA',i2.2) + // } + // else { + // // write(grid,1003) n+50 1003 format('KA',i2.2) + // } + // // go to 40 + // } + // else { + // // error + // return -1; + // } + + // Check the locator + if (len != 4) return -1; + if (grid4[0] < 'A' || grid4[0] > 'R') return -1; + if (grid4[1] < 'A' || grid4[1] > 'R') return -1; + if (grid4[2] < '0' || grid4[2] > '9') return -1; + if (grid4[3] < '0' || grid4[3] > '9') return -1; + + // OK, we have a properly formatted grid locator + int lng = (grid4[0] - 'A') * 20; + lng += (grid4[2] - '0') * 2; + lng = 179 - lng; + + int lat = (grid4[1] - 'A') * 10; + lat += (grid4[3] - '0') * 1; + lat -= 90; + + int16_t ng = (lng + 180) / 2; + ng *= 180; + ng += lat + 90; + + return ng; +} + + +int packmsg(const char *msg, uint8_t *dat) { // , itype, bcontest + // TODO: maximum allowed length? + if (strlen(msg) > 18) { + return -1; + } + + char msg2[19]; // Including zero terminator! + + fmtmsg(msg2, msg); + + //printf("msg2 = [%s]\n", msg2); + + // TODO: Change 'CQ n ' type messages to 'CQ 00n ' + //if(msg(1:3).eq.'CQ ' .and. msg(4:4).ge.'0' .and. msg(4:4).le.'9' & + // .and. msg(5:5).eq.' ') msg='CQ 00'//msg(4:) + + if (starts_with(msg2, "CQ ")) { + if (msg2[3] == 'D' && msg2[4] == 'X' && is_space(msg2[5])) { + // Change 'CQ DX ' to 'CQ9DX ' + msg2[2] = '9'; + } + else if (is_letter(msg2[3]) && is_letter(msg2[4]) && is_space(msg2[5])) { + // Change 'CQ xy ' type messages to 'E9xy ' + msg2[0] = 'E'; + msg2[1] = '9'; + // Delete the extra space + char *ptr = msg2 + 2; + while (*ptr) { + ptr[0] = ptr[1]; + ++ptr; + } + } + } + + // Locate the first space in the message and replace it with zero terminator + char *s1 = strchr(msg2, ' '); + if (s1 == NULL) { + // TODO: handle this (plain text message?) + return -2; + } + *s1 = 0; + ++s1; + + // Locate the second space in the message + char *s2 = strchr(s1 + 1, ' '); + if (s2 == NULL) { + // If the second space is not found, point to the end of string + // to allow for blank grid (third field) + s2 = msg2 + strlen(msg2); + } + else { + *s2 = 0; + ++s2; + } + + // Pack message fields into integers + int nc1 = packcall(msg2); + int nc2 = packcall(s1); + int ng = packgrid(s2); + + // TODO: callsign prefixes/suffixes + + // TODO: plain text messages + //call packtext(msg,nc1,nc2,ng) + //ng=ng+32768 + + if (nc1 < 0 || nc2 < 0 || ng < 0) { + return -3; + } + //printf("nc1 = %d [%04X], nc2 = %d [%04X], ng = %d\n", nc1, nc1, nc2, nc2, ng); + + // Originally the data was packed in bytes of 6 bits. + // This seems to waste memory unnecessary and complicate the code, so we pack it in 8 bit values. + pack3_8bit(nc1, nc2, ng, dat); + //pack3_6bit(nc1, nc2, ng, dat); + + return 0; +} diff --git a/pack.h b/pack.h new file mode 100644 index 0000000..1bbb77f --- /dev/null +++ b/pack.h @@ -0,0 +1,9 @@ +#pragma once + +#include + + +// Pack FT8 text message into 72 bits +// [IN] msg - FT8 message (e.g. "CQ TE5T KN01") +// [OUT] packed - 9 byte array to store the 72 bit payload (MSB first) +int packmsg(const char *msg, uint8_t *dat); diff --git a/test.cpp b/test.cpp new file mode 100644 index 0000000..bcfe654 --- /dev/null +++ b/test.cpp @@ -0,0 +1,192 @@ +#include +#include +#include +#include + +#include "pack.h" +#include "encode.h" + + +void convert_8bit_to_6bit(uint8_t *dst, const uint8_t *src, int nBits) { + // Zero-fill the destination array as we will only be setting bits later + for (int j = 0; j < (nBits + 5) / 6; ++j) { + dst[j] = 0; + } + + // Set the relevant bits + uint8_t mask_src = (1 << 7); + uint8_t mask_dst = (1 << 5); + for (int i = 0, j = 0; nBits > 0; --nBits) { + if (src[i] & mask_src) { + dst[j] |= mask_dst; + } + mask_src >>= 1; + if (mask_src == 0) { + mask_src = (1 << 7); + ++i; + } + mask_dst >>= 1; + if (mask_dst == 0) { + mask_dst = (1 << 5); + ++j; + } + } +} + + +void synth_fsk(const uint8_t *symbols, int nSymbols, float f0, float spacing, float symbol_rate, float signal_rate, float *signal) { + float phase = 0; + float dt = 1/signal_rate; + float dt_sym = 1/symbol_rate; + float t = 0; + int j = 0; + int i = 0; + while (j < nSymbols) { + float f = f0 + symbols[j] * spacing; + phase += 2 * M_PI * f / signal_rate; + signal[i] = sin(phase); + t += dt; + if (t >= dt_sym) { + // Move to the next symbol + t -= dt_sym; + ++j; + } + ++i; + } +} + + +void save_wav(const float *signal, int num_samples, int sample_rate, const char *path) { + FILE *f = fopen(path, "wb"); + char subChunk1ID[4] = {'f', 'm', 't', ' '}; + uint32_t subChunk1Size = 16; // 16 for PCM + uint16_t audioFormat = 1; // PCM = 1 + uint16_t numChannels = 1; + uint16_t bitsPerSample = 16; + uint32_t sampleRate = sample_rate; + uint16_t blockAlign = numChannels * bitsPerSample / 8; + uint32_t byteRate = sampleRate * blockAlign; + + char subChunk2ID[4] = {'d', 'a', 't', 'a'}; + uint32_t subChunk2Size = num_samples * blockAlign; + + char chunkID[4] = {'R', 'I', 'F', 'F'}; + uint32_t chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); + char format[4] = {'W', 'A', 'V', 'E'}; + + int16_t *raw_data = (int16_t *)malloc(num_samples * blockAlign); + for (int i = 0; i < num_samples; i++) { + float x = signal[i]; + if (x > 1.0) x = 1.0; + else if (x < -1.0) x = -1.0; + raw_data[i] = int(0.5 + (x * 32767.0)); + } + + // NOTE: works only on little-endian architecture + fwrite(chunkID, sizeof(chunkID), 1, f); + fwrite(&chunkSize, sizeof(chunkSize), 1, f); + fwrite(format, sizeof(format), 1, f); + + fwrite(subChunk1ID, sizeof(subChunk1ID), 1, f); + fwrite(&subChunk1Size, sizeof(subChunk1Size), 1, f); + fwrite(&audioFormat, sizeof(audioFormat), 1, f); + fwrite(&numChannels, sizeof(numChannels), 1, f); + fwrite(&sampleRate, sizeof(sampleRate), 1, f); + fwrite(&byteRate, sizeof(byteRate), 1, f); + fwrite(&blockAlign, sizeof(blockAlign), 1, f); + fwrite(&bitsPerSample, sizeof(bitsPerSample), 1, f); + + fwrite(subChunk2ID, sizeof(subChunk2ID), 1, f); + fwrite(&subChunk2Size, sizeof(subChunk2Size), 1, f); + + fwrite(raw_data, blockAlign, num_samples, f); + + fclose(f); + + free(raw_data); +} + + +void test1() { + //const char *test_in3 = "CQ DL7ACA JO40"; // 62, 32, 32, 49, 37, 27, 59, 2, 30, 19, 49, 16 + //const char *test_in3 = "VA3UG F1HMR 73"; // 52, 54, 60, 12, 55, 54, 7, 19, 2, 23, 59, 16 + const char *test_in3 = "RA3Y VE3NLS 73"; // 46, 6, 32, 22, 55, 20, 11, 32, 53, 23, 59, 16 + uint8_t test_out3[9]; + int rc = packmsg(test_in3, test_out3); + + printf("RC = %d\n", rc); + + for (int i = 0; i < 9; ++i) { + printf("%02x ", test_out3[i]); + } + printf("\n"); + + uint8_t test_out4[12]; + convert_8bit_to_6bit(test_out4, test_out3, 72); + for (int i = 0; i < 12; ++i) { + printf("%d ", test_out4[i]); + } + printf("\n"); +} + +void test2() { + uint8_t test_in[11] = { 0xF1, 0x02, 0x03, 0x04, 0x05, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xFF }; + uint8_t test_out[22]; + + encode174(test_in, test_out); + + for (int j = 0; j < 22; ++j) { + printf("%02x ", test_out[j]); + } + printf("\n"); +} + +void test3() { + uint8_t test_in2[10] = { 0x11, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x04, 0x01, 0x00 }; + uint16_t crc1 = ft8_crc(test_in2, 76); // Calculate CRC of 76 bits only + printf("CRC: %04x\n", crc1); // should be 0x0708 +} + + +int main(int argc, char **argv) { + if (argc < 3) return -1; + + //const char *message = "G0UPL YL3JG 73"; + const char *message = argv[1]; + const char *wav_path = argv[2]; + + uint8_t packed[9]; + int rc = packmsg(message, packed); + if (rc < 0) { + printf("Cannot parse message!\n"); + printf("RC = %d\n", rc); + return -2; + } + + printf("Packed data: "); + for (int j = 0; j < 9; ++j) { + printf("%02x ", packed[j]); + } + printf("\n"); + + uint8_t tones[NN]; + genft8(packed, 0, tones); + + printf("FSK tones: "); + for (int j = 0; j < NN; ++j) { + printf("%d", tones[j]); + } + printf("\n"); + + const int num_samples = (int)(0.5 + NN / 6.25 * 12000); + const int num_silence = (15 * 12000 - num_samples) / 2; + float signal[num_silence + num_samples + num_silence]; + for (int i = 0; i < num_silence + num_samples + num_silence; i++) { + signal[i] = 0; + } + + synth_fsk(tones, NN, 1000, 6.25, 6.25, 12000, signal + num_silence); + save_wav(signal, num_silence + num_samples + num_silence, 12000, wav_path); + + return 0; +} \ No newline at end of file diff --git a/utils/convert_generator.py b/utils/convert_generator.py new file mode 100644 index 0000000..4347b9f --- /dev/null +++ b/utils/convert_generator.py @@ -0,0 +1,6 @@ +import sys + +for line in sys.stdin: + line = line.strip() + b = ['0x' + line[i*2:i*2+2] for i in range(len(line)/2)] + print '{ %s },' % (', '.join(b))