From 5aa5bd077cedf7691a27b276074bbc1cabb74a4f Mon Sep 17 00:00:00 2001 From: Karlis Goba Date: Thu, 18 Oct 2018 10:42:43 +0300 Subject: [PATCH] First draft --- .gitignore | 1 + Makefile | 5 + README.md | 17 +- encode.cpp | 262 ++++++++++++++++++++++++ encode.h | 28 +++ pack.cpp | 401 +++++++++++++++++++++++++++++++++++++ pack.h | 9 + test.cpp | 192 ++++++++++++++++++ utils/convert_generator.py | 6 + 9 files changed, 919 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 encode.cpp create mode 100644 encode.h create mode 100644 pack.cpp create mode 100644 pack.h create mode 100644 test.cpp create mode 100644 utils/convert_generator.py 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))