First draft

This commit is contained in:
Karlis Goba 2018-10-18 10:42:43 +03:00
parent 1efae07b2e
commit 5aa5bd077c
9 changed files with 919 additions and 2 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.o

5
Makefile Normal file
View file

@ -0,0 +1,5 @@
CXXFLAGS = -std=c++14
LDFLAGS = -lm
test: test.o encode.o pack.o
$(CXX) $(LDFLAGS) -o $@ $^

View file

@ -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.

262
encode.cpp Normal file
View file

@ -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;
}
}

28
encode.h Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <stdint.h>
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);

401
pack.cpp Normal file
View file

@ -0,0 +1,401 @@
#include <string.h>
#include <cstdio>
#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;
}

9
pack.h Normal file
View file

@ -0,0 +1,9 @@
#pragma once
#include <stdint.h>
// 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);

192
test.cpp Normal file
View file

@ -0,0 +1,192 @@
#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <cmath>
#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;
}

View file

@ -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))