diff --git a/src/flex_decoder_biw.c b/src/flex_decoder_biw.c index ffc2c37..ca001f4 100644 --- a/src/flex_decoder_biw.c +++ b/src/flex_decoder_biw.c @@ -26,7 +26,7 @@ bool process_biw(uint32_t biw_raw, flex_frame_t* frame) #ifdef DEBUG_BIW debug_printf("Raw BIW: 0x%x, reversed: 0x%x\n", biw_raw, reverse_bits_32(biw_raw)); #endif - flex_val_result_t result = validateWord(&biw_raw, (TASK_VALIDATE_CHECKSUM | TASK_REPAIR_2)); + flex_val_result_t result = validateWord(&biw_raw, TASK_VALIDATE_AND_REPAIR_2); switch(result) { case VALIDATE_REPAIRED_1: @@ -76,19 +76,19 @@ void parse_for_additional_biws(flex_frame_t* frame) switch(frame->biw.eob_info) { case 3: - if(validateWord(&(frame->blocks[0].word[3]), TASK_REPAIR_2 | TASK_VALIDATE_CHECKSUM)) + if(validateWord(&(frame->blocks[0].word[3]), TASK_VALIDATE_AND_REPAIR_2)) { process_biw_extra(frame->blocks[0].word[3]); } /* fall through */ case 2: - if(validateWord(&(frame->blocks[0].word[2]), TASK_REPAIR_2 | TASK_VALIDATE_CHECKSUM)) + if(validateWord(&(frame->blocks[0].word[2]), TASK_VALIDATE_AND_REPAIR_2)) { process_biw_extra(frame->blocks[0].word[2]); } /* fall through */ case 1: - if(validateWord(&(frame->blocks[0].word[1]), TASK_REPAIR_2 | TASK_VALIDATE_CHECKSUM)) + if(validateWord(&(frame->blocks[0].word[1]), TASK_VALIDATE_AND_REPAIR_2)) { process_biw_extra(frame->blocks[0].word[1]); } @@ -106,40 +106,49 @@ void process_biw_extra(uint32_t biw_raw) { uint32_t type = (biw_raw) >> 20; type = REVERSE_BITS_8((uint8_t)type) & 0x7; - uint8_t month, day, seconds, minutes, hour; - uint16_t year; switch(type) { // local id case 0x0: +#ifdef DEBUG biw_raw >>= 17; debug_printf("LOCAL ID SET: TIMEZONE=0x%x\n", (uint8_t)REVERSE_BITS_8((uint8_t)biw_raw) & 0x1F); debug_printf("WARNING: TIME NOT BEING HANDLED!\n"); +#endif break; // MDY case 0x1: +#ifdef DEBUG + { biw_raw >>= 7; - month = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x0F; + uint8_t month = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x0F; biw_raw >>= 5; - day = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; + uint8_t day = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; biw_raw >>= 5; - year = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; + uint16_t year = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; debug_printf("DATE SET: MONTH=%d, DAY=%d, YEAR=%d\n", month, day, year); debug_printf("WARNING: TIME NOT BEING HANDLED!\n"); + } +#endif break; // HMS case 0x2: +#ifdef DEBUG + { biw_raw >>= 6; - seconds = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x07; + uint8_t seconds = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x07; seconds = (seconds * 7) + (seconds >> 1); biw_raw >>= 6; - minutes = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x3F; + uint8_t minutes = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x3F; biw_raw >>= 5; - hour = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; + uint8_t hour = REVERSE_BITS_8((uint8_t)(biw_raw)) & 0x1F; debug_printf("TIME SET: HOUR=%d, MIN=%d, SEC=%d\n", hour, minutes, seconds); debug_printf("WARNING: TIME NOT BEING HANDLED!\n"); + } +#endif + /* fall through */ // Spare / offset case 0x3: debug_printf("SPARE/OFFSET SET\n"); diff --git a/src/flex_decoder_vectors.c b/src/flex_decoder_vectors.c index 04293c9..0feaad3 100644 --- a/src/flex_decoder_vectors.c +++ b/src/flex_decoder_vectors.c @@ -65,8 +65,8 @@ bool isAddressLong(uint32_t address_decoded) uint32_t decodeAddress(flex_frame_t* frame, uint8_t addr_pos) { - validateWord(getWord(frame, addr_pos), TASK_REPAIR_1 | TASK_REPAIR_2); - validateWord(getWord(frame, addr_pos + 1), TASK_REPAIR_1 | TASK_REPAIR_2); + validateWord(getWord(frame, addr_pos), TASK_REPAIR_BOTH); + validateWord(getWord(frame, addr_pos + 1), TASK_REPAIR_BOTH); bool long_address = checkForLongAddress(frame, addr_pos); uint32_t address = reverse_bits_32(*getWord(frame, addr_pos)); @@ -115,7 +115,7 @@ flex_vector_t decodeVector(flex_frame_t* frame, uint32_t vector_start, uint32_t flex_vector_t vec; if(VALIDATE_FAIL == - validateWord(getWord(frame, vector_start), TASK_VALIDATE_CHECKSUM | TASK_REPAIR_2)) + validateWord(getWord(frame, vector_start), TASK_VALIDATE_AND_REPAIR_2)) { debug_printf("Irreparable vector discarded: %d, value: 0x%x\n", vector_start, *getWord(frame, vector_start)); @@ -170,7 +170,7 @@ flex_vector_t decodeVector(flex_frame_t* frame, uint32_t vector_start, uint32_t flex_alpha_msg_header_t decode_alpha_header(uint32_t first_word, uint32_t second_word) { - flex_alpha_msg_header_t hdr; + flex_alpha_msg_header_t hdr = {0}; hdr.word = first_word; hdr.fragmentcheck = REVERSE_BITS_8((uint8_t)(first_word >> 24)); // Reverse k0..k7 @@ -272,7 +272,7 @@ void decode_numeric_msg(flex_frame_t* frame, flex_vector_t* vec, uint8_t vec_ind unsigned count = 4; unsigned start_offset = 0; unsigned bits_in_curr_word = 21; - unsigned char digit; + unsigned char digit = 0; assert(frame && vec && msg); @@ -461,7 +461,7 @@ void decode_binary_msg(flex_frame_t* frame, flex_vector_t* vec, uint8_t vec_inde unsigned __attribute__((unused)) bits_per_symbol = 1; unsigned __attribute__((unused)) signature; unsigned count = 8; - unsigned char byte; + unsigned char byte = 0; assert(frame && vec && msg); diff --git a/src/flex_types.h b/src/flex_types.h index 1f2b18f..fa62461 100644 --- a/src/flex_types.h +++ b/src/flex_types.h @@ -49,9 +49,14 @@ typedef enum typedef enum { + TASK_NONE = 0, TASK_VALIDATE_CHECKSUM = (1 << 0), TASK_REPAIR_1 = (1 << 1), TASK_REPAIR_2 = (1 << 2), + /// Combined: validate BCH and attempt two-bit repair + TASK_VALIDATE_AND_REPAIR_2 = (TASK_VALIDATE_CHECKSUM | TASK_REPAIR_2), + /// Combined: attempt both one-bit and two-bit repair + TASK_REPAIR_BOTH = (TASK_REPAIR_1 | TASK_REPAIR_2), } flex_val_task_t; typedef enum diff --git a/test/bits_tests.c b/test/bits_tests.c new file mode 100644 index 0000000..f842ba8 --- /dev/null +++ b/test/bits_tests.c @@ -0,0 +1,166 @@ +/* + * Copyright © 2019 Embedded Artistry LLC. + * License: MIT. See LICENSE file for details. + */ + +#include "flex_decoder.h" +#include "tests.h" + +/* Tests for reverse_bits_32() */ + +static void test_reverse_bits_32_zero(void** state) +{ + assert_int_equal(reverse_bits_32(0x00000000), 0x00000000); +} + +static void test_reverse_bits_32_all_ones(void** state) +{ + assert_int_equal(reverse_bits_32(0xFFFFFFFF), 0xFFFFFFFF); +} + +static void test_reverse_bits_32_single_high_bit(void** state) +{ + /* 0x80000000 = 1000...0000, reversed = 0000...0001 */ + assert_int_equal(reverse_bits_32(0x80000000), 0x00000001); +} + +static void test_reverse_bits_32_single_low_bit(void** state) +{ + /* 0x00000001 = 0000...0001, reversed = 1000...0000 */ + assert_int_equal(reverse_bits_32(0x00000001), 0x80000000); +} + +static void test_reverse_bits_32_byte_pattern(void** state) +{ + /* + * The function reverses all 32 bits (byte-swap + in-byte reversal). + * 0xAB = 1010 1011, reversed = 1101 0101 = 0xD5 + * reverse_bits_32(0xAB000000) => 0x000000D5 + */ + assert_int_equal(reverse_bits_32(0xAB000000), 0x000000D5); +} + +static void test_reverse_bits_32_known_word(void** state) +{ + /* + * reverse_bits_32(0x12345678): + * bytes: 0x78, 0x56, 0x34, 0x12 + * 0x78 = 0111 1000 -> 0001 1110 = 0x1E + * 0x56 = 0101 0110 -> 0110 1010 = 0x6A + * 0x34 = 0011 0100 -> 0010 1100 = 0x2C + * 0x12 = 0001 0010 -> 0100 1000 = 0x48 + * result: q[3]=0x1E, q[2]=0x6A, q[1]=0x2C, q[0]=0x48 => 0x1E6A2C48 + */ + assert_int_equal(reverse_bits_32(0x12345678), 0x1E6A2C48); +} + +static void test_reverse_bits_32_roundtrip(void** state) +{ + /* Double-reversing should recover the original value */ + uint32_t value = 0xDEADBEEF; + assert_int_equal(reverse_bits_32(reverse_bits_32(value)), value); +} + +static void test_reverse_bits_32_alternating(void** state) +{ + /* + * 0xAAAAAAAA = 1010 1010 ... repeated + * Reversed: 0101 0101 ... = 0x55555555 + */ + assert_int_equal(reverse_bits_32(0xAAAAAAAA), 0x55555555); + assert_int_equal(reverse_bits_32(0x55555555), 0xAAAAAAAA); +} + +/* Tests for count_set_bits() */ + +static void test_count_set_bits_zero(void** state) +{ + assert_int_equal(count_set_bits(0x00000000), 0); +} + +static void test_count_set_bits_all_ones(void** state) +{ + assert_int_equal(count_set_bits(0xFFFFFFFF), 32); +} + +static void test_count_set_bits_boundary_values(void** state) +{ + /* Single bits at low and high ends */ + assert_int_equal(count_set_bits(0x00000001), 1); + assert_int_equal(count_set_bits(0x80000000), 1); + /* Nibbles at low and high ends */ + assert_int_equal(count_set_bits(0x0000000F), 4); + assert_int_equal(count_set_bits(0xF0000000), 4); +} + +static void test_count_set_bits_byte(void** state) +{ + assert_int_equal(count_set_bits(0x000000FF), 8); +} + +static void test_count_set_bits_alternating(void** state) +{ + /* 0xAAAAAAAA = 1010...1010 = 16 bits set */ + assert_int_equal(count_set_bits(0xAAAAAAAA), 16); + /* 0x55555555 = 0101...0101 = 16 bits set */ + assert_int_equal(count_set_bits(0x55555555), 16); +} + +static void test_count_set_bits_known(void** state) +{ + /* 0x12345678: count individual bytes + * 0x78 = 0111 1000 = 4 bits + * 0x56 = 0101 0110 = 4 bits + * 0x34 = 0011 0100 = 3 bits + * 0x12 = 0001 0010 = 2 bits + * total = 13 bits + */ + assert_int_equal(count_set_bits(0x12345678), 13); +} + +/* Tests for REVERSE_BITS_8 macro */ + +static void test_reverse_bits_8_macro(void** state) +{ + /* + * REVERSE_BITS_8 uses a 64-bit intermediate, so mask to 8 bits to isolate + * the reversed byte. + */ + /* 0x01 = 0000 0001 -> 1000 0000 = 0x80 */ + assert_int_equal(REVERSE_BITS_8(0x01) & 0xFF, 0x80); + /* 0x80 = 1000 0000 -> 0000 0001 = 0x01 */ + assert_int_equal(REVERSE_BITS_8(0x80) & 0xFF, 0x01); + /* 0x0F = 0000 1111 -> 1111 0000 = 0xF0 */ + assert_int_equal(REVERSE_BITS_8(0x0F) & 0xFF, 0xF0); + /* 0xFF = 1111 1111 -> 1111 1111 = 0xFF */ + assert_int_equal(REVERSE_BITS_8(0xFF) & 0xFF, 0xFF); + /* 0x00 = 0000 0000 -> 0000 0000 = 0x00 */ + assert_int_equal(REVERSE_BITS_8(0x00) & 0xFF, 0x00); + /* 0xAB = 1010 1011 -> 1101 0101 = 0xD5 */ + assert_int_equal(REVERSE_BITS_8(0xAB) & 0xFF, 0xD5); +} + +#pragma mark - Public Functions - + +int bits_tests(void) +{ + const struct CMUnitTest bits_test_list[] = { + cmocka_unit_test(test_reverse_bits_32_zero), + cmocka_unit_test(test_reverse_bits_32_all_ones), + cmocka_unit_test(test_reverse_bits_32_single_high_bit), + cmocka_unit_test(test_reverse_bits_32_single_low_bit), + cmocka_unit_test(test_reverse_bits_32_byte_pattern), + cmocka_unit_test(test_reverse_bits_32_known_word), + cmocka_unit_test(test_reverse_bits_32_roundtrip), + cmocka_unit_test(test_reverse_bits_32_alternating), + cmocka_unit_test(test_count_set_bits_zero), + cmocka_unit_test(test_count_set_bits_all_ones), + cmocka_unit_test(test_count_set_bits_boundary_values), + cmocka_unit_test(test_count_set_bits_byte), + cmocka_unit_test(test_count_set_bits_alternating), + cmocka_unit_test(test_count_set_bits_known), + cmocka_unit_test(test_reverse_bits_8_macro), + }; + + return cmocka_run_group_tests(bits_test_list, NULL, NULL); +} diff --git a/test/decode_address.c b/test/decode_address.c index 8e661d4..ddd56de 100644 --- a/test/decode_address.c +++ b/test/decode_address.c @@ -8,56 +8,142 @@ #include "tests.h" #include -static uint8_t offset = 0; -static size_t len = FLEX_WORKING_PAYLOAD_SIZE; static flex_payload_t payload = {0}; static uint8_t working_raw_payload[FLEX_WORKING_PAYLOAD_SIZE] = {0}; -static void decode_long_address_full_payload(void** state) +/** + * Helper: initialize payload/frame from a raw capture buffer. + * Strips any leading 0x55 sync byte and the 2-byte sync C header, then + * runs convert_to_blocks / validate_blocks / process_biw. + * Returns the result of process_biw (false on BIW validation failure). + */ +static bool setup_frame(const uint8_t* data) { memset(&payload, 0, sizeof(flex_payload_t)); memset(working_raw_payload, 0, FLEX_WORKING_PAYLOAD_SIZE); payload.mode = FLEX_MODE_2FSK_1600BPS; - memcpy(working_raw_payload, fsk2_1600_numeric, FLEX_WORKING_PAYLOAD_SIZE); + memcpy(working_raw_payload, data, FLEX_WORKING_PAYLOAD_SIZE); + + uint8_t off = 0; + size_t ln = FLEX_WORKING_PAYLOAD_SIZE; if(working_raw_payload[0] == 0x55) { - // With the radio configuration for 3200bps 2-FSK, we end up receiving - // an extra 8 bits of the B sync field. So if payload[0] is 0x55, we're - // going to strip it off via an offset. - offset = 1; - len--; + off = 1; + ln--; } + off += 2; /* bypass sync C header */ + ln -= 2; - offset += 2; // Bypass SYNC C - len -= 2; - uint8_t phase = 0; - - convert_to_blocks(&working_raw_payload[offset], len, payload.mode, &payload.frame, phase); + convert_to_blocks(&working_raw_payload[off], ln, payload.mode, &payload.frame, 0); validate_blocks(&payload.frame); uint32_t biw_raw = *(const uint32_t*)(&(payload.frame.blocks[0].word[0])); - bool result = process_biw(biw_raw, &payload.frame); + return process_biw(biw_raw, &payload.frame); +} + +static void decode_long_address_full_payload(void** state) +{ + bool result = setup_frame(fsk2_1600_numeric); assert_true(result); - unsigned vector_start = payload.frame.biw.vector_start; unsigned address_start = payload.frame.biw.address_start; - int vec_count = (vector_start - payload.frame.biw.eob_info) - 1; + unsigned vector_start = payload.frame.biw.vector_start; + int vec_count = (int)(vector_start - payload.frame.biw.eob_info) - 1; assert_true(vec_count > 0); - assert_true(VALIDATE_FAIL != validateWord(getWord(&payload.frame, address_start + 1), - TASK_REPAIR_1 | TASK_REPAIR_2)); - assert_true(VALIDATE_FAIL != validateWord(getWord(&payload.frame, address_start), - TASK_REPAIR_1 | TASK_REPAIR_2)); + assert_true(VALIDATE_FAIL != + validateWord(getWord(&payload.frame, address_start + 1), TASK_REPAIR_BOTH)); + assert_true(VALIDATE_FAIL != + validateWord(getWord(&payload.frame, address_start), TASK_REPAIR_BOTH)); assert_int_equal(decodeAddress(&payload.frame, address_start), 123456789); } static void decode_long_address_raw(void** state) { - assert_int_equal(longBinaryToCapcode(0x3843, 0xf187), 123455555); - assert_true(isAddressLong(longBinaryToCapcode(0x3843, 0xf187))); + uint32_t capcode = longBinaryToCapcode(0x3843, 0xf187); + assert_int_equal(capcode, 123455555); + assert_true(isAddressLong(capcode)); +} + +/* Tests for shortBinaryToCapcode() */ + +static void test_short_binary_to_capcode_minimum(void** state) +{ + /* + * SHORT_CAPCODE_CONVERSION = 32768 = 0x8000 + * FLEX_SHORT_ADDRESS_MIN = 0x8001 + * shortBinaryToCapcode(0x8001) = 0x8001 - 32768 = 1 + */ + assert_int_equal(shortBinaryToCapcode(0x8001), 1); +} + +static void test_short_binary_to_capcode_known_value(void** state) +{ + /* + * Verify the conversion for a typical capcode value. + * capcode = binary_address - SHORT_CAPCODE_CONVERSION (32768) + */ + assert_int_equal(shortBinaryToCapcode(32768 + 1000), 1000); + assert_int_equal(shortBinaryToCapcode(32768 + 50000), 50000); +} + +/* Tests for isAddressLong() */ + +static void test_is_address_long_with_long_capcode(void** state) +{ + /* FLEX_LONG_ADDRESS_CAPCODE_MIN = 2100225 */ + assert_true(isAddressLong(2100225)); + assert_true(isAddressLong(123456789)); + assert_true(isAddressLong(999999999)); +} + +static void test_is_address_long_with_short_capcode(void** state) +{ + /* Any capcode below FLEX_LONG_ADDRESS_CAPCODE_MIN uses short address encoding */ + assert_false(isAddressLong(1)); + assert_false(isAddressLong(1000)); + assert_false(isAddressLong(2100224)); +} + +static void test_is_address_long_boundary(void** state) +{ + /* Boundary condition: exactly at the long address threshold */ + assert_false(isAddressLong(2100224)); /* just below threshold */ + assert_true(isAddressLong(2100225)); /* exactly at threshold */ + assert_true(isAddressLong(2100226)); /* just above threshold */ +} + +/* Tests for longBinaryToCapcode() */ + +static void test_long_binary_to_capcode_roundtrip_check(void** state) +{ + /* + * The known test value from the raw address test: + * longBinaryToCapcode(0x3843, 0xf187) = 123455555 + * This is the cornerstone verification used in embedded artistry testing. + * All outputs must classify as a long address. + */ + uint32_t capcode = longBinaryToCapcode(0x3843, 0xf187); + assert_int_equal(capcode, 123455555); + assert_true(isAddressLong(capcode)); + assert_true(capcode >= 2100225); +} + +/* Tests for checkForLongAddress() using real frame data */ + +static void test_check_for_long_address_on_known_payload(void** state) +{ + /* + * fsk2_1600_numeric targets capcode 123456789 which is a long address. + * After decoding we must detect that the address field uses 2 words. + */ + bool result = setup_frame(fsk2_1600_numeric); + assert_true(result); + + assert_true(checkForLongAddress(&payload.frame, payload.frame.biw.address_start)); } #pragma mark - Public Functions - @@ -67,6 +153,13 @@ int decode_address_tests(void) const struct CMUnitTest decode_address_test_list[] = { cmocka_unit_test(decode_long_address_raw), cmocka_unit_test(decode_long_address_full_payload), + cmocka_unit_test(test_short_binary_to_capcode_minimum), + cmocka_unit_test(test_short_binary_to_capcode_known_value), + cmocka_unit_test(test_is_address_long_with_long_capcode), + cmocka_unit_test(test_is_address_long_with_short_capcode), + cmocka_unit_test(test_is_address_long_boundary), + cmocka_unit_test(test_long_binary_to_capcode_roundtrip_check), + cmocka_unit_test(test_check_for_long_address_on_known_payload), }; return cmocka_run_group_tests(decode_address_test_list, NULL, NULL); diff --git a/test/fsk2_1600_tests.c b/test/fsk2_1600_tests.c index 490e4a4..11dcbd9 100644 --- a/test/fsk2_1600_tests.c +++ b/test/fsk2_1600_tests.c @@ -34,32 +34,169 @@ static int testTeardown(void** state) return 0; } -static void fsk2_1600_test_payload_numeric_long_addr(void** state) +/** + * Helper: run the full 1600bps decode pipeline on the working_raw_payload buffer. + * Handles any leading 0x55 sync byte (3200bps artifact) and the 2-byte sync C header. + * Returns true if process_biw succeeds, false otherwise. + */ +static bool run_1600_decode_pipeline(void) +{ + uint8_t offset = 0; + size_t len = FLEX_WORKING_PAYLOAD_SIZE; + + /* Handle potential extra 0x55 sync byte present in 3200bps captures */ + if(working_raw_payload[0] == 0x55) + { + offset = 1; + len--; + } + + offset += 2; /* Bypass sync C header (2 bytes) */ + len -= 2; + + uint8_t phase = 0; + convert_to_blocks(&working_raw_payload[offset], len, payload.mode, &payload.frame, phase); + validate_blocks(&payload.frame); + + uint32_t biw_raw = *(const uint32_t*)(&(payload.frame.blocks[0].word[0])); + return process_biw(biw_raw, &payload.frame); +} + +/** + * Helper: assert BIW fields match the expected layout for numeric/phone messages + * (priority=2, vector_start=3, address_start=1, carry_on=0, eob_info=0). + */ +static void assert_numeric_biw(void) +{ + assert_int_equal(payload.frame.biw.priority, 2); + assert_int_equal(payload.frame.biw.carry_on, 0); + assert_int_equal(payload.frame.biw.eob_info, 0); + assert_int_equal(payload.frame.biw.address_start, 1); + assert_int_equal(payload.frame.biw.vector_start, 3); +} + +/** + * Helper: assert BIW fields match the expected layout for tone/alphanumeric messages + * (priority=1, vector_start=2, address_start=1, carry_on=0, eob_info=0). + */ +static void assert_tone_alpha_biw(void) { - flex_state_t current_state = FLEX_STATE_2FSK_RX; + assert_int_equal(payload.frame.biw.priority, 1); + assert_int_equal(payload.frame.biw.carry_on, 0); + assert_int_equal(payload.frame.biw.eob_info, 0); + assert_int_equal(payload.frame.biw.address_start, 1); + assert_int_equal(payload.frame.biw.vector_start, 2); +} +static void fsk2_1600_test_payload_numeric_long_addr(void** state) +{ memcpy(working_raw_payload, fsk2_1600_numeric, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + assert_numeric_biw(); + + /* Address words must be BCH-valid */ + unsigned addr_start = payload.frame.biw.address_start; + assert_true(validateWord(getWord(&payload.frame, addr_start), TASK_REPAIR_BOTH) != + VALIDATE_FAIL); + assert_true(validateWord(getWord(&payload.frame, addr_start + 1), TASK_REPAIR_BOTH) != + VALIDATE_FAIL); + + /* Decoded capcode must match the expected long address */ + uint32_t address = decodeAddress(&payload.frame, addr_start); + assert_int_equal(address, 123456789); + assert_true(isAddressLong(address)); } static void fsk2_1600_test_payload_numeric_short_addr(void** state) { - flex_state_t current_state = FLEX_STATE_2FSK_RX; - memcpy(working_raw_payload, fsk2_1600_numeric2, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + assert_numeric_biw(); + + /* Decoded address must be a valid non-zero capcode */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); } static void fsk2_1600_test_payload_phonenum_long_addr(void** state) { - flex_state_t current_state = FLEX_STATE_2FSK_RX; - memcpy(working_raw_payload, fsk2_1600_numeric_phonenum_long_addr, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + assert_numeric_biw(); + + /* This payload targets a long address */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); + assert_true(isAddressLong(address)); } static void fsk2_1600_test_payload_phonenum_short_addr(void** state) { - flex_state_t current_state = FLEX_STATE_2FSK_RX; - memcpy(working_raw_payload, fsk2_1600_numeric_phonenum_short_addr, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + assert_numeric_biw(); + + /* Decoded address must be valid */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); +} + +static void fsk2_1600_test_payload_short_numeric_long_addr(void** state) +{ + memcpy(working_raw_payload, fsk2_1600_short_numeric_long_addr, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + assert_numeric_biw(); + + /* This payload targets a long address */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); + assert_true(isAddressLong(address)); +} + +static void fsk2_1600_test_payload_tone_short_addr(void** state) +{ + memcpy(working_raw_payload, fsk_1600_tone_short_addr, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + /* Tone messages have vector_start=2 and priority=1 */ + assert_tone_alpha_biw(); + + /* Decoded address must be valid */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); +} + +static void fsk2_1600_test_payload_alphanumeric_andrews_equipment(void** state) +{ + memcpy(working_raw_payload, fsk2_1600_alpha_andrews_equipment_short_addr, + FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_1600_decode_pipeline(); + assert_true(result); + + /* Alphanumeric messages have vector_start=2 and priority=1 */ + assert_tone_alpha_biw(); + + /* Decoded address must be valid */ + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_true(address != 0); } #pragma mark - Public Functions - @@ -75,6 +212,12 @@ int fsk2_1600_tests(void) testTeardown), cmocka_unit_test_setup_teardown(fsk2_1600_test_payload_phonenum_short_addr, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_1600_test_payload_short_numeric_long_addr, testSetup, + testTeardown), + cmocka_unit_test_setup_teardown(fsk2_1600_test_payload_tone_short_addr, testSetup, + testTeardown), + cmocka_unit_test_setup_teardown(fsk2_1600_test_payload_alphanumeric_andrews_equipment, + testSetup, testTeardown), }; return cmocka_run_group_tests(fsk2_1600_test_list, NULL, NULL); diff --git a/test/fsk2_3200_tests.c b/test/fsk2_3200_tests.c index 492baf1..4eb402f 100644 --- a/test/fsk2_3200_tests.c +++ b/test/fsk2_3200_tests.c @@ -34,11 +34,104 @@ static int testTeardown(void** state) return 0; } -static void fsk2_3200_test(void** state) +/** + * Helper: run the full 3200bps decode pipeline on working_raw_payload. + * Handles the leading 0x55 sync byte(s) and the 2-byte sync C header. + * Decodes phase 0. Returns true if process_biw succeeds. + */ +static bool run_3200_decode_pipeline(uint8_t phase) +{ + uint8_t offset = 0; + size_t len = FLEX_WORKING_PAYLOAD_SIZE; + + /* For 3200bps captures, leading 0x55 bytes may appear from the B sync field */ + while(count_set_bits(working_raw_payload[offset] ^ 0x55) < 3) + { + offset++; + len--; + } + + offset += 2; /* Bypass sync C header (2 bytes) */ + len -= 2; + + convert_to_blocks(&working_raw_payload[offset], len, payload.mode, &payload.frame, phase); + validate_blocks(&payload.frame); + + uint32_t biw_raw = *(const uint32_t*)(&(payload.frame.blocks[0].word[0])); + return process_biw(biw_raw, &payload.frame); +} + +static void fsk2_3200_test_alpha_biw(void** state) +{ + /* + * Decode the alpha message payload and verify BIW fields match the + * values validated by the state machine tests. + */ + memcpy(working_raw_payload, fsk2_3200_alpha, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_3200_decode_pipeline(0); + assert_true(result); + + assert_int_equal(payload.frame.biw.priority, 2); + assert_int_equal(payload.frame.biw.carry_on, 0); + assert_int_equal(payload.frame.biw.eob_info, 0); + assert_int_equal(payload.frame.biw.address_start, 1); + assert_int_equal(payload.frame.biw.vector_start, 3); +} + +static void fsk2_3200_test_alpha_address(void** state) { - flex_state_t current_state = FLEX_STATE_2FSK_RX; + /* After decoding, the address in the frame must be non-zero */ + memcpy(working_raw_payload, fsk2_3200_alpha, FLEX_WORKING_PAYLOAD_SIZE); + bool result = run_3200_decode_pipeline(0); + assert_true(result); + + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_int_not_equal(address, 0); +} + +static void fsk2_3200_test_alpha_vec_count(void** state) +{ + /* Vector count must be positive for a valid frame */ memcpy(working_raw_payload, fsk2_3200_alpha, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_3200_decode_pipeline(0); + assert_true(result); + + int vec_count = + (int)(payload.frame.biw.vector_start - payload.frame.biw.eob_info) - 1; + assert_true(vec_count > 0); +} + +static void fsk2_3200_test_short_addr_phase_b_biw(void** state) +{ + /* + * This dataset contains a message on phase B (phase index 1). + * Verify BIW fields match the state machine test expectations. + */ + memcpy(working_raw_payload, fsk2_3200_short_addr_phase_b, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_3200_decode_pipeline(1); + assert_true(result); + + assert_int_equal(payload.frame.biw.priority, 1); + assert_int_equal(payload.frame.biw.carry_on, 0); + assert_int_equal(payload.frame.biw.eob_info, 0); + assert_int_equal(payload.frame.biw.address_start, 1); + assert_int_equal(payload.frame.biw.vector_start, 2); +} + +static void fsk2_3200_test_short_addr_phase_b_address(void** state) +{ + /* After decoding the phase B frame, the address must be non-zero */ + memcpy(working_raw_payload, fsk2_3200_short_addr_phase_b, FLEX_WORKING_PAYLOAD_SIZE); + + bool result = run_3200_decode_pipeline(1); + assert_true(result); + + uint32_t address = decodeAddress(&payload.frame, payload.frame.biw.address_start); + assert_int_not_equal(address, 0); } #pragma mark - Public Functions - @@ -46,7 +139,13 @@ static void fsk2_3200_test(void** state) int fsk2_3200_tests(void) { const struct CMUnitTest fsk2_3200_test_list[] = { - cmocka_unit_test_setup_teardown(fsk2_3200_test, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_test_alpha_biw, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_test_alpha_address, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_test_alpha_vec_count, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_test_short_addr_phase_b_biw, testSetup, + testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_test_short_addr_phase_b_address, testSetup, + testTeardown), }; return cmocka_run_group_tests(fsk2_3200_test_list, NULL, NULL); diff --git a/test/main.c b/test/main.c index 226ce09..b5ae5bb 100644 --- a/test/main.c +++ b/test/main.c @@ -12,12 +12,17 @@ int main(void) // Generate JUnit results // cmocka_set_message_output(CM_OUTPUT_XML); + overall_result |= bits_tests(); + overall_result |= validation_tests(); overall_result |= state_machine_sync_tests(); overall_result |= state_machine_fsk2_1600_tests(); overall_result |= state_machine_fsk2_3200_tests(); overall_result |= state_machine_fsk4_1600_tests(); overall_result |= state_machine_fsk4_3200_tests(); + overall_result |= sync_tests(); overall_result |= decode_address_tests(); + overall_result |= fsk2_1600_tests(); + overall_result |= fsk2_3200_tests(); overall_result |= fsk4_1600_tests(); overall_result |= fsk4_3200_tests(); diff --git a/test/meson.build b/test/meson.build index fe34819..59552b8 100644 --- a/test/meson.build +++ b/test/meson.build @@ -2,6 +2,8 @@ test_files = [ 'main.c', + 'bits_tests.c', + 'validation_tests.c', 'decode_address.c', 'sync_tests.c', 'fsk2_1600_tests.c', diff --git a/test/state_machine/sync_tests.c b/test/state_machine/sync_tests.c index fbf9e7a..2342277 100644 --- a/test/state_machine/sync_tests.c +++ b/test/state_machine/sync_tests.c @@ -64,6 +64,7 @@ int state_machine_sync_tests(void) { const struct CMUnitTest sync_test_list[] = { cmocka_unit_test_setup_teardown(fsk2_1600_sync_test, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_3200_sync_test, testSetup, testTeardown), }; return cmocka_run_group_tests(sync_test_list, NULL, NULL); diff --git a/test/sync_tests.c b/test/sync_tests.c index 42f49cf..b3a28f3 100644 --- a/test/sync_tests.c +++ b/test/sync_tests.c @@ -33,20 +33,46 @@ static int testTeardown(void** state) return 0; } -// TODO: refactor this into a state machine test file - static void fsk2_1600_sync_test(void** state) { flex_state_t current_state = FLEX_STATE_WAIT_FOR_RX; memcpy(working_raw_payload, fsk_sync_2fsk_1600bps, FLEX_SYNC_PAYLOAD_SIZE); - initial_rx_handler(&payload, working_raw_payload, FLEX_SYNC_PAYLOAD_SIZE); + current_state = + interpret_message(current_state, &payload, working_raw_payload, FLEX_SYNC_PAYLOAD_SIZE); + + /* A valid 1600bps sync packet must set the mode and advance to the prepare state */ + assert_int_equal(current_state, FLEX_STATE_2FSK_PREPARE_RX_AT_1600_BPS); + assert_int_equal(payload.mode, FLEX_MODE_2FSK_1600BPS); } static void fsk2_3200_sync_test(void** state) { flex_state_t current_state = FLEX_STATE_WAIT_FOR_RX; memcpy(working_raw_payload, fsk_sync_2fsk_3200bps, FLEX_SYNC_PAYLOAD_SIZE); + + current_state = + interpret_message(current_state, &payload, working_raw_payload, FLEX_SYNC_PAYLOAD_SIZE); + + /* A valid 3200bps sync packet must set the mode and advance to the prepare state */ + assert_int_equal(current_state, FLEX_STATE_2FSK_PREPARE_RX_AT_3200_BPS); + assert_int_equal(payload.mode, FLEX_MODE_2FSK_3200BPS); +} + +static void fsk2_sync_invalid_length_test(void** state) +{ + /* + * If the sync payload length is wrong (not exactly FLEX_SYNC_PAYLOAD_SIZE = 8), + * initial_rx_handler must keep the machine in WAIT_FOR_RX. + */ + flex_state_t current_state = FLEX_STATE_WAIT_FOR_RX; + memcpy(working_raw_payload, fsk_sync_2fsk_1600bps, FLEX_SYNC_PAYLOAD_SIZE); + + /* Pass one byte less than required */ + current_state = + interpret_message(current_state, &payload, working_raw_payload, FLEX_SYNC_PAYLOAD_SIZE - 1); + + assert_int_equal(current_state, FLEX_STATE_WAIT_FOR_RX); } #pragma mark - Public Functions - @@ -56,6 +82,7 @@ int sync_tests(void) const struct CMUnitTest sync_test_list[] = { cmocka_unit_test_setup_teardown(fsk2_1600_sync_test, testSetup, testTeardown), cmocka_unit_test_setup_teardown(fsk2_3200_sync_test, testSetup, testTeardown), + cmocka_unit_test_setup_teardown(fsk2_sync_invalid_length_test, testSetup, testTeardown), }; return cmocka_run_group_tests(sync_test_list, NULL, NULL); diff --git a/test/tests.h b/test/tests.h index 07d461b..b88cfb0 100644 --- a/test/tests.h +++ b/test/tests.h @@ -26,4 +26,9 @@ int fsk4_1600_tests(void); int fsk4_3200_tests(void); int decode_address_tests(void); +// Utility and Validation Tests + +int bits_tests(void); +int validation_tests(void); + #endif // TEST_H_ diff --git a/test/validation_tests.c b/test/validation_tests.c new file mode 100644 index 0000000..a8d6ffa --- /dev/null +++ b/test/validation_tests.c @@ -0,0 +1,171 @@ +/* + * Copyright © 2019 Embedded Artistry LLC. + * License: MIT. See LICENSE file for details. + */ + +#include "flex_decoder.h" +#include "tests.h" +#include + +/* Tests for recreate_crc() and validateBCH() */ + +static void test_recreate_crc_produces_valid_bch(void** state) +{ + /* + * recreate_crc() takes a 32-bit word with the upper 21 bits as data and + * fills in the lower 11 bits as the BCH check bits + parity. + * The result must always pass validateBCH(). + */ + uint32_t payload_bits = 0x12345800; /* data in upper 21 bits */ + uint32_t valid_word = recreate_crc(payload_bits); + assert_true(validateBCH(valid_word)); +} + +static void test_recreate_crc_preserves_data_bits(void** state) +{ + /* The upper 21 bits (data) should be unchanged after recreate_crc */ + uint32_t payload_bits = 0xABCDE800; + uint32_t valid_word = recreate_crc(payload_bits); + assert_int_equal(valid_word >> 11, payload_bits >> 11); +} + +static void test_recreate_crc_all_zeros(void** state) +{ + /* All-zero payload should still produce a BCH-valid word */ + uint32_t valid_word = recreate_crc(0x00000000); + assert_true(validateBCH(valid_word)); +} + +static void test_recreate_crc_all_ones_payload(void** state) +{ + /* All-ones in the data portion should produce a BCH-valid word */ + uint32_t valid_word = recreate_crc(0xFFFFF800); + assert_true(validateBCH(valid_word)); +} + +static void test_validateBCH_multiple_valid_words(void** state) +{ + /* Verify validateBCH works for a range of payloads */ + uint32_t payloads[] = { + 0x00001800, 0x12345800, 0x55555800, 0x87654800, 0xAAAAA800, + }; + + for(unsigned i = 0; i < sizeof(payloads) / sizeof(payloads[0]); i++) + { + uint32_t valid_word = recreate_crc(payloads[i]); + assert_true(validateBCH(valid_word)); + } +} + +/* Tests for validateWord() */ + +static void test_validateWord_idle_word(void** state) +{ + /* + * The idle pattern 0xAAAAAAAA is treated specially - it bypasses BCH + * validation and always returns VALIDATE_PASS. It must also not be + * modified. + */ + uint32_t idle = 0xAAAAAAAA; + flex_val_result_t result = validateWord(&idle, TASK_REPAIR_BOTH); + assert_int_not_equal(result, VALIDATE_FAIL); + assert_int_equal(idle, 0xAAAAAAAA); +} + +static void test_validateWord_no_task_fails_invalid(void** state) +{ + /* + * A word that fails BCH validation with no repair tasks should return + * VALIDATE_FAIL. The value 0x00000001 is not a valid BCH codeword. + */ + uint32_t bad_word = 0x00000001; + flex_val_result_t result = validateWord(&bad_word, TASK_NONE); + assert_int_equal(result, VALIDATE_FAIL); +} + +/* Tests for attempt_repair() */ + +static void test_attempt_repair_single_bit_corrects(void** state) +{ + /* + * BCH (31,21) codes can detect and correct single-bit errors. + * Flipping one bit in a valid codeword must be repairable with TASK_REPAIR_1. + */ + uint32_t valid_word = recreate_crc(0x12345800); + + /* Flip bit 5 (a check bit in the lower 11-bit region) */ + uint32_t corrupted = valid_word ^ (1u << 5); + + /* BCH guarantees a single bit flip produces a non-codeword */ + assert_false(validateBCH(corrupted)); + + flex_val_result_t result = attempt_repair(&corrupted, TASK_REPAIR_1); + assert_int_equal(result, VALIDATE_REPAIRED_1); + assert_true(validateBCH(corrupted)); +} + +static void test_attempt_repair_single_bit_in_data_region(void** state) +{ + /* Flipping a bit in the data region (upper 21 bits) should also be repairable */ + uint32_t valid_word = recreate_crc(0x87654800); + + /* Flip bit 15 (a data bit) */ + uint32_t corrupted = valid_word ^ (1u << 15); + assert_false(validateBCH(corrupted)); + + flex_val_result_t result = attempt_repair(&corrupted, TASK_REPAIR_1); + assert_int_not_equal(result, VALIDATE_FAIL); + assert_true(validateBCH(corrupted)); +} + +static void test_attempt_repair_double_bit_corrects(void** state) +{ + /* + * TASK_REPAIR_2 exhaustively tries all pairs of bits. + * Flipping two bits in a valid codeword should be repairable. + */ + uint32_t valid_word = recreate_crc(0x87654800); + + /* Flip two bits in the check region */ + uint32_t corrupted = valid_word ^ (1u << 3) ^ (1u << 8); + assert_false(validateBCH(corrupted)); + + flex_val_result_t result = attempt_repair(&corrupted, TASK_REPAIR_2); + assert_int_not_equal(result, VALIDATE_FAIL); + assert_true(validateBCH(corrupted)); +} + +static void test_attempt_repair_no_task_returns_fail(void** state) +{ + /* + * With no repair tasks, attempt_repair should immediately return + * VALIDATE_FAIL without modifying the word. 0x00000001 is not a valid + * BCH codeword. + */ + uint32_t bad_word = 0x00000001; + uint32_t original = bad_word; + flex_val_result_t result = attempt_repair(&bad_word, TASK_NONE); + assert_int_equal(result, VALIDATE_FAIL); + assert_int_equal(bad_word, original); +} + +#pragma mark - Public Functions - + +int validation_tests(void) +{ + const struct CMUnitTest validation_test_list[] = { + cmocka_unit_test(test_recreate_crc_produces_valid_bch), + cmocka_unit_test(test_recreate_crc_preserves_data_bits), + cmocka_unit_test(test_recreate_crc_all_zeros), + cmocka_unit_test(test_recreate_crc_all_ones_payload), + cmocka_unit_test(test_validateBCH_multiple_valid_words), + cmocka_unit_test(test_validateWord_idle_word), + cmocka_unit_test(test_validateWord_no_task_fails_invalid), + cmocka_unit_test(test_attempt_repair_single_bit_corrects), + cmocka_unit_test(test_attempt_repair_single_bit_in_data_region), + cmocka_unit_test(test_attempt_repair_double_bit_corrects), + cmocka_unit_test(test_attempt_repair_no_task_returns_fail), + }; + + return cmocka_run_group_tests(validation_test_list, NULL, NULL); +}