diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0a38247 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,70 @@ +name: Build and Package Firmware + +on: + push: + branches: + - '*' + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + container: espressif/idf:release-v5.2 + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Configure Safe Directory + run: | + git config --global --add safe.directory "*" + + - name: Set Version Identifier + run: | + git rev-parse --short HEAD > version.txt + echo "Build version (commit ID): $(cat version.txt)" + + - name: Build and Run Host Unit Tests + run: | + cmake -S tests -B tests/build + cmake --build tests/build + ./tests/build/test_decoder + + - name: Build Firmware + run: | + . $IDF_PATH/export.sh + idf.py build + + - name: Create Flashing Instructions Document + run: | + mkdir -p dist + cat << 'EOF' > dist/README_FLASHING.txt + ESP32 ALDL Bridge Firmware Flash Instructions + ============================================= + + Prerequisites: + - Python 3 installed + - esptool installed: pip install esptool + + Connect your ESP32 to your PC, identify its serial port, and run the following command to flash: + + esptool.py --chip esp32 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 2MB --flash_freq 40m 0x1000 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-aldl.bin + EOF + + - name: Stage Firmware Binaries + run: | + cp build/esp32-aldl.bin dist/ + cp build/bootloader/bootloader.bin dist/ + cp build/partition_table/partition-table.bin dist/ + + - name: Package Release + run: | + tar -czvf esp32-aldl-firmware.tar.gz -C dist . + + - name: Upload Firmware Package + uses: actions/upload-artifact@v3 + with: + name: esp32-aldl-firmware-${{ github.sha }} + path: esp32-aldl-firmware.tar.gz diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 8a9d914..6b00812 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,2 +1,8 @@ -idf_component_register(SRCS "main.c" +idf_component_register(SRCS "main.c" "decoder.c" INCLUDE_DIRS ".") + +if(EXISTS "${PROJECT_DIR}/version.txt") + file(READ "${PROJECT_DIR}/version.txt" PROJECT_VER_CONTENT) + string(STRIP "${PROJECT_VER_CONTENT}" PROJECT_VER_CONTENT) + target_compile_definitions(${COMPONENT_LIB} PRIVATE PROJECT_VER="${PROJECT_VER_CONTENT}") +endif() diff --git a/main/decoder.c b/main/decoder.c new file mode 100644 index 0000000..1e8a9e8 --- /dev/null +++ b/main/decoder.c @@ -0,0 +1,115 @@ +#include "decoder.h" + +static enqueue_frame_cb_t s_enqueue_cb = NULL; +static print_frame_cb_t s_print_cb = NULL; + +void decoder_init(enqueue_frame_cb_t enqueue_cb, print_frame_cb_t print_cb) { + s_enqueue_cb = enqueue_cb; + s_print_cb = print_cb; +} + +uint8_t classify_pulse(uint32_t us) { + if (us < MIN_VALID_US) return PC_GLITCH; + if (us > MAX_VALID_US) return PC_IDLE_GAP; + if (us > MERGE_THRESHOLD_US) return PC_MERGED; + if (us < THRESHOLD_US) return PC_LOGIC_0; + return PC_LOGIC_1; +} + +void reset_decoder(struct DecoderContext *ctx_ptr) { + ctx_ptr->state = DS_HUNT_SYNC; + ctx_ptr->sync_count = 0; + ctx_ptr->bit_count = 0; + ctx_ptr->byte_count = 0; + ctx_ptr->separator_count = 0; + ctx_ptr->frame_errors = 0; + ctx_ptr->bytes_this_frame = 0; +} + +static void feed_bit(struct DecoderContext *ctx_ptr, uint8_t pc) { + switch (ctx_ptr->state) { + case DS_HUNT_SYNC: + if (pc == PC_LOGIC_1) { + ctx_ptr->sync_count++; + if (ctx_ptr->sync_count >= SYNC_ONES_NEEDED) { + ctx_ptr->sync_count = 0; + ctx_ptr->byte_count = 0; + ctx_ptr->bit_count = 0; + ctx_ptr->separator_count = 0; + ctx_ptr->frame_errors = 0; + ctx_ptr->bytes_this_frame = 0; + ctx_ptr->state = DS_AWAIT_START; + } + } else { + ctx_ptr->sync_count = 0; + } + break; + + case DS_AWAIT_START: + if (pc == PC_LOGIC_0) { + ctx_ptr->current_byte = 0; + ctx_ptr->bit_count = 0; + ctx_ptr->separator_count = 0; + ctx_ptr->state = DS_READ_BITS; + } else { + ctx_ptr->separator_count++; + if (ctx_ptr->separator_count > MAX_SEPARATORS) { + reset_decoder(ctx_ptr); + } + } + break; + + case DS_READ_BITS: { + uint8_t bit_val = (pc == PC_LOGIC_1) ? 1u : 0u; + ctx_ptr->current_byte = (uint8_t)((ctx_ptr->current_byte << 1) | bit_val); + ctx_ptr->bit_count++; + + if (ctx_ptr->bit_count == 8) { + ctx_ptr->frame[ctx_ptr->byte_count] = ctx_ptr->current_byte; + ctx_ptr->bytes_this_frame++; + ctx_ptr->bit_count = 0; + ctx_ptr->byte_count++; + + if (ctx_ptr->byte_count >= PAYLOAD_BYTES) { + ctx_ptr->frames_decoded++; + + if (s_print_cb) { + s_print_cb(ctx_ptr->frames_decoded, ctx_ptr->frame); + } + if (s_enqueue_cb) { + s_enqueue_cb(ctx_ptr->frame, PAYLOAD_BYTES); + } + + reset_decoder(ctx_ptr); + } else { + ctx_ptr->separator_count = 0; + ctx_ptr->state = DS_AWAIT_START; + } + } + break; + } + default: + reset_decoder(ctx_ptr); + break; + } +} + +void process_pulse(struct DecoderContext *ctx_ptr, uint32_t pulse_us) { + uint8_t pc = classify_pulse(pulse_us); + if (pc == PC_GLITCH) return; + + if (pc == PC_IDLE_GAP) { + reset_decoder(ctx_ptr); + return; + } + + if (pc == PC_MERGED) { + uint32_t hidden_est = pulse_us - LOGIC1_PULSE_US; + uint8_t hidden_bit = (hidden_est >= THRESHOLD_US) ? PC_LOGIC_1 : PC_LOGIC_0; + feed_bit(ctx_ptr, hidden_bit); + feed_bit(ctx_ptr, PC_LOGIC_1); + return; + } + + feed_bit(ctx_ptr, pc); +} diff --git a/main/decoder.h b/main/decoder.h new file mode 100644 index 0000000..73eb1f4 --- /dev/null +++ b/main/decoder.h @@ -0,0 +1,83 @@ +#ifndef DECODER_H +#define DECODER_H + +#include +#include +#include + +#define LOGIC0_PULSE_US 1111u +#define LOGIC1_PULSE_US 4167u +#define THRESHOLD_US 2639u +#define MIN_VALID_US 300u +#define MERGE_THRESHOLD_US 8000u +#define MAX_VALID_US 13500u +#define MAX_SEPARATORS 12u +#define SYNC_ONES_NEEDED 8u +#define PAYLOAD_BYTES 25u + +#define PC_GLITCH ((uint8_t)0) +#define PC_LOGIC_0 ((uint8_t)1) +#define PC_LOGIC_1 ((uint8_t)2) +#define PC_IDLE_GAP ((uint8_t)3) +#define PC_MERGED ((uint8_t)4) + +#define DS_HUNT_SYNC ((uint8_t)0) +#define DS_AWAIT_START ((uint8_t)1) +#define DS_READ_BITS ((uint8_t)2) + +struct BtFrame { + uint8_t data[PAYLOAD_BYTES]; + uint8_t len; +}; + +struct DecoderContext { + uint8_t state; + uint8_t sync_count; + uint8_t bit_count; + uint8_t current_byte; + uint8_t byte_count; + uint8_t separator_count; + uint8_t frame[PAYLOAD_BYTES]; + uint32_t frame_errors; + uint32_t frames_decoded; + uint32_t bytes_this_frame; +}; + +struct RingBuffer { + volatile uint32_t data[256]; + volatile uint16_t head; + volatile uint16_t tail; +}; + +#define RB_MASK ((uint16_t)255u) + +// Callbacks for hardware bridging (e.g. FreeRTOS queueing, hardware logging) +typedef void (*enqueue_frame_cb_t)(const uint8_t *frame_data, uint8_t len); +typedef void (*print_frame_cb_t)(uint32_t frames_decoded, const uint8_t *frame_data); + +// Initialize decoder callbacks +void decoder_init(enqueue_frame_cb_t enqueue_cb, print_frame_cb_t print_cb); + +// Core decoding interface +uint8_t classify_pulse(uint32_t us); +void reset_decoder(struct DecoderContext *ctx_ptr); +void process_pulse(struct DecoderContext *ctx_ptr, uint32_t pulse_us); + +// Ring buffer inline helpers +static inline void rb_push(struct RingBuffer *rb_ptr, uint32_t v) { + uint16_t next = (rb_ptr->head + 1u) & RB_MASK; + if (next == rb_ptr->tail) return; + rb_ptr->data[rb_ptr->head] = v; + __asm__ __volatile__("" ::: "memory"); + rb_ptr->head = next; +} + +static inline bool rb_pop(struct RingBuffer *rb_ptr, uint32_t *out) { + if (rb_ptr->tail == rb_ptr->head) return false; + *out = rb_ptr->data[rb_ptr->tail]; + __asm__ __volatile__("" ::: "memory"); + rb_ptr->tail = (rb_ptr->tail + 1u) & RB_MASK; + return true; +} + +#endif // DECODER_H diff --git a/main/main.c b/main/main.c index c128057..b287473 100644 --- a/main/main.c +++ b/main/main.c @@ -13,58 +13,19 @@ #include "esp_bt_device.h" #include "esp_gap_bt_api.h" #include "esp_spp_api.h" +#include "decoder.h" #define ALDL_PIN GPIO_NUM_4 -#define LOGIC0_PULSE_US 1111u -#define LOGIC1_PULSE_US 4167u -#define THRESHOLD_US 2639u -#define MIN_VALID_US 300u -#define MERGE_THRESHOLD_US 8000u -#define MAX_VALID_US 13500u -#define MAX_SEPARATORS 12u -#define SYNC_ONES_NEEDED 8u -#define PAYLOAD_BYTES 25u - #define BT_DEVICE_NAME "ESP32-ALDL" #define BT_QUEUE_DEPTH 4u -#define PC_GLITCH ((uint8_t)0) -#define PC_LOGIC_0 ((uint8_t)1) -#define PC_LOGIC_1 ((uint8_t)2) -#define PC_IDLE_GAP ((uint8_t)3) -#define PC_MERGED ((uint8_t)4) - -#define DS_HUNT_SYNC ((uint8_t)0) -#define DS_AWAIT_START ((uint8_t)1) -#define DS_READ_BITS ((uint8_t)2) +#ifndef PROJECT_VER +#define PROJECT_VER "unknown" +#endif static const char *TAG = "ALDL"; -struct BtFrame { - uint8_t data[PAYLOAD_BYTES]; - uint8_t len; -}; - -struct DecoderContext { - uint8_t state; - uint8_t sync_count; - uint8_t bit_count; - uint8_t current_byte; - uint8_t byte_count; - uint8_t separator_count; - uint8_t frame[PAYLOAD_BYTES]; - uint32_t frame_errors; - uint32_t frames_decoded; - uint32_t bytes_this_frame; -}; - -struct RingBuffer { - volatile uint32_t data[256]; - volatile uint16_t head; - volatile uint16_t tail; -}; - static struct RingBuffer rb; static struct DecoderContext ctx; static QueueHandle_t bt_queue = NULL; @@ -72,24 +33,6 @@ static QueueHandle_t bt_queue = NULL; static uint32_t spp_handle = 0; static bool bt_connected = false; -#define RB_MASK ((uint16_t)255u) - -static inline void IRAM_ATTR rb_push(uint32_t v) { - uint16_t next = (rb.head + 1u) & RB_MASK; - if (next == rb.tail) return; - rb.data[rb.head] = v; - __asm__ __volatile__("" ::: "memory"); - rb.head = next; -} - -static inline bool rb_pop(uint32_t *out) { - if (rb.tail == rb.head) return false; - *out = rb.data[rb.tail]; - __asm__ __volatile__("" ::: "memory"); - rb.tail = (rb.tail + 1u) & RB_MASK; - return true; -} - static volatile uint64_t isr_fall_us = 0; static void IRAM_ATTR aldl_gpio_isr(void* arg) { @@ -98,128 +41,30 @@ static void IRAM_ATTR aldl_gpio_isr(void* arg) { isr_fall_us = now; } else { if (isr_fall_us != 0) { - rb_push((uint32_t)(now - isr_fall_us)); + rb_push(&rb, (uint32_t)(now - isr_fall_us)); isr_fall_us = 0; } } } -static uint8_t classify_pulse(uint32_t us) { - if (us < MIN_VALID_US) return PC_GLITCH; - if (us > MAX_VALID_US) return PC_IDLE_GAP; - if (us > MERGE_THRESHOLD_US) return PC_MERGED; - if (us < THRESHOLD_US) return PC_LOGIC_0; - return PC_LOGIC_1; -} - -static void reset_decoder(void) { - ctx.state = DS_HUNT_SYNC; - ctx.sync_count = 0; - ctx.bit_count = 0; - ctx.byte_count = 0; - ctx.separator_count = 0; - ctx.frame_errors = 0; - ctx.bytes_this_frame = 0; -} - -static void enqueue_frame(void) { +// Hardware callback to queue frames for Bluetooth transmission +static void enqueue_frame_hw(const uint8_t *frame_data, uint8_t len) { struct BtFrame f; - memcpy(f.data, ctx.frame, PAYLOAD_BYTES); - f.len = PAYLOAD_BYTES; + memcpy(f.data, frame_data, len); + f.len = len; if (xQueueSend(bt_queue, &f, 0) != pdTRUE) { ESP_LOGW(TAG, "BT queue full"); } } -static void print_frame(void) { +// Hardware callback to print frames to ESP Log console +static void print_frame_hw(uint32_t frames_decoded, const uint8_t *frame_data) { char hex_str[ PAYLOAD_BYTES * 3 + 1 ]; int offset = 0; for (uint8_t i = 0; i < PAYLOAD_BYTES; i++) { - offset += sprintf(hex_str + offset, "%02X ", ctx.frame[i]); + offset += sprintf(hex_str + offset, "%02X ", frame_data[i]); } - ESP_LOGI(TAG, "[FRAME #%lu] %s", ctx.frames_decoded, hex_str); -} - -static void feed_bit(uint8_t pc) { - switch (ctx.state) { - case DS_HUNT_SYNC: - if (pc == PC_LOGIC_1) { - ctx.sync_count++; - if (ctx.sync_count >= SYNC_ONES_NEEDED) { - ctx.sync_count = 0; - ctx.byte_count = 0; - ctx.bit_count = 0; - ctx.separator_count = 0; - ctx.frame_errors = 0; - ctx.bytes_this_frame = 0; - ctx.state = DS_AWAIT_START; - } - } else { - ctx.sync_count = 0; - } - break; - - case DS_AWAIT_START: - if (pc == PC_LOGIC_0) { - ctx.current_byte = 0; - ctx.bit_count = 0; - ctx.separator_count = 0; - ctx.state = DS_READ_BITS; - } else { - ctx.separator_count++; - if (ctx.separator_count > MAX_SEPARATORS) { - reset_decoder(); - } - } - break; - - case DS_READ_BITS: { - uint8_t bit_val = (pc == PC_LOGIC_1) ? 1u : 0u; - ctx.current_byte = (uint8_t)((ctx.current_byte << 1) | bit_val); - ctx.bit_count++; - - if (ctx.bit_count == 8) { - ctx.frame[ctx.byte_count] = ctx.current_byte; - ctx.bytes_this_frame++; - ctx.bit_count = 0; - ctx.byte_count++; - - if (ctx.byte_count >= PAYLOAD_BYTES) { - ctx.frames_decoded++; - print_frame(); - enqueue_frame(); - reset_decoder(); - } else { - ctx.separator_count = 0; - ctx.state = DS_AWAIT_START; - } - } - break; - } - default: - reset_decoder(); - break; - } -} - -static void process_pulse(uint32_t pulse_us) { - uint8_t pc = classify_pulse(pulse_us); - if (pc == PC_GLITCH) return; - - if (pc == PC_IDLE_GAP) { - reset_decoder(); - return; - } - - if (pc == PC_MERGED) { - uint32_t hidden_est = pulse_us - LOGIC1_PULSE_US; - uint8_t hidden_bit = (hidden_est >= THRESHOLD_US) ? PC_LOGIC_1 : PC_LOGIC_0; - feed_bit(hidden_bit); - feed_bit(PC_LOGIC_1); - return; - } - - feed_bit(pc); + ESP_LOGI(TAG, "[FRAME #%lu] %s", (unsigned long)frames_decoded, hex_str); } static void btTransmitTask(void* pvParameters) { @@ -246,8 +91,8 @@ static void aldlDecodeTask(void* pvParameters) { uint32_t pulse_us = 0; for (;;) { bool did_work = false; - while (rb_pop(&pulse_us)) { - process_pulse(pulse_us); + while (rb_pop(&rb, &pulse_us)) { + process_pulse(&ctx, pulse_us); did_work = true; } if (!did_work) vTaskDelay(1); @@ -258,7 +103,7 @@ static void statusTask(void* pvParameters) { for (;;) { vTaskDelay(pdMS_TO_TICKS(5000)); ESP_LOGI(TAG, "[STATUS] frames=%lu bt=%s", - ctx.frames_decoded, + (unsigned long)ctx.frames_decoded, bt_connected ? "UP" : "waiting"); } } @@ -305,7 +150,6 @@ static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { case ESP_SPP_SRV_STOP_EVT: ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT"); break; - default: break; } @@ -360,12 +204,16 @@ void app_main(void) { ESP_LOGI(TAG, "============================================"); ESP_LOGI(TAG, " ESP32 ALDL Bridge — GM 1227170 Fiero 2.8 "); + ESP_LOGI(TAG, " Version: %s", PROJECT_VER); ESP_LOGI(TAG, " 160-baud PWM — AA55 Hard Sync Active "); ESP_LOGI(TAG, "============================================"); memset(&rb, 0, sizeof(rb)); memset(&ctx, 0, sizeof(ctx)); - ctx.state = DS_HUNT_SYNC; + + // Initialize the decoder with hardware callbacks + decoder_init(enqueue_frame_hw, print_frame_hw); + reset_decoder(&ctx); gpio_config_t io = {}; io.intr_type = GPIO_INTR_ANYEDGE; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..d6c3add --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) +project(test_decoder C) + +set(CMAKE_C_STANDARD 99) + +# Include decoder headers from main +include_directories(../main) + +# Add compilation target linking tests and decoder sources +add_executable(test_decoder test_decoder.c ../main/decoder.c) diff --git a/tests/test_decoder.c b/tests/test_decoder.c new file mode 100644 index 0000000..019ad93 --- /dev/null +++ b/tests/test_decoder.c @@ -0,0 +1,281 @@ +#include +#include +#include +#include +#include +#include "decoder.h" + +// Simple testing harness definitions +static int tests_run = 0; +static int tests_failed = 0; + +#define RUN_TEST(test) do { \ + printf("Running %s...\n", #test); \ + tests_run++; \ + int failed_before = tests_failed; \ + test(); \ + if (tests_failed == failed_before) { \ + printf(" -> %s passed.\n", #test); \ + } else { \ + printf(" -> %s FAILED.\n", #test); \ + } \ +} while (0) + +#define ASSERT_TRUE(cond, msg) do { \ + if (!(cond)) { \ + printf(" [FAIL] Line %d: %s (condition: %s)\n", __LINE__, msg, #cond); \ + tests_failed++; \ + return; \ + } \ +} while(0) + +#define ASSERT_INT_EQ(expected, actual, msg) do { \ + if ((expected) != (actual)) { \ + printf(" [FAIL] Line %d: %s (expected %d, got %d)\n", __LINE__, msg, (int)(expected), (int)(actual)); \ + tests_failed++; \ + return; \ + } \ +} while(0) + +// Globals to track frame outputs from decoder callbacks +static uint8_t last_enqueued_frame[PAYLOAD_BYTES]; +static uint8_t last_enqueued_len = 0; +static int enqueue_count = 0; +static int print_count = 0; + +static void mock_enqueue_frame(const uint8_t *frame_data, uint8_t len) { + memcpy(last_enqueued_frame, frame_data, len); + last_enqueued_len = len; + enqueue_count++; +} + +static void mock_print_frame(uint32_t frames_decoded, const uint8_t *frame_data) { + print_count++; +} + +// --------------------------------------------------------------------------- +// ── TEST CASES ───────────────────────────────────────────────────────────── +// --------------------------------------------------------------------------- + +static void test_ring_buffer(void) { + struct RingBuffer rb; + memset(&rb, 0, sizeof(rb)); + + uint32_t out = 0; + // Empty buffer pop should return false + ASSERT_TRUE(!rb_pop(&rb, &out), "Pop on empty ring buffer should return false"); + + // Push and pop single value + rb_push(&rb, 12345u); + ASSERT_TRUE(rb_pop(&rb, &out), "Pop on non-empty ring buffer should return true"); + ASSERT_INT_EQ(12345u, out, "Popped value should match pushed value"); + ASSERT_TRUE(!rb_pop(&rb, &out), "Ring buffer should be empty after single pop"); + + // Push multiple and verify FIFO order + rb_push(&rb, 10u); + rb_push(&rb, 20u); + rb_push(&rb, 30u); + + ASSERT_TRUE(rb_pop(&rb, &out), "Pop 1"); + ASSERT_INT_EQ(10u, out, "Value 1"); + ASSERT_TRUE(rb_pop(&rb, &out), "Pop 2"); + ASSERT_INT_EQ(20u, out, "Value 2"); + ASSERT_TRUE(rb_pop(&rb, &out), "Pop 3"); + ASSERT_INT_EQ(30u, out, "Value 3"); + ASSERT_TRUE(!rb_pop(&rb, &out), "Empty check"); + + // Test buffer capacity/limit + // RB_MASK is 255 (size 256). We can hold at most 255 items before next == tail. + for (uint32_t i = 0; i < 300; i++) { + rb_push(&rb, i); + } + // Buffer head should have stopped advancing when it hit tail - 1. + // Let's verify that we can pop elements without infinite loop. + int count = 0; + while (rb_pop(&rb, &out)) { + count++; + } + ASSERT_TRUE(count <= 255, "Buffer must drop elements on overflow instead of corrupting pointers"); +} + +static void test_classify_pulse(void) { + // MIN_VALID_US = 300 + // THRESHOLD_US = 2639 + // MERGE_THRESHOLD_US = 8000 + // MAX_VALID_US = 13500 + + ASSERT_INT_EQ(PC_GLITCH, classify_pulse(100), "Less than MIN_VALID_US is glitch"); + ASSERT_INT_EQ(PC_LOGIC_0, classify_pulse(1000), "Typical logical 0 (1.11ms) is logic 0"); + ASSERT_INT_EQ(PC_LOGIC_1, classify_pulse(4000), "Typical logical 1 (4.16ms) is logic 1"); + ASSERT_INT_EQ(PC_MERGED, classify_pulse(10000), "Between MERGE_THRESHOLD_US and MAX_VALID_US is merged"); + ASSERT_INT_EQ(PC_IDLE_GAP, classify_pulse(15000), "Greater than MAX_VALID_US is idle gap"); +} + +static void test_reset_decoder(void) { + struct DecoderContext ctx; + ctx.state = DS_READ_BITS; + ctx.sync_count = 5; + ctx.bit_count = 3; + ctx.current_byte = 0xAA; + ctx.byte_count = 10; + ctx.separator_count = 2; + ctx.frame_errors = 4; + ctx.frames_decoded = 42; // Frames decoded should NOT be reset + + reset_decoder(&ctx); + + ASSERT_INT_EQ(DS_HUNT_SYNC, ctx.state, "Reset state should be DS_HUNT_SYNC"); + ASSERT_INT_EQ(0, ctx.sync_count, "sync_count reset"); + ASSERT_INT_EQ(0, ctx.bit_count, "bit_count reset"); + ASSERT_INT_EQ(0, ctx.byte_count, "byte_count reset"); + ASSERT_INT_EQ(0, ctx.separator_count, "separator_count reset"); + ASSERT_INT_EQ(0, ctx.frame_errors, "frame_errors reset"); + ASSERT_INT_EQ(42, ctx.frames_decoded, "frames_decoded should persist across reset"); +} + +static void test_sync_hunting(void) { + struct DecoderContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.state = DS_HUNT_SYNC; + + // Feed logical 0, should stay in HUNT + process_pulse(&ctx, 1111); + ASSERT_INT_EQ(DS_HUNT_SYNC, ctx.state, "Logical 0 does not trigger sync"); + + // Feed 7 logical 1s, should stay in HUNT + for (int i = 0; i < 7; i++) { + process_pulse(&ctx, 4167); + } + ASSERT_INT_EQ(DS_HUNT_SYNC, ctx.state, "7 logic 1s are not enough for sync"); + + // Feed 8th logical 1, should change state to DS_AWAIT_START + process_pulse(&ctx, 4167); + ASSERT_INT_EQ(DS_AWAIT_START, ctx.state, "8 logic 1s triggers transition to DS_AWAIT_START"); +} + +// Helper to simulate feeding a bit (0 or 1) to the decoder +static void feed_simulated_bit(struct DecoderContext *ctx, int bit) { + if (bit == 0) { + // Send a logic 0 pulse + process_pulse(ctx, 1111); + } else { + // Send a logic 1 pulse + process_pulse(ctx, 4167); + } +} + +// Helper to send a start bit (0) +static void feed_start_bit(struct DecoderContext *ctx) { + feed_simulated_bit(ctx, 0); +} + +// Helper to send a full byte: start bit + 8 bits +static void feed_byte(struct DecoderContext *ctx, uint8_t byte_val) { + // 1. Send start bit (0) + feed_start_bit(ctx); + + // 2. Send 8 data bits (MSB first) + for (int i = 7; i >= 0; i--) { + int bit = (byte_val >> i) & 1; + feed_simulated_bit(ctx, bit); + } +} + +static void test_decode_frame(void) { + struct DecoderContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.state = DS_HUNT_SYNC; + + enqueue_count = 0; + print_count = 0; + memset(last_enqueued_frame, 0, sizeof(last_enqueued_frame)); + + // 1. Sync + for (int i = 0; i < 8; i++) { + process_pulse(&ctx, 4167); + } + ASSERT_INT_EQ(DS_AWAIT_START, ctx.state, "Sync lock established"); + + // 2. Feed 25 distinct bytes (e.g. 0x01, 0x02, ..., 0x19) + for (uint8_t val = 1; val <= 25; val++) { + feed_byte(&ctx, val); + } + + // 3. Verify frame enqueued and callbacks triggered + ASSERT_INT_EQ(1, enqueue_count, "One frame should be enqueued"); + ASSERT_INT_EQ(1, print_count, "One frame should be printed"); + ASSERT_INT_EQ(PAYLOAD_BYTES, last_enqueued_len, "Payload size should be 25 bytes"); + + // 4. Verify contents + for (uint8_t i = 0; i < 25; i++) { + ASSERT_INT_EQ(i + 1, last_enqueued_frame[i], "Decoded data byte mismatch"); + } + + // 5. Decoder should have reset back to HUNT state + ASSERT_INT_EQ(DS_HUNT_SYNC, ctx.state, "Decoder should reset back to DS_HUNT_SYNC after full frame"); +} + +static void test_merged_pulse(void) { + struct DecoderContext ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.state = DS_HUNT_SYNC; + + enqueue_count = 0; + + // 1. Sync + for (int i = 0; i < 8; i++) { + process_pulse(&ctx, 4167); + } + + // 2. Feed first byte up to bit 6 + feed_start_bit(&ctx); + // Send 6 logical 0 bits + for (int i = 0; i < 6; i++) { + feed_simulated_bit(&ctx, 0); + } + + // At this point: bit_count = 6 + // We want the next pulses to represent bit 7 and bit 8. + // Instead of two separate pulses, we feed a merged pulse representing: + // a logical 1 (4167us) followed directly by a logical 1 (4167us) without a transition. + // Total merged pulse duration: 4167 + 4167 = 8334us. + // Let's pass 9000us which should classify as PC_MERGED (> 8000us). + // Hidden estimation: us - LOGIC1_PULSE_US = 9000 - 4167 = 4833us. + // 4833us >= THRESHOLD_US (2639) -> classifies as hidden logical 1, followed by logical 1. + process_pulse(&ctx, 9000); + + // This single process_pulse should have pushed two bits (1 then 1), + // completing the 8 data bits of the first byte! + ASSERT_INT_EQ(0, ctx.bit_count, "Byte should be completed by the merged pulse"); + ASSERT_INT_EQ(1, ctx.byte_count, "Byte count should increment to 1"); + + // The byte assembled should be: start (0), then six 0s, then two 1s. + // Value = 0b00000011 = 0x03. + ASSERT_INT_EQ(0x03, ctx.frame[0], "Merged pulse decoded byte mismatch"); +} + +// --------------------------------------------------------------------------- +// ── MAIN ENTRY ───────────────────────────────────────────────────────────── +// --------------------------------------------------------------------------- + +int main(void) { + printf("==========================================\n"); + printf(" Starting ALDL Host Decoder Unit Tests \n"); + printf("==========================================\n"); + + // Bind callbacks + decoder_init(mock_enqueue_frame, mock_print_frame); + + RUN_TEST(test_ring_buffer); + RUN_TEST(test_classify_pulse); + RUN_TEST(test_reset_decoder); + RUN_TEST(test_sync_hunting); + RUN_TEST(test_decode_frame); + RUN_TEST(test_merged_pulse); + + printf("\n==========================================\n"); + printf(" Test Summary: %d run, %d failed.\n", tests_run, tests_failed); + printf("==========================================\n"); + + return (tests_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +}