feat: add decoder module, host unit tests, and GitHub Actions CI pipeline
Build and Package Firmware / build (push) Failing after 2m0s

This commit is contained in:
2026-06-12 14:04:58 +01:00
parent f5ec189dc4
commit 98d35a82e1
7 changed files with 587 additions and 174 deletions
+70
View File
@@ -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
+7 -1
View File
@@ -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()
+115
View File
@@ -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);
}
+83
View File
@@ -0,0 +1,83 @@
#ifndef DECODER_H
#define DECODER_H
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#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
+21 -173
View File
@@ -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;
+10
View File
@@ -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)
+281
View File
@@ -0,0 +1,281 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#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;
}