diff --git a/Makefile b/Makefile index 73ec27b..8626c6b 100644 --- a/Makefile +++ b/Makefile @@ -9,15 +9,20 @@ COMMON_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(COMMON_SRC)) FFT_SRC = $(wildcard fft/*.c) FFT_OBJ = $(patsubst %.c,$(BUILD_DIR)/%.o,$(FFT_SRC)) -TARGETS = gen_ft8 decode_ft8 test_ft8 +TARGETS = libft8.a gen_ft8 decode_ft8 test_ft8 -CFLAGS = -fsanitize=address -O3 -ggdb3 -CPPFLAGS = -std=c11 -I. +ifdef FT8_DEBUG +CFLAGS = -fsanitize=address -ggdb3 -DHAVE_STPCPY -I. -DFTX_DEBUG_PRINT LDFLAGS = -fsanitize=address -lm +else +CFLAGS = -O3 -DHAVE_STPCPY -I. +LDFLAGS = -lm +endif # Optionally, use Portaudio for live audio input +# Portaudio is a C++ library, so then you need to set CC=clang++ or CC=g++ ifdef PORTAUDIO_PREFIX -CPPFLAGS += -DUSE_PORTAUDIO -I$(PORTAUDIO_PREFIX)/include +CFLAGS += -DUSE_PORTAUDIO -I$(PORTAUDIO_PREFIX)/include LDFLAGS += -lportaudio -L$(PORTAUDIO_PREFIX)/lib endif @@ -26,24 +31,28 @@ endif all: $(TARGETS) clean: - rm -rf $(BUILD_DIR) $(TARGETS) + rm -rf $(BUILD_DIR) $(TARGETS) run_tests: test_ft8 @./test_ft8 -install: - $(AR) rc libft8.a $(FT8_OBJ) $(COMMON_OBJ) +install: libft8.a install libft8.a /usr/lib/libft8.a -gen_ft8: $(BUILD_DIR)/demo/gen_ft8.o $(FT8_OBJ) $(COMMON_OBJ) $(FFT_OBJ) - $(CC) $(LDFLAGS) -o $@ $^ +gen_ft8: $(BUILD_DIR)/demo/gen_ft8.o libft8.a + $(CC) $(CFLAGS) -o $@ .build/demo/gen_ft8.o -lft8 -L. -lm -decode_ft8: $(BUILD_DIR)/demo/decode_ft8.o $(FT8_OBJ) $(COMMON_OBJ) $(FFT_OBJ) - $(CC) $(LDFLAGS) -o $@ $^ +decode_ft8: $(BUILD_DIR)/demo/decode_ft8.o libft8.a $(FFT_OBJ) + $(CC) $(CFLAGS) -o $@ $(BUILD_DIR)/demo/decode_ft8.o $(FFT_OBJ) -lft8 -L. -lm -test_ft8: $(BUILD_DIR)/test/test.o $(FT8_OBJ) - $(CC) $(LDFLAGS) -o $@ $^ +test_ft8: $(BUILD_DIR)/test/test.o libft8.a + $(CC) $(CFLAGS) -o $@ .build/test/test.o -lft8 -L. -lm $(BUILD_DIR)/%.o: %.c @mkdir -p $(dir $@) - $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $^ + $(CC) $(CFLAGS) -o $@ -c $^ + +lib: libft8.a + +libft8.a: $(FT8_OBJ) $(COMMON_OBJ) + $(AR) rc libft8.a $(FT8_OBJ) $(COMMON_OBJ) diff --git a/demo/decode_ft8.c b/demo/decode_ft8.c index 1f45967..dcb0299 100644 --- a/demo/decode_ft8.c +++ b/demo/decode_ft8.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 199309L #include #include #include @@ -212,7 +213,8 @@ void decode(const monitor_t* mon, struct tm* tm_slot_start) ++num_decoded; char text[FTX_MAX_MESSAGE_LENGTH]; - ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text); + ftx_message_offsets_t offsets; + ftx_message_rc_t unpack_status = ftx_message_decode(&message, &hash_if, text, &offsets); if (unpack_status != FTX_MESSAGE_RC_OK) { snprintf(text, sizeof(text), "Error [%d] while unpacking!", (int)unpack_status); diff --git a/ft8/message.c b/ft8/message.c index 21f5a49..5975636 100644 --- a/ft8/message.c +++ b/ft8/message.c @@ -12,7 +12,6 @@ ////////////////////////////////////////////////////// Static function prototypes ////////////////////////////////////////////////////////////// -static bool trim_brackets(char* result, const char* original, int length); static void add_brackets(char* result, const char* original, int length); /// Compute hash value for a callsign and save it in a hash table via the provided callsign hash interface. @@ -25,7 +24,8 @@ static void add_brackets(char* result, const char* original, int length); static bool save_callsign(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint32_t* n22_out, uint16_t* n12_out, uint16_t* n10_out); static bool lookup_callsign(const ftx_callsign_hash_interface_t* hash_if, ftx_callsign_hash_type_t hash_type, uint32_t hash, char* callsign); -static int32_t pack_basecall(const char* callsign, int length); +/// returns the numeric value if it matches "CQ nnn" or "CQ a[bcd]", otherwise -1 +static int parse_cq_modifier(const char* string); /// Pack a special token, a 22-bit hash code, or a valid base call into a 29-bit integer. static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* hash_if, uint8_t* ip); @@ -35,7 +35,7 @@ static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* /// @param[in] i3 Payload type (3 bits), 1 or 2 /// @param[in] hash_if Callsign hash table interface (can be NULL) /// @param[out] result Unpacked callsign (max size: 13 characters including the terminating \0) -static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result); +static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result, ftx_field_t* field_type); /// Pack a non-standard base call into a 28-bit integer. static bool pack58(const ftx_callsign_hash_interface_t* hash_if, const char* callsign, uint64_t* n58); @@ -44,7 +44,7 @@ static bool pack58(const ftx_callsign_hash_interface_t* hash_if, const char* cal static bool unpack58(uint64_t n58, const ftx_callsign_hash_interface_t* hash_if, char* callsign); static uint16_t packgrid(const char* grid4); -static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra); +static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra, ftx_field_t* extra_field_type); /////////////////////////////////////////////////////////// Exported functions ///////////////////////////////////////////////////////////////// @@ -120,35 +120,70 @@ ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interf char extra[20]; const char* parse_position = message_text; - parse_position = copy_token(call_to, 12, parse_position); - parse_position = copy_token(call_de, 12, parse_position); - parse_position = copy_token(extra, 20, parse_position); + const bool is_cq = starts_with(message_text, "CQ "); + if (is_cq) { + parse_position += 3; + memset(call_to, 0, sizeof(call_to)); - if (call_to[11] != '\0') + // copy the next token temporarily (for the debug message) + copy_token(call_de, 12, parse_position); + LOG(LOG_DEBUG, "next token after CQ: '%s' in '%s'\n", call_de, message_text); + + // see if the word after CQ matches the a[bcd] or nnn pattern, and append to call_to + int cq_modifier_v = parse_cq_modifier(message_text); + if (cq_modifier_v >= 0) { + // treat "CQ nnn" or "CQ a[bcd]" as a single token: + // copy the CQ and then the next token to call_to + memcpy(call_to, "CQ \0", 4); + parse_position = copy_token(call_to + 3, sizeof(call_to) - 3, parse_position); + LOG(LOG_DEBUG, "CQ modifier encoding %d; parse_pos after CQ: %s in %s\n", cq_modifier_v, parse_position, message_text); + } else { + memcpy(call_to, "CQ\0", 3); + // the next token should be call_de, which we will get below + } + } else { + // else it's not a CQ: expect first token to be the "to" callsign + parse_position = copy_token(call_to, sizeof(call_to), parse_position); + } + // now we are fairly sure the next word should be the "de" callsign + parse_position = copy_token(call_de, sizeof(call_de), parse_position); + // and the word after that may be a grid or signal report + parse_position = copy_token(extra, sizeof(extra), parse_position); + + LOG(LOG_DEBUG, "ftx_message_encode: parsed '%s' '%s' '%s'; remaining chars '%s'\n", call_to, call_de, extra, parse_position); + + if (call_to[sizeof(call_to) - 1] != '\0') { // token too long return FTX_MESSAGE_RC_ERROR_CALLSIGN1; } - if (call_de[11] != '\0') + if (call_de[sizeof(call_de) - 1] != '\0') { // token too long return FTX_MESSAGE_RC_ERROR_CALLSIGN2; } - if (extra[19] != '\0') + if (extra[sizeof(extra) - 1] != '\0') { // token too long return FTX_MESSAGE_RC_ERROR_GRID; } ftx_message_rc_t rc; - rc = ftx_message_encode_std(msg, hash_if, call_to, call_de, extra); + if (!parse_position[0]) { + // up to 3 tokens with no leftovers + rc = ftx_message_encode_std(msg, hash_if, call_to, call_de, extra); + if (rc == FTX_MESSAGE_RC_OK) + return rc; + LOG(LOG_DEBUG, " ftx_message_encode_std failed: %d\n", rc); + rc = ftx_message_encode_nonstd(msg, hash_if, call_to, call_de, extra); + if (rc == FTX_MESSAGE_RC_OK) + return rc; + LOG(LOG_DEBUG, " ftx_message_encode_nonstd failed: %d\n", rc); + } + rc = ftx_message_encode_free(msg, message_text); if (rc == FTX_MESSAGE_RC_OK) return rc; - rc = ftx_message_encode_nonstd(msg, hash_if, call_to, call_de, extra); - if (rc == FTX_MESSAGE_RC_OK) - return rc; - - // rc = ftx_message_encode_telemetry_hex(msg, hash_if, call_to, call_de, extra); + LOG(LOG_DEBUG, " ftx_message_encode_free failed: %d\n", rc); return rc; } @@ -157,9 +192,11 @@ ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_in { uint8_t ipa, ipb; + LOG(LOG_DEBUG, "ftx_message_encode_std '%s' '%s' '%s'\n", call_to, call_de, extra); + int32_t n28a = pack28(call_to, hash_if, &ipa); int32_t n28b = pack28(call_de, hash_if, &ipb); - LOG(LOG_DEBUG, "n29a = %d, n29b = %d\n", n28a, n28b); + LOG(LOG_DEBUG, " n29a = %d, n29b = %d\n", n28a, n28b); if (n28a < 0) return FTX_MESSAGE_RC_ERROR_CALLSIGN1; @@ -176,6 +213,13 @@ ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_in } } + char* slash_de = strchr(call_de, '/'); + uint8_t icq = (uint8_t)equals(call_to, "CQ") || starts_with(call_to, "CQ "); + if (slash_de && (slash_de - call_de >= 2) && icq && !(equals(slash_de, "/P") || equals(slash_de, "/R"))) + { + return FTX_MESSAGE_RC_ERROR_CALLSIGN2; // nonstandard call: need a type 4 message + } + uint16_t igrid4 = packgrid(extra); LOG(LOG_DEBUG, "igrid4 = %d\n", igrid4); @@ -211,19 +255,12 @@ ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash { uint8_t i3 = 4; - uint8_t icq = (uint8_t)equals(call_to, "CQ"); + LOG(LOG_DEBUG, "ftx_message_encode_nonstd '%s' '%s' '%s'\n", call_to, call_de, extra); + + uint8_t icq = (uint8_t)equals(call_to, "CQ") || starts_with(call_to, "CQ "); int len_call_to = strlen(call_to); int len_call_de = strlen(call_de); - // if ((icq != 0) || (pack_basecall(call_to, len_call_to) >= 0)) - // { - // if (pack_basecall(call_de, len_call_de) >= 0) - // { - // // no need for encode_nonstd, should use encode_std - // return FTX_MESSAGE_RC_ERROR_CALLSIGN2; - // } - // } - if ((icq == 0) && ((len_call_to < 3))) return FTX_MESSAGE_RC_ERROR_CALLSIGN1; if ((len_call_de < 3)) @@ -258,6 +295,7 @@ ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash iflip = 0; n12 = 0; call58 = call_de; + LOG(LOG_DEBUG, "CQ: 58-bit '%s'; omitting grid '%s'\n", call58, extra); } if (!pack58(hash_if, call58, &n58)) @@ -293,7 +331,62 @@ ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash return FTX_MESSAGE_RC_OK; } -ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message) +ftx_message_rc_t ftx_message_encode_free(ftx_message_t* msg, const char* text) +{ + uint8_t str_len = strlen(text); + if (str_len > 13) + { + // Too long text + return FTX_MESSAGE_RC_ERROR_TYPE; + } + + uint8_t b71[9]; + memset(b71, 0, 9); + int8_t cid; + char c; + + for (int idx = 0; idx < 13; idx++) + { + if (idx < str_len) + { + c = text[idx]; + } + else + { + c = ' '; + } + cid = nchar(c, FT8_CHAR_TABLE_FULL); + if (cid == -1) + { + return FTX_MESSAGE_RC_ERROR_TYPE; + } + uint16_t rem = cid; + for (int i = 8; i >= 0; i--) + { + rem += b71[i] * 42; + b71[i] = rem & 0xff; + rem = rem >> 8; + } + } + ftx_message_rc_t ret = ftx_message_encode_telemetry(msg, b71); + msg->payload[9] = 0; // i3.n3 = 0.0; etc. + return ret; +} + +// TODO set byte 9? or must the caller do it? +ftx_message_rc_t ftx_message_encode_telemetry(ftx_message_t* msg, const uint8_t* telemetry) +{ + // Shift bits in telemetry left by 1 bit to right-align the data + uint8_t carry = 0; + for (int i = 8; i >= 0; --i) + { + msg->payload[i] = (telemetry[i] << 1) | (carry >> 7); + carry = telemetry[i] & 0x80; + } + return FTX_MESSAGE_RC_OK; +} + +ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message, ftx_message_offsets_t* offsets) { ftx_message_rc_t rc; @@ -303,15 +396,20 @@ ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_ char* field3 = buf + 14 + 14; message[0] = '\0'; + for (int i = 0; i < FTX_MAX_MESSAGE_FIELDS; ++i) + { + offsets->types[i] = FTX_FIELD_UNKNOWN; + offsets->offsets[i] = -1; + } ftx_message_type_t msg_type = ftx_message_get_type(msg); switch (msg_type) { case FTX_MESSAGE_TYPE_STANDARD: - rc = ftx_message_decode_std(msg, hash_if, field1, field2, field3); + rc = ftx_message_decode_std(msg, hash_if, field1, field2, field3, offsets->types); break; case FTX_MESSAGE_TYPE_NONSTD_CALL: - rc = ftx_message_decode_nonstd(msg, hash_if, field1, field2, field3); + rc = ftx_message_decode_nonstd(msg, hash_if, field1, field2, field3, offsets->types); break; case FTX_MESSAGE_TYPE_FREE_TEXT: ftx_message_decode_free(msg, field1); @@ -335,14 +433,18 @@ ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_ if (field1 != NULL) { // TODO join fields via whitespace + const char* message_start = message; message = append_string(message, field1); + offsets->offsets[0] = 0; if (field2 != NULL) { message = append_string(message, " "); + offsets->offsets[1] = message - message_start; message = append_string(message, field2); - if (field3 != NULL) + if ((field3 != NULL) && (field3[0] != 0)) { message = append_string(message, " "); + offsets->offsets[2] = message - message_start; message = append_string(message, field3); } } @@ -351,7 +453,8 @@ ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_ return rc; } -ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra) +ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, + char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]) { uint32_t n29a, n29b; uint16_t igrid4; @@ -379,15 +482,15 @@ ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_h call_to[0] = call_de[0] = extra[0] = '\0'; // Unpack both callsigns - if (unpack28(n29a >> 1, n29a & 1u, i3, hash_if, call_to) < 0) + if (unpack28(n29a >> 1, n29a & 1u, i3, hash_if, call_to, &field_types[0]) < 0) { return FTX_MESSAGE_RC_ERROR_CALLSIGN1; } - if (unpack28(n29b >> 1, n29b & 1u, i3, hash_if, call_de) < 0) + if (unpack28(n29b >> 1, n29b & 1u, i3, hash_if, call_de, &field_types[1]) < 0) { return FTX_MESSAGE_RC_ERROR_CALLSIGN2; } - if (unpackgrid(igrid4, ir, extra) < 0) + if (unpackgrid(igrid4, ir, extra, &field_types[2]) < 0) { return FTX_MESSAGE_RC_ERROR_GRID; } @@ -397,7 +500,8 @@ ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_h } // non-standard messages, code originally by KD8CEC -ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra) +ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, + char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]) { uint16_t n12, iflip, nrpt, icq; uint64_t n58; @@ -437,22 +541,37 @@ ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsig if (icq == 0) { strcpy(call_to, call_1); + field_types[0] = FTX_FIELD_CALL; if (nrpt == 1) + { strcpy(extra, "RRR"); + field_types[2] = FTX_FIELD_TOKEN; + } else if (nrpt == 2) + { strcpy(extra, "RR73"); + field_types[2] = FTX_FIELD_TOKEN; + } else if (nrpt == 3) + { strcpy(extra, "73"); + field_types[2] = FTX_FIELD_TOKEN; + } else + { extra[0] = '\0'; + field_types[2] = FTX_FIELD_NONE; + } } else { strcpy(call_to, "CQ"); extra[0] = '\0'; + field_types[0] = FTX_FIELD_TOKEN; + field_types[2] = FTX_FIELD_NONE; } strcpy(call_de, call_2); - + field_types[1] = FTX_FIELD_CALL; LOG(LOG_INFO, "Decoded non-standard (type %d) message [%s] [%s] [%s]\n", i3, call_to, call_de, extra); return FTX_MESSAGE_RC_OK; } @@ -518,11 +637,11 @@ void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry) void ftx_message_print(ftx_message_t* msg) { printf("["); - for (int i = 0; i < PAYLOAD_LENGTH_BYTES; ++i) + for (int i = 0; i < FTX_PAYLOAD_LENGTH_BYTES; ++i) { if (i > 0) printf(" "); - printf("%02x", msg->payload[i])); + printf("%02x", msg->payload[i]); } printf("]"); } @@ -530,22 +649,6 @@ void ftx_message_print(ftx_message_t* msg) /////////////////////////////////////////////////////////// Static functions ///////////////////////////////////////////////////////////////// -static bool trim_brackets(char* result, const char* original, int length) -{ - if (original[0] == '<' && original[length - 1] == '>') - { - memcpy(result, original + 1, length - 2); - result[length - 2] = '\0'; - return true; - } - else - { - memcpy(result, original, length); - result[length] = '\0'; - return false; - } -} - static void add_brackets(char* result, const char* original, int length) { result[0] = '<'; @@ -613,7 +716,7 @@ static bool lookup_callsign(const ftx_callsign_hash_interface_t* hash_if, ftx_ca return found; } -static int32_t pack_basecall(const char* callsign, int length) +int32_t pack_basecall(const char* callsign, int length) { if (length > 2) { @@ -672,6 +775,38 @@ static int32_t pack_basecall(const char* callsign, int length) return -1; } +// returns the numeric value if it matches CQ_nnn or CQ_a[bcd], otherwise -1 +static int parse_cq_modifier(const char* string) +{ + int nnum = 0, nlet = 0; + + // encode CQ_nnn or CQ_a[bcd] + int m = 0; + for (int i = 3; i < 8; ++i) { + if (!string[i] || is_space(string[i])) + break; + else if (is_digit(string[i])) + ++nnum; + else if (is_letter(string[i])) { + ++nlet; + m = 27 * m + (string[i] - 'A' + 1); + } else { + // non-digit non-letter characters (such as '/') are not allowed + return -1; + } + } + LOG(LOG_DEBUG, "CQ_nnn/CQ_a[bcd] '%s' %d/%d\n", string, nnum, nlet); + if (nnum == 3 && nlet == 0) { + LOG(LOG_DEBUG, "CQ_nnn detected: %d\n", atoi(string + 3)); + return atoi(string + 3); + } + else if (nnum == 0 && nlet <= 4) { + LOG(LOG_DEBUG, "CQ_a[bcd] detected: m %d\n", m); + return 1000 + m; + } + return -1; // not a special CQ +} + static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* hash_if, uint8_t* ip) { LOG(LOG_DEBUG, "pack28() callsign [%s]\n", callsign); @@ -688,13 +823,14 @@ static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* int length = strlen(callsign); LOG(LOG_DEBUG, "Callsign length = %d\n", length); - if (starts_with(callsign, "CQ_") && length < 8) + if (starts_with(callsign, "CQ ") && length < 8) { - int nnum = 0, nlet = 0; - - // TODO: decode CQ_nnn or CQ_abcd - LOG(LOG_WARN, "CQ_nnn/CQ_abcd detected, not implemented\n"); - return -1; + int v = parse_cq_modifier(callsign); + if (v < 0) { + LOG(LOG_WARN, "CQ_nnn/CQ_a[bcd] '%s' not allowed\n", callsign); + return -1; + } + return 3 + v; } // Detect /R and /P suffix for basecall check @@ -718,10 +854,12 @@ static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* if ((length >= 3) && (length <= 11)) { // Treat this as a nonstandard callsign: compute its 22-bit hash - LOG(LOG_DEBUG, "Encoding as non-standard callsign\n"); + LOG(LOG_DEBUG, "Encoding '%s' as non-standard callsign\n", callsign); uint32_t n22; - if (!save_callsign(hash_if, callsign, &n22, NULL, NULL)) + if (!save_callsign(hash_if, callsign, &n22, NULL, NULL)) { + LOG(LOG_DEBUG, " Failed to save '%s'\n", callsign); return -1; // Error (some problem with callsign contents) + } *ip = 0; return NTOKENS + n22; // 22-bit hashed callsign } @@ -729,20 +867,29 @@ static int32_t pack28(const char* callsign, const ftx_callsign_hash_interface_t* return -1; // Error } -static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result) +static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_hash_interface_t* hash_if, char* result, ftx_field_t* field_type) { LOG(LOG_DEBUG, "unpack28() n28=%d i3=%d\n", n28, i3); - // Check for special tokens DE, QRZ, CQ, CQ_nnn, CQ_aaaa + // Check for special tokens DE, QRZ, CQ, CQ nnn, CQ a[bcd] if (n28 < NTOKENS) { if (n28 <= 2u) { if (n28 == 0) + { strcpy(result, "DE"); + *field_type = FTX_FIELD_TOKEN; + } else if (n28 == 1) + { strcpy(result, "QRZ"); - else /* if (n28 == 2) */ + *field_type = FTX_FIELD_TOKEN; + } + else + { /* if (n28 == 2) */ strcpy(result, "CQ"); + *field_type = FTX_FIELD_TOKEN; + } return 0; // Success } if (n28 <= 1002u) @@ -750,6 +897,7 @@ static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_has // CQ nnn with 3 digits strcpy(result, "CQ "); int_to_dd(result + 3, n28 - 3, 3, false); + *field_type = FTX_FIELD_TOKEN_WITH_ARG; return 0; // Success } if (n28 <= 532443ul) @@ -768,7 +916,8 @@ static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_has } strcpy(result, "CQ "); - strcat(result, trim_front(aaaa)); + strcat(result, trim_front(aaaa, ' ')); + *field_type = FTX_FIELD_TOKEN_WITH_ARG; return 0; // Success } // unspecified @@ -780,6 +929,7 @@ static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_has { // This is a 22-bit hash of a result lookup_callsign(hash_if, FTX_CALLSIGN_HASH_22_BITS, n28, result); + *field_type = FTX_FIELD_CALL; return 0; // Success } @@ -837,6 +987,7 @@ static int unpack28(uint32_t n28, uint8_t ip, uint8_t i3, const ftx_callsign_has // Save the result to hash table save_callsign(hash_if, result, NULL, NULL, NULL); + *field_type = FTX_FIELD_CALL; return 0; // Success } @@ -940,7 +1091,7 @@ static uint16_t packgrid(const char* grid4) return MAXGRID4 + 1; } -static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra) +static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra, ftx_field_t* extra_field_type) { char* dst = extra; @@ -962,7 +1113,8 @@ static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra) dst[1] = 'A' + (n % 18); // A..R n /= 18; dst[0] = 'A' + (n % 18); // A..R - // if (ir > 0 && strncmp(call_to, "CQ", 2) == 0) return -1; + // if (ir > 0 && strncmp(call_to, "CQ", 2) == 0) return -1; + *extra_field_type = FTX_FIELD_GRID; } else { @@ -970,22 +1122,31 @@ static int unpackgrid(uint16_t igrid4, uint8_t ir, char* extra) int irpt = igrid4 - MAXGRID4; // Check special cases first (irpt > 0 always) - if (irpt == 1) - dst[0] = '\0'; - else if (irpt == 2) - strcpy(dst, "RRR"); - else if (irpt == 3) - strcpy(dst, "RR73"); - else if (irpt == 4) - strcpy(dst, "73"); - else + switch (irpt) { + case 1: + dst[0] = '\0'; + *extra_field_type = FTX_FIELD_NONE; + break; + case 2: + strcpy(dst, "RRR"); + *extra_field_type = FTX_FIELD_TOKEN; + break; + case 3: + strcpy(dst, "RR73"); + *extra_field_type = FTX_FIELD_TOKEN; + break; + case 4: + strcpy(dst, "73"); + *extra_field_type = FTX_FIELD_TOKEN; + break; + default: // Extract signal report as a two digit number with a + or - sign if (ir > 0) - { *dst++ = 'R'; // Add "R" before report - } int_to_dd(dst, irpt - 35, 2, true); + *extra_field_type = FTX_FIELD_RST; + break; } // if (irpt >= 2 && strncmp(call_to, "CQ", 2) == 0) return -1; } diff --git a/ft8/message.h b/ft8/message.h index 2399295..a88a9c8 100644 --- a/ft8/message.h +++ b/ft8/message.h @@ -11,6 +11,7 @@ extern "C" #define FTX_PAYLOAD_LENGTH_BYTES 10 ///< number of bytes to hold 77 bits of FTx payload data #define FTX_MAX_MESSAGE_LENGTH 35 ///< max message length = callsign[13] + space + callsign[13] + space + report[6] + terminator +#define FTX_MAX_MESSAGE_FIELDS 3 // may need to get longer for multi-part messages (DXpedition, contest etc.) /// Structure that holds the decoded message typedef struct @@ -78,6 +79,28 @@ typedef enum FTX_MESSAGE_RC_ERROR_TYPE } ftx_message_rc_t; +typedef enum +{ + FTX_FIELD_UNKNOWN, + FTX_FIELD_NONE, + FTX_FIELD_TOKEN, // RRR, RR73, 73, DE, QRZ, CQ, ... + FTX_FIELD_TOKEN_WITH_ARG, // CQ nnn, CQ abcd + FTX_FIELD_CALL, + FTX_FIELD_GRID, + FTX_FIELD_RST +} ftx_field_t; + +typedef struct +{ + // parallel arrays: + // e.g. "CQ POTA W9XYZ AB12" generates + // types { FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_CALL, FTX_FIELD_CALL_GRID" } + // offsets { 0, 8, 14 } + // Both arrays end where offsets[i] < 0 + ftx_field_t types[FTX_MAX_MESSAGE_FIELDS]; + int16_t offsets[FTX_MAX_MESSAGE_FIELDS]; +} ftx_message_offsets_t; + // Callsign types and sizes: // * Std. call (basecall) - 1-2 letter/digit prefix (at least one letter), 1 digit area code, 1-3 letter suffix, // total 3-6 chars (exception: 7 character calls with prefixes 3DA0- and 3XA..3XZ-) @@ -93,7 +116,17 @@ ftx_message_type_t ftx_message_get_type(const ftx_message_t* msg); // bool ftx_message_check_recipient(const ftx_message_t* msg, const char* callsign); -/// Pack (encode) a text message +/// Pack (encode) a callsign in the standard way, and return the numeric representation. +/// Returns -1 if \a callsign cannot be encoded in the standard way. +/// This function can be used to decide whether to call ftx_message_encode_std() or ftx_message_decode_nonstd(). +/// Alternatively, ftx_message_encode_std() itself fails when one of the callsigns cannot be packed this way. +int32_t pack_basecall(const char* callsign, int length); + +/// Pack (encode) a text message, guessing which message type to use and falling back on failure: +/// if there are 3 or fewer tokens, try ftx_message_encode_std first, +/// then ftx_message_encode_nonstd if that fails because of a non-standard callsign; +/// otherwise fall back to ftx_message_encode_free. +/// If you already know which type to use, you can call one of those functions directly. ftx_message_rc_t ftx_message_encode(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* message_text); /// Pack Type 1 (Standard 77-bit message) or Type 2 (ditto, with a "/P" call) message @@ -105,13 +138,13 @@ ftx_message_rc_t ftx_message_encode_std(ftx_message_t* msg, ftx_callsign_hash_in /// Pack Type 4 (One nonstandard call and one hashed call) message ftx_message_rc_t ftx_message_encode_nonstd(ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, const char* call_to, const char* call_de, const char* extra); -void ftx_message_encode_free(const char* text); -void ftx_message_encode_telemetry_hex(const char* telemetry_hex); -void ftx_message_encode_telemetry(const uint8_t* telemetry); +/// Pack plain text, up to 13 characters +ftx_message_rc_t ftx_message_encode_free(ftx_message_t* msg, const char* text); +ftx_message_rc_t ftx_message_encode_telemetry(ftx_message_t* msg, const uint8_t* telemetry); -ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message); -ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra); -ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra); +ftx_message_rc_t ftx_message_decode(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* message, ftx_message_offsets_t* offsets); +ftx_message_rc_t ftx_message_decode_std(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]); +ftx_message_rc_t ftx_message_decode_nonstd(const ftx_message_t* msg, ftx_callsign_hash_interface_t* hash_if, char* call_to, char* call_de, char* extra, ftx_field_t field_types[FTX_MAX_MESSAGE_FIELDS]); void ftx_message_decode_free(const ftx_message_t* msg, char* text); void ftx_message_decode_telemetry_hex(const ftx_message_t* msg, char* telemetry_hex); void ftx_message_decode_telemetry(const ftx_message_t* msg, uint8_t* telemetry); diff --git a/ft8/text.c b/ft8/text.c index ff2d20e..e8b6101 100644 --- a/ft8/text.c +++ b/ft8/text.c @@ -2,21 +2,21 @@ #include -const char* trim_front(const char* str) +const char* trim_front(const char* str, char to_trim) { - // Skip leading whitespace - while (*str == ' ') + // Skip leading to_trim characters + while (*str == to_trim) { str++; } return str; } -void trim_back(char* str) +void trim_back(char* str, char to_trim) { - // Skip trailing whitespace by replacing it with '\0' characters + // Skip trailing to_trim characters by replacing them with '\0' characters int idx = strlen(str) - 1; - while (idx >= 0 && str[idx] == ' ') + while (idx >= 0 && str[idx] == to_trim) { str[idx--] = '\0'; } @@ -24,15 +24,23 @@ void trim_back(char* str) char* trim(char* str) { - str = (char*)trim_front(str); - trim_back(str); + str = (char*)trim_front(str, ' '); + trim_back(str, ' '); + // return a pointer to the first non-whitespace character + return str; +} + +char* trim_brackets(char* str) +{ + str = (char*)trim_front(str, '<'); + trim_back(str, '>'); // return a pointer to the first non-whitespace character return str; } void trim_copy(char* trimmed, const char* str) { - str = (char*)trim_front(str); + str = (char*)trim_front(str, ' '); int len = strlen(str) - 1; while (len >= 0 && str[len] == ' ') { @@ -107,6 +115,7 @@ void fmtmsg(char* msg_out, const char* msg_in) *msg_out = 0; // Add zero termination } +// Returns pointer to the null terminator within the given string (destination) char* append_string(char* string, const char* token) { while (*token != '\0') @@ -124,7 +133,7 @@ const char* copy_token(char* token, int length, const char* string) // Copy characters until a whitespace character or the end of string while (*string != ' ' && *string != '\0') { - if (length > 0) + if (length > 1) { *token = *string; token++; diff --git a/ft8/text.h b/ft8/text.h index e4c28d8..d395d69 100644 --- a/ft8/text.h +++ b/ft8/text.h @@ -11,8 +11,8 @@ extern "C" // Utility functions for characters and strings -const char* trim_front(const char* str); -void trim_back(char* str); +const char* trim_front(const char* str, char to_trim); +void trim_back(char* str, char to_trim); /// In-place whitespace trim from front and back: /// 1) trims the back by changing whitespaces to '\0' @@ -23,6 +23,12 @@ char* trim(char* str); /// Trim whitespace from start and end of string void trim_copy(char* trimmed, const char* str); +/// In-place trim of <> characters from front and back: +/// 1) trims the back by changing > to '\0' +/// 2) trims the front by skipping < +/// @return trimmed string (pointer to first non-whitespace character) +char* trim_brackets(char* str); + char to_upper(char c); bool is_digit(char c); bool is_letter(char c); diff --git a/test/test.c b/test/test.c index 4feffef..b94cda0 100644 --- a/test/test.c +++ b/test/test.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include #include #include @@ -101,11 +102,19 @@ void test3() { } */ -#define CHECK(condition) \ - if (!(condition)) \ - { \ - printf("FAIL: Condition \'" #condition "' failed!\n"); \ - return; \ +#define CHECK(condition) \ + if (!(condition)) \ + { \ + printf("FAIL! Condition \'" #condition "' failed\n\n"); \ + return; \ + } + +#define CHECK_EQ_VAL(this, that) \ + if ((this) != (that)) \ + { \ + printf("FAIL! Expected " #this " (%d) == " #that " (%d)\n\n", \ + (this), (that)); \ + return; \ } #define TEST_END printf("Test OK\n\n") @@ -172,57 +181,54 @@ ftx_callsign_hash_interface_t hash_if = { .save_hash = hashtable_add }; -void test_std_msg(const char* call_to_tx, const char* call_de_tx, const char* extra_tx) +void test_std_msg(const char* call_to_tx, ftx_field_t to_field, const char* call_de_tx, ftx_field_t de_field, const char* extra_tx, ftx_field_t extra_field) { ftx_message_t msg; ftx_message_init(&msg); ftx_message_rc_t rc_encode = ftx_message_encode_std(&msg, &hash_if, call_to_tx, call_de_tx, extra_tx); - CHECK(rc_encode == FTX_MESSAGE_RC_OK); printf("Encoded [%s] [%s] [%s]\n", call_to_tx, call_de_tx, extra_tx); + CHECK_EQ_VAL(rc_encode, FTX_MESSAGE_RC_OK); - char call_to[14]; - char call_de[14]; + char call_to_arr[14]; + char call_de_arr[14]; char extra[14]; - ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, &hash_if, call_to, call_de, extra); - CHECK(rc_decode == FTX_MESSAGE_RC_OK); + char *call_to = call_to_arr; + char *call_de = call_de_arr; + ftx_field_t types[FTX_MAX_MESSAGE_FIELDS]; + ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, &hash_if, call_to, call_de, extra, types); + CHECK_EQ_VAL(rc_decode, FTX_MESSAGE_RC_OK); printf("Decoded [%s] [%s] [%s]\n", call_to, call_de, extra); - CHECK(0 == strcmp(call_to, call_to_tx)); - CHECK(0 == strcmp(call_de, call_de_tx)); - CHECK(0 == strcmp(extra, extra_tx)); - // CHECK(1 == 2); + call_to = trim_brackets(call_to); + call_de = trim_brackets(call_de); + CHECK_EQ_VAL(0, strcmp(call_to, call_to_tx)); + CHECK_EQ_VAL(0, strcmp(call_de, call_de_tx)); + CHECK_EQ_VAL(0, strcmp(extra, extra_tx)); + CHECK_EQ_VAL(to_field, types[0]); + CHECK_EQ_VAL(de_field, types[1]); + CHECK_EQ_VAL(extra_field, types[2]); TEST_END; } -void test_msg(const char* call_to_tx, const char* call_de_tx, const char* extra_tx) +void test_msg(const char* message_text, ftx_message_type_t expected_type, const char* expected, ftx_callsign_hash_interface_t* hash_if) { - char message_text[12 + 12 + 20]; - char* copy_ptr = message_text; - copy_ptr = stpcpy(copy_ptr, call_to_tx); - copy_ptr = stpcpy(copy_ptr, " "); - copy_ptr = stpcpy(copy_ptr, call_de_tx); - if (strlen(extra_tx) > 0) - { - copy_ptr = stpcpy(copy_ptr, " "); - copy_ptr = stpcpy(copy_ptr, extra_tx); - } printf("Testing [%s]\n", message_text); ftx_message_t msg; ftx_message_init(&msg); - ftx_message_rc_t rc_encode = ftx_message_encode(&msg, NULL, message_text); - CHECK(rc_encode == FTX_MESSAGE_RC_OK); + ftx_message_rc_t rc_encode = ftx_message_encode(&msg, hash_if, message_text); + CHECK_EQ_VAL(rc_encode, FTX_MESSAGE_RC_OK); + CHECK_EQ_VAL(expected_type, ftx_message_get_type(&msg)); - char call_to[14]; - char call_de[14]; - char extra[14]; - ftx_message_rc_t rc_decode = ftx_message_decode_std(&msg, NULL, call_to, call_de, extra); - CHECK(rc_decode == FTX_MESSAGE_RC_OK); - CHECK(0 == strcmp(call_to, call_to_tx)); - CHECK(0 == strcmp(call_de, call_de_tx)); - CHECK(0 == strcmp(extra, extra_tx)); - // CHECK(1 == 2); + char message_decoded[12 + 12 + 20]; + ftx_message_offsets_t offsets; + ftx_message_rc_t rc_decode = ftx_message_decode(&msg, hash_if, message_decoded, &offsets); + CHECK_EQ_VAL(rc_decode, FTX_MESSAGE_RC_OK); + printf("Decoded [%s]; offsets %d:%d %d:%d %d:%d\n", message_decoded, + offsets.offsets[0], offsets.types[0], offsets.offsets[1], offsets.types[1], offsets.offsets[2], offsets.types[2]); + CHECK_EQ_VAL(0, strcmp(expected, message_decoded)); + // TODO check offsets TEST_END; } @@ -232,9 +238,11 @@ int main() { // test1(); // test4(); - const char* callsigns[] = { "YL3JG", "W1A", "W1A/R", "W5AB", "W8ABC", "DE6ABC", "DE6ABC/R", "DE7AB", "DE9A", "3DA0X", "3DA0XYZ", "3DA0XYZ/R", "3XZ0AB", "3XZ0A" }; - const char* tokens[] = { "CQ", "QRZ" }; + const char* callsigns[] = { "YL3JG", "W1A", "W1A/R", "W5AB", "W8ABC", "DE6ABC", "DE6ABC/R", "DE7AB", "DE9A", "3DA0X", "3DA0XYZ", "3DA0XYZ/R", "3XZ0AB", "3XZ0A", "CQ1CQ" }; + const char* tokens[] = { "CQ", "QRZ", "CQ 123", "CQ 000", "CQ POTA", "CQ SA", "CQ O", "CQ ASD" }; + const ftx_field_t token_types[] = { FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG, FTX_FIELD_TOKEN_WITH_ARG }; const char* grids[] = { "KO26", "RR99", "AA00", "RR09", "AA01", "RRR", "RR73", "73", "R+10", "R+05", "R-12", "R-02", "+10", "+05", "-02", "-02", "" }; + const ftx_field_t grid_types[] = { FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_GRID, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_TOKEN, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_RST, FTX_FIELD_NONE }; for (int idx_grid = 0; idx_grid < SIZEOF_ARRAY(grids); ++idx_grid) { @@ -242,19 +250,37 @@ int main() { for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2) { - test_std_msg(callsigns[idx_callsign], callsigns[idx_callsign2], grids[idx_grid]); + test_std_msg(callsigns[idx_callsign], FTX_FIELD_CALL, callsigns[idx_callsign2], FTX_FIELD_CALL, grids[idx_grid], grid_types[idx_grid]); } } for (int idx_token = 0; idx_token < SIZEOF_ARRAY(tokens); ++idx_token) { for (int idx_callsign2 = 0; idx_callsign2 < SIZEOF_ARRAY(callsigns); ++idx_callsign2) { - test_std_msg(tokens[idx_token], callsigns[idx_callsign2], grids[idx_grid]); + test_std_msg(tokens[idx_token], token_types[idx_token], callsigns[idx_callsign2], FTX_FIELD_CALL, grids[idx_grid], grid_types[idx_grid]); } } } - - // test_std_msg("YOMAMA", "MYMAMA/QRP", "73"); + test_msg("CQ K7IHZ DM43", FTX_MESSAGE_TYPE_STANDARD, + "CQ K7IHZ DM43", &hash_if); + test_msg("CQ EA8/G5LSI", FTX_MESSAGE_TYPE_NONSTD_CALL, + "CQ EA8/G5LSI", &hash_if); + test_msg("EA8/G5LSI R2RFE RR73", FTX_MESSAGE_TYPE_STANDARD, + " R2RFE RR73", &hash_if); + test_msg("R2RFE/P EA8/G5LSI R+12", FTX_MESSAGE_TYPE_STANDARD, + "R2RFE/P R+12", &hash_if); + test_msg("TNX BOB 73 GL", FTX_MESSAGE_TYPE_FREE_TEXT, + "TNX BOB 73 GL", &hash_if); // message with 4 tokens must be free text + test_msg("TNX BOB 73", FTX_MESSAGE_TYPE_STANDARD, + " 73", &hash_if); // can't distinguish special callsigns from other tokens + test_msg("CQ YL/LB2JK KO16sw", FTX_MESSAGE_TYPE_NONSTD_CALL, + "CQ YL/LB2JK", &hash_if); // grid not allowed with nonstandard call + test_msg("CQ POTA YL/LB2JK KO16sw", FTX_MESSAGE_TYPE_NONSTD_CALL, + "CQ YL/LB2JK", &hash_if); // CQ modifier not allowed with nonstandard call + test_msg("CQ JA LB2JK JO59", FTX_MESSAGE_TYPE_STANDARD, + "CQ JA LB2JK JO59", &hash_if); + test_msg("CQ 123 LB2JK JO59", FTX_MESSAGE_TYPE_STANDARD, + "CQ 123 LB2JK JO59", &hash_if); return 0; }