From 3d2bdbf2465899aa632eda08d5327a6c118d254a Mon Sep 17 00:00:00 2001 From: Stephen D <57571338+scd31@users.noreply.github.com> Date: Sat, 23 Mar 2024 07:05:45 -0300 Subject: [PATCH] CATS support (#86) * FIFO mode working * fifo underflow check * fix TX LED. Not perfect - comes on for longer than we're actually TXing for, because it includes the time to fill the buffer and probably some overhead. But good enough for now * move to big FIFO buffer. Had to implement a recommendation from the errata * increase timeout * a bit of cleanup I missed * encoding CATS packets inline. Still need to support GPS and NodeInfo whiskers * cleanup * additional whiskers * fix GPS and revert some settings * revert config * add balloon node info + more precise altitude * address feedback * Add comment about CATS packet parity array size * bug fixes --------- Co-authored-by: Stephen D Co-authored-by: Mikael Nousiainen --- README.md | 14 ++- src/codecs/cats/cats.c | 34 +++++++ src/codecs/cats/cats.h | 15 ++++ src/codecs/cats/crc.c | 32 +++++++ src/codecs/cats/crc.h | 9 ++ src/codecs/cats/interleaver.c | 45 ++++++++++ src/codecs/cats/interleaver.h | 9 ++ src/codecs/cats/ldpc.c | 100 +++++++++++++++++++++ src/codecs/cats/ldpc.h | 9 ++ src/codecs/cats/ldpc_matrices.c | 95 ++++++++++++++++++++ src/codecs/cats/ldpc_matrices.h | 26 ++++++ src/codecs/cats/whisker.c | 151 ++++++++++++++++++++++++++++++++ src/codecs/cats/whisker.h | 15 ++++ src/codecs/cats/whiten.c | 36 ++++++++ src/codecs/cats/whiten.h | 9 ++ src/codecs/fsk/fsk.h | 12 +++ src/codecs/raw/raw.c | 46 ++++++++++ src/codecs/raw/raw.h | 13 +++ src/config.c | 5 ++ src/config.h | 28 ++++++ src/config_internal.h | 1 + src/drivers/si4063/si4063.c | 150 ++++++++++++++++++++++++++++++- src/drivers/si4063/si4063.h | 6 ++ src/gps.h | 4 +- src/hal/delay.h | 2 + src/radio.c | 40 ++++++++- src/radio_internal.h | 2 + src/radio_payload_cats.c | 51 +++++++++++ src/radio_payload_cats.h | 8 ++ src/radio_si4063.c | 65 +++++++++++++- 30 files changed, 1023 insertions(+), 9 deletions(-) create mode 100644 src/codecs/cats/cats.c create mode 100644 src/codecs/cats/cats.h create mode 100644 src/codecs/cats/crc.c create mode 100644 src/codecs/cats/crc.h create mode 100644 src/codecs/cats/interleaver.c create mode 100644 src/codecs/cats/interleaver.h create mode 100644 src/codecs/cats/ldpc.c create mode 100644 src/codecs/cats/ldpc.h create mode 100644 src/codecs/cats/ldpc_matrices.c create mode 100644 src/codecs/cats/ldpc_matrices.h create mode 100644 src/codecs/cats/whisker.c create mode 100644 src/codecs/cats/whisker.h create mode 100644 src/codecs/cats/whiten.c create mode 100644 src/codecs/cats/whiten.h create mode 100644 src/codecs/raw/raw.c create mode 100644 src/codecs/raw/raw.h create mode 100644 src/radio_payload_cats.c create mode 100644 src/radio_payload_cats.h diff --git a/README.md b/README.md index 3100794..0c62572 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,17 @@ On an external Si5351 clock generator connected to the external I²C bus of the * The Horus 4FSK v1 and v2 modes have significantly [improved performance compared to APRS or RTTY](https://github.com/projecthorus/horusdemodlib/wiki). * Use [horus-gui](https://github.com/projecthorus/horus-gui) software to receive the 4FSK mode and to submit packets to [Habhub](http://habhub.org/) high-altitude balloon tracking platform. * See [horus-gui installation and usage instructions](https://github.com/projecthorus/horusdemodlib/wiki/1.1-Horus-GUI-Reception-Guide-(Windows-Linux-OSX)) and [horusdemodlib](https://github.com/projecthorus/horusdemodlib) library that is responsible for demodulating the signal. -* In order to use Horus 4FSK mode on a flight, you will need to request a new Horus 4FSK payload ID in GitHub according to the instructions at: https://github.com/projecthorus/horusdemodlib/wiki#how-do-i-transmit-it +* In order to use Horus 4FSK mode on a flight, you will need to request a new Horus 4FSK payload ID in GitHub according to the instructions at: https://github.com/projecthorus/horusdemodlib/wiki#how-do-i-transmit-it + +#### Notes about CATS (DFM-17 only) + +* CATS is a [modern packet radio standard](https://cats.radio/) designed for communication and telemetry. Due to its increased efficiency over APRS, it allows for fast beacon times (1 Hz or more) without congesting the network. +* To receive CATS, you can either use an [I-Gate board](https://www.tindie.com/products/hamcats/cats-i-gate-board/) on a Raspberry Pi, or just a standard RTL-SDR dongle. + * See [here](https://gitlab.scd31.com/cats/igate) for the I-Gate board software. + * See [here](https://gitlab.scd31.com/cats/sdr-igate) for the SDR software. +* In either case, CATS packets that are received get forwarded to FELINET, and relayed to APRS-IS. This means your CATS packets will show up on [aprs.fi](https://aprs.fi) + * If you're relying on APRS gating, be sure to set an SSID below 100 or the APRS network may reject it. +* For more information, be sure to check [the standard](https://gitlab.scd31.com/cats/cats-standard/-/blob/master/standard.pdf). ### External sensors (RS41 only) @@ -607,6 +617,8 @@ rtl_fm -f 432500000 -M fm -s 250k -r 48000 -g 22 - | ./aprs - * Various authors with smaller contributions from GitHub pull requests * Original codebase: DF8OE and other authors of the [RS41HUP](https://github.com/df8oe/RS41HUP) project * Horus 4FSK code adapted from [darksidelemm fork of RS41HUP](https://github.com/darksidelemm/RS41HUP) project +* CATS code adapted from [CATS reference implementation](https://gitlab.scd31.com/cats/ham-cats/) + * LDPC encoder adapted from [libCATS](https://github.com/CamK06/libCATS) # Additional documentation diff --git a/src/codecs/cats/cats.c b/src/codecs/cats/cats.c new file mode 100644 index 0000000..d831cfe --- /dev/null +++ b/src/codecs/cats/cats.c @@ -0,0 +1,34 @@ +#include + +#include "cats.h" +#include "crc.h" +#include "whiten.h" +#include "ldpc.h" +#include "interleaver.h" + +cats_packet cats_create(uint8_t *data) +{ + cats_packet c = {.data = data, .len = 0}; + return c; +} + +// This makes changes to packet->data. +// Don't call it more than once, and don't use +// the packet after calling! +size_t cats_fully_encode(cats_packet packet, uint8_t *out) +{ + + // 0. CRC + int new_len = cats_append_crc(packet.data, packet.len); + // 1. whiten + cats_whiten(packet.data, new_len); + // 2. ldpc + new_len = cats_ldpc_encode(packet.data, new_len); + // 3. length + out[0] = new_len; + out[1] = new_len >> 8; + // 4. interleave + cats_interleave(out + 2, packet.data, new_len); + + return new_len + 2; +} diff --git a/src/codecs/cats/cats.h b/src/codecs/cats/cats.h new file mode 100644 index 0000000..b978e74 --- /dev/null +++ b/src/codecs/cats/cats.h @@ -0,0 +1,15 @@ +#ifndef __CATS_H +#define __CATS_H + +#include +#include + +typedef struct _cats_packet { + uint8_t *data; + size_t len; +} cats_packet; + +cats_packet cats_create(uint8_t *payload); +size_t cats_fully_encode(cats_packet packet, uint8_t *out); + +#endif diff --git a/src/codecs/cats/crc.c b/src/codecs/cats/crc.c new file mode 100644 index 0000000..9887333 --- /dev/null +++ b/src/codecs/cats/crc.c @@ -0,0 +1,32 @@ +#include "crc.h" + +uint16_t calc_crc(uint8_t *data, size_t length); + +size_t cats_append_crc(uint8_t *data, size_t len) +{ + uint16_t crc = calc_crc(data, len); + data[len++] = crc; + data[len++] = crc >> 8; + + return len; +} + +// https://stackoverflow.com/questions/69850602/converting-c-to-java-for-crc16-ibm-sdlc +uint16_t calc_crc(uint8_t *data, size_t length) +{ + uint8_t *ptr = data; + uint8_t crcbyte1 = 0xFF; + uint8_t crcbyte2 = 0xFF; + for (int i = 0; i < length; i++) { + uint8_t r1 = *ptr++ ^ crcbyte2; + r1 = (r1 << 4) ^ r1; + crcbyte2 = (r1 << 4) | (r1 >> 4); + crcbyte2 = (crcbyte2 & 0x0F ) ^ crcbyte1; + crcbyte1 = r1; + r1 = (r1 << 3) | (r1 >> 5); + crcbyte2 = crcbyte2 ^ (r1 & 0xF8); + crcbyte1 = crcbyte1 ^ (r1 & 0x07); + } + + return ~((crcbyte1 << 8) | crcbyte2); +} diff --git a/src/codecs/cats/crc.h b/src/codecs/cats/crc.h new file mode 100644 index 0000000..1b12dda --- /dev/null +++ b/src/codecs/cats/crc.h @@ -0,0 +1,9 @@ +#ifndef __CATS_CRC_H +#define __CATS_CRC_H + +#include +#include + +size_t cats_append_crc(uint8_t *data, size_t len); + +#endif diff --git a/src/codecs/cats/interleaver.c b/src/codecs/cats/interleaver.c new file mode 100644 index 0000000..9118118 --- /dev/null +++ b/src/codecs/cats/interleaver.c @@ -0,0 +1,45 @@ +#include + +#include "interleaver.h" + +static inline bool get_bit(uint8_t *arr, size_t i); +static inline void set_bit(uint8_t *arr, size_t i, bool value); + +void cats_interleave(uint8_t *dest, uint8_t *src, size_t len) +{ + size_t bit_len = len * 8; + + size_t out_i = 0; + for(int i = 0; i < 32; i++) { + for(int j = 0; j < bit_len; j += 32) { + if(i + j >= bit_len) { + continue; + } + + set_bit(dest, out_i, get_bit(src, i + j)); + out_i++; + } + } +} + +static inline bool get_bit(uint8_t *arr, size_t i) +{ + size_t byte_idx = i / 8; + int bit_idx = 7 - (i % 8); + + return (arr[byte_idx] >> bit_idx) & 1; +} + + +static inline void set_bit(uint8_t *arr, size_t i, bool value) +{ + size_t byte_idx = i / 8; + int bit_idx = 7 - (i % 8); + + if(value) { + arr[byte_idx] |= 1 << bit_idx; + } + else { + arr[byte_idx] &= ~(1 << bit_idx); + } +} diff --git a/src/codecs/cats/interleaver.h b/src/codecs/cats/interleaver.h new file mode 100644 index 0000000..4475819 --- /dev/null +++ b/src/codecs/cats/interleaver.h @@ -0,0 +1,9 @@ +#ifndef __CATS_INTERLEAVER_H +#define __CATS_INTERLEAVER_H + +#include +#include + +void cats_interleave(uint8_t *dest, uint8_t *src, size_t len); + +#endif diff --git a/src/codecs/cats/ldpc.c b/src/codecs/cats/ldpc.c new file mode 100644 index 0000000..c9709c6 --- /dev/null +++ b/src/codecs/cats/ldpc.c @@ -0,0 +1,100 @@ +#include +#include + +#include "ldpc.h" +#include "ldpc_matrices.h" + +#define GET_BIT(byte, bit) (((byte) & (1 << (7-(bit)))) != 0) +#define SET_BIT(byte, bit) (byte |= 1 << 7-bit) +#define CLEAR_BIT(byte, bit) (byte &= ~(1 << 7-bit)) +#define FLIP_BIT(byte, bit) (byte ^= (1 << (7 - bit))) + +cats_ldpc_code_t *cats_ldpc_pick_code(int len) +{ + if(len >= 128) { + return &tm2048; + } + else if(len >= 32) { + return &tc512; + } + else if(len >= 16) { + return &tc256; + } + else { + return &tc128; + } +} + +// For more information on this, see the CATS standard +// https://gitlab.scd31.com/cats/cats-standard +// Section 5.2 +// returns # of bytes written to parity_out +size_t cats_ldpc_encode_chunk(uint8_t *data, cats_ldpc_code_t *code, uint8_t *parity_out) +{ + // Code parameters + int data_length_bits = code->data_length_bits; + int parity_length_bits = code->code_length_bits - code->data_length_bits; + int circ_size = code->circulant_size; + const uint64_t* gc = code->matrix; + int row_len = parity_length_bits / 64; + + memset(parity_out, 0x00, parity_length_bits / 8); + + for (int offset = 0; offset < circ_size; offset++) { + for (int crow = 0; crow < data_length_bits / circ_size; crow++) { + int bit = crow * circ_size + offset; + if(GET_BIT(data[bit / 8], bit % 8)) { + for (int idx = 0; idx < row_len; idx++) { + uint64_t circ = gc[(crow * row_len) + idx]; + for(int j = 0; j < 8; j++) { + parity_out[idx*8 + j] ^= (uint8_t)(circ >> ((7 - j) * 8)); + } + } + } + } + + for (int block = 0; block < parity_length_bits / circ_size; block++) { + uint8_t* parityblock = &parity_out[block * circ_size / 8]; + uint8_t carry = parityblock[0] >> 7; + for (int x = (circ_size / 8)-1; x >= 0; x--) { + uint8_t c = parityblock[x] >> 7; + parityblock[x] = (parityblock[x] << 1) | carry; + carry = c; + } + } + } + + return parity_length_bits / 8; +} + +size_t cats_ldpc_encode(uint8_t *data, size_t len) +{ + // Didn't implement the 512-byte LDPC variant - unnecessary and uses a lot of space + // This means we can only encode up to 511 bytes + assert(len < 512); + + // A 128 byte array is needed to support CATS packets up to 511 bytes. + uint8_t parity[128]; + + // Split data into blocks and encode each block + int i = 0; + while(i < len) { + cats_ldpc_code_t *code = cats_ldpc_pick_code(len - i); + int data_length_bits = code->data_length_bits; + int data_length = data_length_bits / 8; + + uint8_t chunk[code->code_length_bits / 8]; + memset(chunk, 0xAA, data_length); + memcpy(chunk, data + i, (len-i < data_length) ? (len - i) : data_length); + + size_t parity_len = cats_ldpc_encode_chunk(chunk, code, parity); + memcpy(data + len + i, parity, parity_len); // Parity + + i += parity_len; + } + + int new_len = (len*2) + (i-len) + 2; // (Data+Parity) + (Padded parity) + length + data[new_len - 2] = len; + data[new_len - 1] = len >> 8; + return new_len; +} diff --git a/src/codecs/cats/ldpc.h b/src/codecs/cats/ldpc.h new file mode 100644 index 0000000..dbe41dc --- /dev/null +++ b/src/codecs/cats/ldpc.h @@ -0,0 +1,9 @@ +#ifndef __CATS_LDPC_H +#define __CATS_LDPC_H + +#include +#include + +size_t cats_ldpc_encode(uint8_t *data, size_t len); + +#endif diff --git a/src/codecs/cats/ldpc_matrices.c b/src/codecs/cats/ldpc_matrices.c new file mode 100644 index 0000000..1711168 --- /dev/null +++ b/src/codecs/cats/ldpc_matrices.c @@ -0,0 +1,95 @@ +#include +#include + +#include "ldpc_matrices.h" + +cats_ldpc_code_t tc128 = { + .code_length_bits = 128, + .data_length_bits = 64, + .punctured_bits = 0, + .bf_working_len = 128 + 0, + .circulant_size = 128/8, + .matrix = tc128_matrix, + .matrix_len = 4 +}; + +cats_ldpc_code_t tc256 = { + .code_length_bits = 256, + .data_length_bits = 128, + .punctured_bits = 0, + .bf_working_len = 256 + 0, + .circulant_size = 256/8, + .matrix = tc256_matrix, + .matrix_len = 8 +}; + +cats_ldpc_code_t tc512 = { + .code_length_bits = 512, + .data_length_bits = 256, + .punctured_bits = 0, + .bf_working_len = 512 + 0, + .circulant_size = 512/8, + .matrix = tc512_matrix, + .matrix_len = 16 +}; + +cats_ldpc_code_t tm2048 = { + .code_length_bits = 2048, + .data_length_bits = 1024, + .punctured_bits = 512, + .bf_working_len = 2048 + 512, + .circulant_size = 512/4, + .matrix = tm2048_matrix, + .matrix_len = 128 +}; + +const uint64_t tc128_matrix[] = { + 0x0E69166BEF4C0BC2, 0x7766137EBB248418, 0xC480FEB9CD53A713, 0x4EAA22FA465EEA11 +}; + +const uint64_t tc256_matrix[] = { + 0x73F5E8390220CE51, 0x36ED68E9F39EB162, 0xBAC812C0BCD24379, 0x4786D9285A09095C, + 0x7DF83F76A5FF4C38, 0x8E6C0D4E025EB712, 0xBAA37B3260CB31C5, 0xD0F66A31FAF511BC +}; + +const uint64_t tc512_matrix[] = { + 0x1D21794A22761FAE, 0x59945014257E130D, 0x74D6054003794014, 0x2DADEB9CA25EF12E, + 0x60E0B6623C5CE512, 0x4D2C81ECC7F469AB, 0x20678DBFB7523ECE, 0x2B54B906A9DBE98C, + 0xF6739BCF54273E77, 0x167BDA120C6C4774, 0x4C071EFF5E32A759, 0x3138670C095C39B5, + 0x28706BD045300258, 0x2DAB85F05B9201D0, 0x8DFDEE2D9D84CA88, 0xB371FAE63A4EB07E +}; + +const uint64_t tm2048_matrix[] = { + 0xCFA794F49FA5A0D8, 0x8BB31D8FCA7EA8BB, 0xA7AE7EE8A68580E3, 0xE922F9E13359B284, + 0x91F72AE8F2D6BF78, 0x30A1F83B3CDBD463, 0xCE95C0EC1F609370, 0xD7E791C870229C1E, + 0x71EF3FDF60E28784, 0x78934DB285DEC9DC, 0x0E95C103008B6BCD, 0xD2DAF85CAE732210, + 0x8326EE83C1FBA56F, 0xDD15B2DDB31FE7F2, 0x3BA0BB43F83C67BD, 0xA1F6AEE46AEF4E62, + 0x565083780CA89ACA, 0xA70CCFB4A888AE35, 0x1210FAD0EC9602CC, 0x8C96B0A86D3996A3, + 0xC0B07FDDA73454C2, 0x5295F72BD5004E80, 0xACCF973FC30261C9, 0x90525AA0CBA006BD, + 0x9F079F09A405F7F8, 0x7AD98429096F2A7E, 0xEB8C9B13B84C06E4, 0x2843A47689A9C528, + 0xDAAA1A175F598DCF, 0xDBAD426CA43AD479, 0x1BA78326E75F38EB, 0x6ED09A45303A6425, + 0x48F42033B7B9A051, 0x49DC839C90291E98, 0x9B2CEBE50A7C2C26, 0x4FC6E7D674063589, + 0xF5B6DEAEBF72106B, 0xA9E6676564C17134, 0x6D5954558D235191, 0x50AAF88D7008E634, + 0x1FA962FBAB864A5F, 0x867C9D6CF4E087AA, 0x5D7AA674BA4B1D8C, 0xD7AE9186F1D3B23B, + 0x047F112791EE97B6, 0x3FB7B58FF3B94E95, 0x93BE39A6365C66B8, 0x77AD316965A72F5B, + 0x1B58F88E49C00DC6, 0xB35855BFF228A088, 0x5C8ED47B61EEC66B, 0x5004FB6E65CBECF3, + 0x77789998FE80925E, 0x0237F570E04C5F5B, 0xED677661EB7FC382, 0x5AB5D5D968C0808C, + 0x2BDB828B19593F41, 0x671B8D0D41DF136C, 0xCB47553C9B3F0EA0, 0x16CC1554C35E6A7D, + 0x97587FEA91D2098E, 0x126EA73CC78658A6, 0xADE19711208186CA, 0x95C7417A15690C45, + 0xBE9C169D889339D9, 0x654C976A85CFD9F7, 0x47C4148E3B4712DA, 0xA3BAD1AD71873D3A, + 0x1CD630C342C5EBB9, 0x183ADE9BEF294E8E, 0x7014C077A5F96F75, 0xBE566C866964D01C, + 0xE72AC43A35AD2166, 0x72EBB3259B77F9BB, 0x18DA8B09194FA1F0, 0xE876A080C9D6A39F, + 0x809B168A3D88E8E9, 0x3D995CE5232C2DC2, 0xC7CFA44A363F628A, 0x668D46C398CAF96F, + 0xD57DBB24AE27ACA1, 0x716F8EA1B8AA1086, 0x7B7796F4A86F1FD5, 0x4C7576AD01C68953, + 0xE75BE79902448236, 0x8F069658F7AAAFB0, 0x975F3AF795E78D25, 0x5871C71B4F4B77F6, + 0x65CD9C359BB2A82D, 0x5353E007166BDD41, 0x2C5447314DB027B1, 0x0B130071AD0398D1, + 0xDE19BC7A6BBCF6A0, 0xFF021AABF12920A5, 0x58BAED484AF89E29, 0xD4DBC170CEF1D369, + 0x4C330B2D11E15B5C, 0xB3815E09605338A6, 0x75E3D1A3541E0E28, 0x4F6556D68D3C8A9E, + 0xE5BB3B297DB62CD2, 0x907F09996967A0F4, 0xFF33AEEE2C8A4A52, 0xFCCF5C39D355C39C, + 0x5FE5F09ABA6BCCE0, 0x2A73401E5F87EAC2, 0xD75702F4F57670DF, 0xA70B1C002F523EEA, + 0x6CE1CE2E05D420CB, 0x867EC0166B8E53A9, 0x9DF9801A1C33058D, 0xD116A0AE7278BBB9, + 0x4CF0B0C792DD8FDB, 0x3ECEAE6F2B7F663D, 0x106A1C296E47C14C, 0x1498B045D57DEFB5, + 0x968F6D8C790263C3, 0x53CF307EF90C1F21, 0x66E6B632F6614E58, 0x267EF096C37718A3, + 0x3D46E5D10E993EB6, 0xDF81518F885EDA1B, 0x6FF518FD48BB8E9D, 0xDBED4AC0F4F5EB89, + 0xBCC64D21A65DB379, 0xABE2E4DC21F109FF, 0x2EC0CE7B5D40973D, 0x13ECF713B01C6F10 +}; diff --git a/src/codecs/cats/ldpc_matrices.h b/src/codecs/cats/ldpc_matrices.h new file mode 100644 index 0000000..cddf7c4 --- /dev/null +++ b/src/codecs/cats/ldpc_matrices.h @@ -0,0 +1,26 @@ +#ifndef __CATS_LDPC_MATRICES_H +#define __CATS_LDPC_MATRICES_H + +typedef struct cats_ldpc_code { + // Code length in bits (data+parity) + int code_length_bits; + // Data length in bits + int data_length_bits; + int punctured_bits; + int bf_working_len; + size_t circulant_size; + size_t matrix_len; + const uint64_t* matrix; +} cats_ldpc_code_t; + +extern cats_ldpc_code_t tc128; +extern cats_ldpc_code_t tc256; +extern cats_ldpc_code_t tc512; +extern cats_ldpc_code_t tm2048; + +extern const uint64_t tc128_matrix[]; +extern const uint64_t tc256_matrix[]; +extern const uint64_t tc512_matrix[]; +extern const uint64_t tm2048_matrix[]; + +#endif diff --git a/src/codecs/cats/whisker.c b/src/codecs/cats/whisker.c new file mode 100644 index 0000000..bb7c54a --- /dev/null +++ b/src/codecs/cats/whisker.c @@ -0,0 +1,151 @@ +#include +#include + +#include "whisker.h" + +// Hardware IDs defined here: https://cats.radio/pages/hardware-ids.html +// It's just a way to tell where CATS packets come from. It doesn't affect anything beyond this +#ifdef RS41 +#define HARDWARE_ID 0x7d00 +#endif +#ifdef DFM17 +#define HARDWARE_ID 0x7d01 +#endif + +#define CATS_IDENT_TYPE 0x00 +#define CATS_GPS_TYPE 0x02 +#define CATS_COMMENT_TYPE 0x03 +#define CATS_NODE_INFO_TYPE 0x09 + +#define CATS_NODE_INFO_HARDWARE_ID_PRESENT 1 +#define CATS_NODE_INFO_SOFTWARE_ID_PRESENT 2 +#define CATS_NODE_INFO_ANTENNA_GAIN_PRESENT 16 +#define CATS_NODE_INFO_TX_POWER_PRESENT 32 +#define CATS_NODE_INFO_VOLTAGE_PRESENT 64 +#define CATS_NODE_INFO_TX_TEMP_PRESENT 128 + +// corresponds to (21 / 4 = 5.25) dBi antenna gain +#define CATS_ANTENNA_GAIN 21 + +// Software IDs are basically just a version number. +// Can increment this whenever something big changes, and then +// you can track what version of your software is being used on CATS +#define SOFTWARE_ID 0x00 + +static inline void cats_push_f16(cats_packet *p, float val) +{ + union { + float f; + uint32_t u; + } fu = { .f = val }; + + assert(sizeof fu.f == sizeof fu.u); + + uint32_t f32 = fu.u; + uint32_t sign = (f32 >> 16) & 0x8000; + uint32_t exponent = ((f32 >> 23) - 127 + 15) & 0x1F; + uint32_t mantissa = f32 & 0x007FFFFF; + uint32_t f16 = sign | (exponent << 10) | (mantissa >> 13); + + p->data[p->len++] = f16; + p->data[p->len++] = f16 >> 8; +} + +static inline void cats_push_f32(cats_packet *p, float val) +{ + union { + float f; + uint32_t u; + } fu = { .f = val }; + + assert(sizeof fu.f == sizeof fu.u); + + uint32_t f32 = fu.u; + p->data[p->len++] = f32; + p->data[p->len++] = f32 >> 8; + p->data[p->len++] = f32 >> 16; + p->data[p->len++] = f32 >> 24; +} + +static inline void cats_push_u16(cats_packet *p, uint16_t val) +{ + for(int i = 0; i < 2; i++) { + p->data[p->len++] = val >> (i * 8); + } +} + +static inline void cats_push_u32(cats_packet *p, uint32_t val) +{ + + for(int i = 0; i < 4; i++) { + p->data[p->len++] = val >> (i * 8); + } +} + +void cats_append_identification_whisker(cats_packet *packet, char *callsign, uint8_t ssid, uint16_t icon) +{ + packet->data[packet->len++] = CATS_IDENT_TYPE; + size_t callsign_len = strlen(callsign); + packet->data[packet->len++] = callsign_len + 3; // len + packet->data[packet->len++] = icon & 0xFF; // icon + packet->data[packet->len++] = (icon >> 8) & 0xFF; // icon + memcpy(packet->data + packet->len, callsign, callsign_len); //callsign + packet->len += callsign_len; + packet->data[packet->len++] = ssid; // ssid +} + +void cats_append_gps_whisker(cats_packet *packet, gps_data gps) +{ + packet->data[packet->len++] = CATS_GPS_TYPE; + packet->data[packet->len++] = 14; // len + int32_t lat_converted = gps.latitude_degrees_1000000 * (1LL<<31) / 90LL / 10000000LL; + int32_t lon_converted = gps.longitude_degrees_1000000 * (1LL<<31) / 180LL / 10000000LL; + cats_push_u32(packet, lat_converted); // lat + cats_push_u32(packet, lon_converted); // lon + cats_push_f16(packet, gps.altitude_mm / 1000.0); // alt + packet->data[packet->len++] = 0; // max error = 0m + packet->data[packet->len++] = gps.heading_degrees_100000 / 100000.0 * 128.0 / 180.0; // heading + cats_push_f16(packet, gps.ground_speed_cm_per_second / 100.0); // horizontal speed +} + +void cats_append_comment_whisker(cats_packet *packet, char *comment) +{ + packet->data[packet->len++] = CATS_COMMENT_TYPE; + size_t comment_len = strlen(comment); + assert(comment_len <= 255); + packet->data[packet->len++] = comment_len; + memcpy(packet->data + packet->len, comment, comment_len); + packet->len += comment_len; +} + +void cats_append_node_info_whisker(cats_packet *packet, telemetry_data *data) +{ + uint8_t *d = packet->data; + size_t *l = &packet->len; + + bool has_altitude = GPS_HAS_FIX(data->gps); + float altitude = data->gps.altitude_mm / 1000.0; + + d[(*l)++] = CATS_NODE_INFO_TYPE; + d[(*l)++] = has_altitude ? 14 : 10; // len + // bitmask + d[(*l)++] = 0; + d[(*l)++] = (CATS_IS_BALLOON << 2) | (has_altitude << 1); + d[(*l)++] = + CATS_NODE_INFO_HARDWARE_ID_PRESENT | + CATS_NODE_INFO_SOFTWARE_ID_PRESENT | + CATS_NODE_INFO_ANTENNA_GAIN_PRESENT | + CATS_NODE_INFO_TX_POWER_PRESENT | + CATS_NODE_INFO_VOLTAGE_PRESENT | + CATS_NODE_INFO_TX_TEMP_PRESENT; + + cats_push_u16(packet, HARDWARE_ID); + d[(*l)++] = SOFTWARE_ID; + d[(*l)++] = CATS_ANTENNA_GAIN; // vertical antenna -> ~5.25 dBi gain + d[(*l)++] = CATS_REPORTED_TX_POWER_DBM * 4.0; // TX power + d[(*l)++] = data->battery_voltage_millivolts / 100; // voltage + d[(*l)++] = data->internal_temperature_celsius_100 / 100; // transmitter temperature + if(has_altitude) { + cats_push_f32(packet, altitude); + } +} diff --git a/src/codecs/cats/whisker.h b/src/codecs/cats/whisker.h new file mode 100644 index 0000000..8ac2e3d --- /dev/null +++ b/src/codecs/cats/whisker.h @@ -0,0 +1,15 @@ +#ifndef __WHISKER_H +#define __WHISKER_H + +#include + +#include "cats.h" +#include "../../gps.h" +#include "../../telemetry.h" + +void cats_append_identification_whisker(cats_packet *packet, char *callsign, uint8_t ssid, uint16_t icon); +void cats_append_gps_whisker(cats_packet *packet, gps_data gps); +void cats_append_comment_whisker(cats_packet *packet, char *message); +void cats_append_node_info_whisker(cats_packet *packet, telemetry_data *data); + +#endif diff --git a/src/codecs/cats/whiten.c b/src/codecs/cats/whiten.c new file mode 100644 index 0000000..9de2652 --- /dev/null +++ b/src/codecs/cats/whiten.c @@ -0,0 +1,36 @@ +#include + +#include "whiten.h" + +uint8_t lfsr_byte(uint16_t *state); +void lfsr(uint16_t *state); + +void cats_whiten(uint8_t *data, uint8_t len) +{ + uint16_t state = 0xE9CF; + + for(int i = 0; i < len; i++) { + uint8_t b = lfsr_byte(&state); + data[i] ^= b; + } +} + +uint8_t lfsr_byte(uint16_t *state) +{ + uint8_t out = 0; + for(int i = 7; i >= 0; i--) { + out |= (*state & 1) << i; + lfsr(state); + } + + return out; +} + +void lfsr(uint16_t *state) +{ + bool lsb = *state & 1; + *state >>= 1; + if(lsb) { + *state ^= 0xB400; + } +} diff --git a/src/codecs/cats/whiten.h b/src/codecs/cats/whiten.h new file mode 100644 index 0000000..f1692ac --- /dev/null +++ b/src/codecs/cats/whiten.h @@ -0,0 +1,9 @@ +#ifndef __CATS_WHITEN_H +#define __CATS_WHITEN_H + +#include +#include + +void cats_whiten(uint8_t *data, uint8_t len); + +#endif diff --git a/src/codecs/fsk/fsk.h b/src/codecs/fsk/fsk.h index ca4ab34..782f791 100644 --- a/src/codecs/fsk/fsk.h +++ b/src/codecs/fsk/fsk.h @@ -38,6 +38,18 @@ typedef struct _fsk_encoder_api { */ uint32_t (*get_symbol_delay)(fsk_encoder *encoder); + /** + * @param encoder + * @return Pointer to packet data + */ + uint8_t *(*get_data)(fsk_encoder *encoder); + + /** + * @param encoder + * @return Length of packet data + */ + uint16_t (*get_data_len)(fsk_encoder *encoder); + void (*set_data)(fsk_encoder *encoder, uint16_t data_length, uint8_t *data); int8_t (*next_tone)(fsk_encoder *encoder); diff --git a/src/codecs/raw/raw.c b/src/codecs/raw/raw.c new file mode 100644 index 0000000..7be41e8 --- /dev/null +++ b/src/codecs/raw/raw.c @@ -0,0 +1,46 @@ +#include + +#include "raw.h" + +typedef struct _raw_encoder { + uint8_t *data; + uint16_t len; +} raw_encoder; + +void raw_encoder_new(fsk_encoder *encoder) +{ + encoder->priv = malloc(sizeof(raw_encoder)); +} + +void raw_encoder_destroy(fsk_encoder *encoder) +{ + if (encoder->priv != NULL) { + free(encoder->priv); + encoder->priv = NULL; + } +} + +void raw_encoder_set_data(fsk_encoder *encoder, uint16_t data_length, uint8_t *data) +{ + raw_encoder *raw = (raw_encoder *) encoder->priv; + raw->data = data; + raw->len = data_length; +} + +uint8_t *raw_encoder_get_data(fsk_encoder *encoder) +{ + raw_encoder *raw = (raw_encoder *) encoder->priv; + return raw->data; +} + +uint16_t raw_encoder_get_data_len(fsk_encoder *encoder) +{ + raw_encoder *raw = (raw_encoder *) encoder->priv; + return raw->len; +} + +fsk_encoder_api raw_fsk_encoder_api = { + .set_data = raw_encoder_set_data, + .get_data = raw_encoder_get_data, + .get_data_len = raw_encoder_get_data_len, +}; diff --git a/src/codecs/raw/raw.h b/src/codecs/raw/raw.h new file mode 100644 index 0000000..aeead08 --- /dev/null +++ b/src/codecs/raw/raw.h @@ -0,0 +1,13 @@ +#ifndef __RAW_H +#define __RAW_H + +#include + +#include "codecs/fsk/fsk.h" + +void raw_encoder_new(fsk_encoder *encoder); +void raw_encoder_destroy(fsk_encoder *encoder); + +extern fsk_encoder_api raw_fsk_encoder_api; + +#endif diff --git a/src/config.c b/src/config.c index ada1e77..a37caff 100644 --- a/src/config.c +++ b/src/config.c @@ -90,6 +90,11 @@ char *aprs_comment_templates[] = { NULL }; +char *cats_comment_templates[] = { + CATS_COMMENT, + NULL +}; + /** * FSQ mode comment message templates. * Maximum length: 130 characters. diff --git a/src/config.h b/src/config.h index 7eed4c2..58fb916 100644 --- a/src/config.h +++ b/src/config.h @@ -188,6 +188,8 @@ #define RADIO_SI4063_TX_HORUS_V1_COUNT 1 #define RADIO_SI4063_TX_HORUS_V2 true #define RADIO_SI4063_TX_HORUS_V2_COUNT 6 +#define RADIO_SI4063_TX_CATS false +#define RADIO_SI4063_TX_CATS_COUNT 1 // Continuous transmit mode can be enabled for *either* Horus V1 or V2, but not both. This disables all other transmission modes. // The continuous mode transmits Horus 4FSK preamble between transmissions @@ -202,6 +204,7 @@ // Use a frequency offset to place FSK tones slightly above the defined frequency for SSB reception #define RADIO_SI4063_TX_FREQUENCY_HORUS_V1 432501000 #define RADIO_SI4063_TX_FREQUENCY_HORUS_V2 432501000 +#define RADIO_SI4063_TX_FREQUENCY_CATS 430500000 /** * RS41 only: External Si5351 radio chip transmission configuration @@ -335,6 +338,31 @@ // Delay transmission for an N second offset, counting from the scheduled time set with TIME_SYNC_SECONDS. #define HORUS_V2_TIME_SYNC_OFFSET_SECONDS 0 +/** + * CATS mode settings + */ +// CATS is a new digital mode, vaguely meant to be a better APRS (but also much more powerful) +// While it offers a number of advantages, probably the best one for balloons is the rapid beacon rate. +// The protocol is meant to allow for a much higher channel capacity, so beaconing every second is totally fine. +// For more information, see here: https://cats.radio/ +#define CATS_CALLSIGN CALLSIGN +#define CATS_SSID 29 // 0 - 255 +// Balloon. See the CATS standard for more options +// https://gitlab.scd31.com/cats/cats-standard/-/blob/master/standard.pdf +#define CATS_ICON 13 +#define CATS_COMMENT "I am a radiosonde. Hear me meow!" +#define CATS_REPORTED_TX_POWER_DBM 20 +// You probably want this to be true +// Set to false if you're using your radiosonde for something other than a balloon payload +// We don't want non-balloons showing up as balloons on FELINET! +#define CATS_IS_BALLOON true + +// Schedule transmission every N seconds, counting from beginning of an hour (based on GPS time). Set to zero to disable time sync. +// See the README file for more detailed documentation about time sync and its offset setting +#define CATS_TIME_SYNC_SECONDS 0 +// Delay transmission for an N second offset, counting from the scheduled time set with TIME_SYNC_SECONDS. +#define CATS_TIME_SYNC_OFFSET_SECONDS 0 + /** * CW settings */ diff --git a/src/config_internal.h b/src/config_internal.h index 73aa5ce..d9ef733 100644 --- a/src/config_internal.h +++ b/src/config_internal.h @@ -30,6 +30,7 @@ extern volatile bool system_initialized; extern char *cw_message_templates[]; extern char *pip_message_templates[]; extern char *aprs_comment_templates[]; +extern char *cats_comment_templates[]; extern char *fsq_comment_templates[]; extern char *ftjt_message_templates[]; diff --git a/src/drivers/si4063/si4063.c b/src/drivers/si4063/si4063.c index 1f69d94..325bde1 100644 --- a/src/drivers/si4063/si4063.c +++ b/src/drivers/si4063/si4063.c @@ -37,14 +37,19 @@ #define SI4063_COMMAND_POWER_UP 0x02 #define SI4063_COMMAND_SET_PROPERTY 0x11 #define SI4063_COMMAND_GPIO_PIN_CFG 0x13 +#define SI4063_COMMAND_FIFO_INFO 0x15 +#define SI4063_COMMAND_GET_INT_STATUS 0x20 #define SI4063_COMMAND_START_TX 0x31 +#define SI4063_COMMAND_REQUEST_DEVICE_STATE 0x33 #define SI4063_COMMAND_CHANGE_STATE 0x34 #define SI4063_COMMAND_GET_ADC_READING 0x14 #define SI4063_COMMAND_READ_CMD_BUFF 0x44 +#define SI4063_COMMAND_WRITE_TX_FIFO 0x66 #define SI4063_STATE_SLEEP 0x01 #define SI4063_STATE_SPI_ACTIVE 0x02 #define SI4063_STATE_READY 0x03 +#define SI4063_STATE_READY2 0x04 #define SI4063_STATE_TX_TUNE 0x05 #define SI4063_STATE_TX 0x07 @@ -218,10 +223,91 @@ void si4063_disable_tx() si4063_set_state(SI4063_STATE_SLEEP); } +// Returns number of bytes sent from *data +// If less than len, remaining bytes will need to be used to top up the buffer +uint16_t si4063_start_tx(uint8_t *data, int len) +{ + // Clear fifo underflow interrupt + si4063_fifo_underflow(); + + // Clear TX FIFO + uint8_t fifo_clear_data[] = {1}; + si4063_send_command(SI4063_COMMAND_FIFO_INFO, 1, fifo_clear_data); + si4063_wait_for_cts(); + + // Add our data to the TX FIFO + int fifo_len = len; + if(fifo_len > 64) { + fifo_len = 64; + } + si4063_send_command(SI4063_COMMAND_WRITE_TX_FIFO, fifo_len, data); + + // Start transmitting + uint8_t tx_cmd[] = { + 0, // channel + SI4063_STATE_SLEEP << 4, + len >> 8, + len & 0xFF, + 0 // delay + }; + si4063_send_command(SI4063_COMMAND_START_TX, sizeof(tx_cmd), tx_cmd); + si4063_wait_for_cts(); + + return fifo_len; +} + +// Add additional bytes to the si4063's FIFO buffer +// Needed for large packets that don't fit in its buffer. +// Keep refilling it while transmitting +// Returns number of bytes taken from *data +// If less than len, you will need to keep calling this +uint16_t si4063_refill_buffer(uint8_t *data, int len) +{ + uint8_t response[2] = {0, 0}; + + // Check how many bytes we have free + si4063_send_command(SI4063_COMMAND_FIFO_INFO, 0, NULL); + si4063_read_response(sizeof(response), response); + + uint8_t free_space = response[1]; + if(free_space < len) { + len = free_space; + } + + si4063_send_command(SI4063_COMMAND_WRITE_TX_FIFO, len, data); + + return len; +} + +// Wait for our buffer to be emptied, and for the si4063 to leave TX mode +// If timeout, we force it to sleep +int si4063_wait_for_tx_complete(int timeout_ms) +{ + for(int i = 0; i < timeout_ms; i++) { + uint8_t status = 0; + si4063_send_command(SI4063_COMMAND_REQUEST_DEVICE_STATE, 0, NULL); + si4063_read_response(1, &status); + + if(status == SI4063_STATE_SLEEP || + status == SI4063_STATE_READY || + status == SI4063_STATE_READY2 || + status == SI4063_STATE_SPI_ACTIVE) { + return HAL_OK; + } + + delay_ms(1); + } + + si4063_disable_tx(); + si4063_wait_for_cts(); + + return HAL_ERROR_TIMEOUT; +} + static int si4063_get_outdiv(const uint32_t frequency_hz) { // Select the output divider according to the recommended ranges in the Si406x datasheet - if (frequency_hz < 177000000UL) { + if (frequency_hz < 177000000UL) { return 24; } else if (frequency_hz < 239000000UL) { return 16; @@ -253,6 +339,18 @@ static int si4063_get_band(const uint32_t frequency_hz) return 0; } +// Also clears status +bool si4063_fifo_underflow() +{ + uint8_t data[] = {0xFF, 0xFF, ~0x20}; // Clear underflow status + si4063_send_command(SI4063_COMMAND_GET_INT_STATUS, sizeof(data), data); + uint8_t response[7]; + si4063_read_response(sizeof(response), response); + + bool fifo_underflow_pending = response[6] & 0x20; + return fifo_underflow_pending; +} + void si4063_set_tx_frequency(const uint32_t frequency_hz) { uint8_t outdiv, band; @@ -307,6 +405,26 @@ void si4063_set_tx_frequency(const uint32_t frequency_hz) si4063_set_frequency_deviation(current_deviation_hz); } +void si4063_set_data_rate(const uint32_t rate_bps) +{ + int rate = rate_bps * 10; + // Set TX_NCO_MODE to our crystal frequency, as recommended by the data sheet for rates <= 200kbps + // Set MODEM_DATA_RATE to rate_bps * 10 (will get downsampled because NCO_MODE defaults to 10x) + uint8_t data[] = { + 0x20, // Group + 0x07, // Set 7 properties + 0x03, // Start from MODEM_DATA_RATE + (rate >> 16) & 0xFF, + (rate >> 8) & 0xFF, + rate & 0xFF, + (SI4063_CLOCK >> 24) & 0xFF, + (SI4063_CLOCK >> 16) & 0xFF, + (SI4063_CLOCK >> 8) & 0xFF, + SI4063_CLOCK & 0xFF, + }; + si4063_send_command(SI4063_COMMAND_SET_PROPERTY, sizeof(data), data); +} + void si4063_set_tx_power(uint8_t power) { uint8_t data[] = { @@ -388,6 +506,10 @@ void si4063_set_modulation_type(si4063_modulation_type type) // Direct Async Mode with FSK modulation data[3] |= 0x02; break; + case SI4063_MODULATION_TYPE_FIFO_FSK: + // FIFO with FSK modulation + data[3] = 0x02; + break; default: return; } @@ -469,6 +591,7 @@ void si4063_configure() 0x03, // 0x03 = GLOBAL_CONFIG 0x40 | // 0x40 = Reserved, needs to be set to 1 0x20 | // 0x20 = Fast sequencer mode + 0x10 | // 129-byte FIFO 0x00 // High-performance mode }; @@ -535,6 +658,31 @@ void si4063_configure() si4063_send_command(SI4063_COMMAND_SET_PROPERTY, sizeof(data), data); } + + { + /* + 2. Detailed Errata Descriptions +2.1 If Configured to Skip Sync and Preamble on Transmit, the TX Data from the FIFO is Corrupted +Description of Errata +If preamble and sync word are excluded from the transmitted data (PREMABLE_TX_LENGTH = 0 and SYNC_CONFIG: SKIP_TX = 1), data from the FIFO is not transmitted correctly. +Affected Conditions / Impacts +Some number of missed bytes will occur at the beginning of the packet and some number of repeated bytes at the end of the packet. +Workaround +Set PKT_FIELD_1_CRC_CONFIG: CRC_START to 1. This will trigger the packet handler and result in transmitting the correct data, +while still not sending a CRC unless enabled in a FIELD configuration. A fix has been identified and will be included in a future release + */ + + // In other words, without this, the FIFO buffer gets corrupted while TXing, because we're not using + // the preamble/sync word stuff + // To be clear - we're not doing any CRC stuff! This is just the recommended workaround + uint8_t data[] = { + 0x12, // Group + 0x01, // Set 1 property + 0x10, // PKT_FIELD_1_CRC_CONFIG + 0x80, // CRC_START + }; + si4063_send_command(SI4063_COMMAND_SET_PROPERTY, sizeof(data), data); + } } // Not used yet, for future use diff --git a/src/drivers/si4063/si4063.h b/src/drivers/si4063/si4063.h index 1a09e2f..82087d3 100644 --- a/src/drivers/si4063/si4063.h +++ b/src/drivers/si4063/si4063.h @@ -8,12 +8,18 @@ typedef enum _si4063_modulation_type { SI4063_MODULATION_TYPE_CW = 0, SI4063_MODULATION_TYPE_OOK, SI4063_MODULATION_TYPE_FSK, + SI4063_MODULATION_TYPE_FIFO_FSK, } si4063_modulation_type; void si4063_enable_tx(); void si4063_inhibit_tx(); void si4063_disable_tx(); +uint16_t si4063_start_tx(uint8_t *data, int len); +uint16_t si4063_refill_buffer(uint8_t *data, int len); +int si4063_wait_for_tx_complete(int timeout_ms); +bool si4063_fifo_underflow(); void si4063_set_tx_frequency(uint32_t frequency_hz); +void si4063_set_data_rate(const uint32_t rate_bps); void si4063_set_tx_power(uint8_t power); void si4063_set_frequency_offset(uint16_t offset); void si4063_set_frequency_deviation(uint32_t deviation); diff --git a/src/gps.h b/src/gps.h index 7d6cc57..6e16c16 100644 --- a/src/gps.h +++ b/src/gps.h @@ -27,9 +27,9 @@ #define GPS_FIX_TIME_ONLY 5 #if GPS_REQUIRE_3D_FIX -#define GPS_HAS_FIX(gps_data) (gps_data.fix_ok && (gps_data.fix == GPS_FIX_3D)) +#define GPS_HAS_FIX(gps_data) ((gps_data).fix_ok && ((gps_data).fix == GPS_FIX_3D)) #else -#define GPS_HAS_FIX(gps_data) (gps_data.fix_ok && (gps_data.fix == GPS_FIX_2D || gps_data.fix == GPS_FIX_3D)) +#define GPS_HAS_FIX(gps_data) ((gps_data).fix_ok && ((gps_data).fix == GPS_FIX_2D || (gps_data).fix == GPS_FIX_3D)) #endif typedef struct _gps_data { diff --git a/src/hal/delay.h b/src/hal/delay.h index 5a3dc82..ecf04b5 100644 --- a/src/hal/delay.h +++ b/src/hal/delay.h @@ -1,6 +1,8 @@ #ifndef __DELAY_H #define __DELAY_H +#include + #ifdef __cplusplus extern "C" { #endif diff --git a/src/radio.c b/src/radio.c index 443f2cb..c848e1a 100644 --- a/src/radio.c +++ b/src/radio.c @@ -10,6 +10,7 @@ #include "codecs/morse/morse.h" #include "codecs/bell/bell.h" #include "codecs/mfsk/mfsk.h" +#include "codecs/raw/raw.h" #include "codecs/jtencode/jtencode.h" #include "drivers/ubxg6010/ubxg6010.h" #include "radio_internal.h" @@ -25,6 +26,7 @@ #include "radio_payload_aprs_weather.h" #include "radio_payload_horus_v1.h" #include "radio_payload_horus_v2.h" +#include "radio_payload_cats.h" #include "radio_payload_wspr.h" #include "radio_payload_jtencode.h" #include "radio_payload_fsq.h" @@ -297,6 +299,20 @@ radio_transmit_entry radio_transmit_schedule[] = { .fsk_encoder_api = &mfsk_fsk_encoder_api, }, #endif +#if RADIO_SI4063_TX_CATS + { + .enabled = RADIO_SI4063_TX_CATS, + .radio_type = RADIO_TYPE_SI4063, + .data_mode = RADIO_DATA_MODE_CATS, + .transmit_count = RADIO_SI4063_TX_CATS_COUNT, + .time_sync_seconds = CATS_TIME_SYNC_SECONDS, + .time_sync_seconds_offset = CATS_TIME_SYNC_OFFSET_SECONDS, + .frequency = RADIO_SI4063_TX_FREQUENCY_CATS, + .tx_power = RADIO_SI4063_TX_POWER, + .payload_encoder = &radio_cats_payload_encoder, + .fsk_encoder_api = &raw_fsk_encoder_api, + }, +#endif #endif #endif @@ -489,6 +505,7 @@ radio_module_state radio_shared_state = { .radio_dma_transfer_active = false, .radio_manual_transmit_active = false, .radio_interrupt_transmit_active = false, + .radio_fifo_transmit_active = false, .radio_current_fsk_tones = NULL, .radio_current_fsk_tone_count = 0, @@ -619,6 +636,12 @@ static bool radio_start_transmit(radio_transmit_entry *entry) &entry->fsk_encoder); entry->fsk_encoder_api->set_data(&entry->fsk_encoder, radio_current_payload_length, radio_current_payload); break; + case RADIO_DATA_MODE_CATS: + enable_gps_during_transmit = true; + + raw_encoder_new(&entry->fsk_encoder); + entry->fsk_encoder_api->set_data(&entry->fsk_encoder, radio_current_payload_length, radio_current_payload); + break; case RADIO_DATA_MODE_WSPR: case RADIO_DATA_MODE_FT8: case RADIO_DATA_MODE_JT65: @@ -685,12 +708,12 @@ static bool radio_start_transmit(radio_transmit_entry *entry) return false; } - log_info("TX start\n"); - if (leds_enabled) { set_red_led(true); } + log_info("TX start\n"); + radio_shared_state.radio_transmission_active = true; return true; @@ -737,6 +760,7 @@ static bool radio_stop_transmit(radio_transmit_entry *entry) radio_shared_state.radio_manual_transmit_active = false; radio_shared_state.radio_interrupt_transmit_active = false; + radio_shared_state.radio_fifo_transmit_active = false; radio_shared_state.radio_dma_transfer_active = false; switch (entry->data_mode) { @@ -753,6 +777,9 @@ static bool radio_stop_transmit(radio_transmit_entry *entry) case RADIO_DATA_MODE_HORUS_V2: mfsk_encoder_destroy(&entry->fsk_encoder); break; + case RADIO_DATA_MODE_CATS: + raw_encoder_destroy(&entry->fsk_encoder); + break; case RADIO_DATA_MODE_WSPR: case RADIO_DATA_MODE_FT8: case RADIO_DATA_MODE_JT65: @@ -954,7 +981,7 @@ void radio_handle_main_loop() log_info("Lat: %ld *1M, Lon: %ld *1M, Alt: %ld m\n", current_telemetry_data.gps.latitude_degrees_1000000 / 10, current_telemetry_data.gps.longitude_degrees_1000000 / 10, - (current_telemetry_data.gps.altitude_mm / 1000) * 3280 / 1000); + (current_telemetry_data.gps.altitude_mm / 1000)); #endif radio_reset_transmit_delay_counter(); @@ -988,7 +1015,9 @@ void radio_handle_main_loop() } } - if (radio_shared_state.radio_transmission_active && radio_shared_state.radio_transmit_next_symbol_flag) { + if (radio_shared_state.radio_transmission_active + && radio_shared_state.radio_transmit_next_symbol_flag + && !radio_shared_state.radio_fifo_transmit_active) { radio_shared_state.radio_transmit_next_symbol_flag = false; if (!radio_shared_state.radio_manual_transmit_active && !radio_shared_state.radio_interrupt_transmit_active) { @@ -1047,6 +1076,9 @@ void radio_init() case RADIO_DATA_MODE_HORUS_V2: // No messages break; + case RADIO_DATA_MODE_CATS: + entry->messages = cats_comment_templates; + break; case RADIO_DATA_MODE_FT8: case RADIO_DATA_MODE_JT65: case RADIO_DATA_MODE_JT9: diff --git a/src/radio_internal.h b/src/radio_internal.h index 8ee3cd5..25aca8f 100644 --- a/src/radio_internal.h +++ b/src/radio_internal.h @@ -29,6 +29,7 @@ typedef enum _radio_data_mode { RADIO_DATA_MODE_FSQ_3, RADIO_DATA_MODE_FSQ_4_5, RADIO_DATA_MODE_FSQ_6, + RADIO_DATA_MODE_CATS, } radio_data_mode; typedef struct _radio_transmit_entry { @@ -70,6 +71,7 @@ typedef struct _radio_module_state { volatile bool radio_dma_transfer_active; volatile bool radio_manual_transmit_active; volatile bool radio_interrupt_transmit_active; + volatile bool radio_fifo_transmit_active; fsk_tone *radio_current_fsk_tones; int8_t radio_current_fsk_tone_count; diff --git a/src/radio_payload_cats.c b/src/radio_payload_cats.c new file mode 100644 index 0000000..8603eb8 --- /dev/null +++ b/src/radio_payload_cats.c @@ -0,0 +1,51 @@ +#include +#include + +#include "config.h" +#include "telemetry.h" +#include "payload.h" +#include "log.h" +#include "codecs/cats/cats.h" +#include "codecs/cats/whisker.h" + +#define CATS_PREAMBLE_BYTE 0x55 +#define CATS_PREAMBLE_LENGTH 4 +#define CATS_SYNC_WORD 0xABCDEF12 +#define CATS_SYNC_WORD_LENGTH 4 + +uint16_t radio_cats_encode(uint8_t *payload, uint16_t length, telemetry_data *telemetry_data, char *message) +{ + uint8_t *cur = payload; + + // preamble + for(int i = 0; i < CATS_PREAMBLE_LENGTH; i++) { + *(cur++) = CATS_PREAMBLE_BYTE; + } + + // sync word + for(int i = CATS_SYNC_WORD_LENGTH - 1; i >= 0; i--) { + *(cur++) = (CATS_SYNC_WORD >> (i * 8)); + } + + uint8_t *data = malloc(length); + cats_packet packet = cats_create(data); + cats_append_identification_whisker(&packet, CATS_CALLSIGN, CATS_SSID, CATS_ICON); + cats_append_comment_whisker(&packet, message); + + if(GPS_HAS_FIX(telemetry_data->gps) && + (telemetry_data->gps.latitude_degrees_1000000 != 0 || + telemetry_data->gps.longitude_degrees_1000000 != 0)) { + cats_append_gps_whisker(&packet, telemetry_data->gps); + } + + cats_append_node_info_whisker(&packet, telemetry_data); + + int len = cats_fully_encode(packet, cur); + free(data); + + return (uint16_t)(CATS_PREAMBLE_LENGTH + CATS_SYNC_WORD_LENGTH + len); +} + +payload_encoder radio_cats_payload_encoder = { + .encode = radio_cats_encode, +}; diff --git a/src/radio_payload_cats.h b/src/radio_payload_cats.h new file mode 100644 index 0000000..fb88e66 --- /dev/null +++ b/src/radio_payload_cats.h @@ -0,0 +1,8 @@ +#ifndef __RADIO_PAYLOAD_CATS_H +#define __RADIO_PAYLOAD_CATS_H + +#include "payload.h" + +extern payload_encoder radio_cats_payload_encoder; + +#endif diff --git a/src/radio_si4063.c b/src/radio_si4063.c index 0e0fe1b..67183d0 100644 --- a/src/radio_si4063.c +++ b/src/radio_si4063.c @@ -14,6 +14,7 @@ #define SI4063_DEVIATION_HZ_RTTY 200.0 #define SI4063_DEVIATION_HZ_APRS 2600.0 +#define SI4063_DEVIATION_HZ_CATS 4800.0 #define CW_SYMBOL_RATE_MULTIPLIER 4 @@ -28,8 +29,10 @@ bool radio_start_transmit_si4063(radio_transmit_entry *entry, radio_module_state { uint16_t frequency_offset; uint32_t frequency_deviation = 0; + uint32_t data_rate = 0; si4063_modulation_type modulation_type; bool use_direct_mode; + bool use_fifo_mode = false; switch (entry->data_mode) { case RADIO_DATA_MODE_CW: @@ -62,6 +65,14 @@ bool radio_start_transmit_si4063(radio_transmit_entry *entry, radio_module_state data_timer_init(entry->fsk_encoder_api->get_symbol_rate(&entry->fsk_encoder)); break; } + case RADIO_DATA_MODE_CATS: + frequency_offset = 0; + frequency_deviation = SI4063_DEVIATION_HZ_CATS; + modulation_type = SI4063_MODULATION_TYPE_FIFO_FSK; + use_direct_mode = false; + use_fifo_mode = true; + data_rate = 9600; + break; default: return false; } @@ -72,7 +83,12 @@ bool radio_start_transmit_si4063(radio_transmit_entry *entry, radio_module_state si4063_set_modulation_type(modulation_type); si4063_set_frequency_deviation(frequency_deviation); - si4063_enable_tx(); + if (use_fifo_mode) { + si4063_set_data_rate(data_rate); + } + else { + si4063_enable_tx(); + } if (use_direct_mode) { spi_uninit(); @@ -96,6 +112,9 @@ bool radio_start_transmit_si4063(radio_transmit_entry *entry, radio_module_state system_disable_tick(); shared_state->radio_interrupt_transmit_active = true; break; + case RADIO_DATA_MODE_CATS: + shared_state->radio_fifo_transmit_active = true; + break; default: break; } @@ -170,6 +189,41 @@ static void radio_handle_main_loop_manual_si4063(radio_transmit_entry *entry, ra system_enable_tick(); } +void radio_handle_fifo_si4063(radio_transmit_entry *entry, radio_module_state *shared_state) { + log_debug("Start FIFO TX\n"); + fsk_encoder_api *fsk_encoder_api = entry->fsk_encoder_api; + fsk_encoder *fsk_enc = &entry->fsk_encoder; + + uint8_t *data = fsk_encoder_api->get_data(fsk_enc); + uint16_t len = fsk_encoder_api->get_data_len(fsk_enc); + + uint16_t written = si4063_start_tx(data, len); + data += written; + len -= written; + + while(len > 0) { + uint16_t written = si4063_refill_buffer(data, len); + data += written; + len -= written; + + if(si4063_fifo_underflow()) { + log_info("FIFO underflow - Aborting\n"); + shared_state->radio_transmission_finished = true; + + return; + } + } + + int err = si4063_wait_for_tx_complete(1000); + if(err != HAL_OK) { + log_info("Error waiting for tx complete: %d\n", err); + } + + log_debug("Finished FIFO TX\n"); + + shared_state->radio_transmission_finished = true; +} + void radio_handle_main_loop_si4063(radio_transmit_entry *entry, radio_module_state *shared_state) { if (entry->radio_type != RADIO_TYPE_SI4063 || shared_state->radio_interrupt_transmit_active) { @@ -181,6 +235,11 @@ void radio_handle_main_loop_si4063(radio_transmit_entry *entry, radio_module_sta return; } + if (shared_state->radio_fifo_transmit_active) { + radio_handle_fifo_si4063(entry, shared_state); + return; + } + if (radio_si4063_state_change) { radio_si4063_state_change = false; pwm_timer_set_frequency(radio_si4063_freq); @@ -266,6 +325,8 @@ bool radio_stop_transmit_si4063(radio_transmit_entry *entry, radio_module_state case RADIO_DATA_MODE_HORUS_V2: data_timer_uninit(); break; + case RADIO_DATA_MODE_CATS: + break; case RADIO_DATA_MODE_APRS_1200: use_direct_mode = true; break; @@ -293,6 +354,8 @@ bool radio_stop_transmit_si4063(radio_transmit_entry *entry, radio_module_state case RADIO_DATA_MODE_HORUS_V2: system_enable_tick(); break; + case RADIO_DATA_MODE_CATS: + break; default: break; }