18 Commits

Author SHA1 Message Date
gronod 918ec047ee Remove branch trigger, add jq, use tag-based versioning and cross-platform release upload
Build and Package Firmware / build (push) Successful in 4m51s
2026-06-14 18:32:37 +01:00
gronod 16cbe1207d Update build workflow to use environment variable for TAG across steps
Build and Package Firmware / build (push) Successful in 4m14s
2026-06-14 18:27:33 +01:00
gronod f5249b8ed2 CI: Tag-only release workflow with cross-platform curl upload
Build and Package Firmware / build (push) Successful in 2m37s
2026-06-14 15:05:26 +01:00
gronod fb815e3d39 Update .gitea/workflows/build.yml
Build and Package Firmware / build (push) Successful in 2m44s
2026-06-14 10:07:14 +01:00
gronod 447c172b30 Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 2m50s
2026-06-14 09:46:38 +01:00
gronod c188969182 Update .gitea/workflows/build.yml
Build and Package Firmware / build (push) Has been cancelled
2026-06-14 09:45:17 +01:00
gronod f6a1b91d73 Add .gitea/workflows/build.yml
Build and Package Firmware / build (push) Successful in 2m40s
2026-06-14 09:38:33 +01:00
gronod 05b8a77205 Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 3m0s
2026-06-14 09:32:55 +01:00
gronod 44a1fe8d79 Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 7s
2026-06-14 09:26:17 +01:00
gronod 0573c39010 Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 21s
2026-06-14 09:19:54 +01:00
gronod f32d8ee9b1 Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 23s
2026-06-14 09:16:15 +01:00
gronod 010c7df01f Update .github/workflows/build.yml
Build and Package Firmware / build (push) Failing after 10s
2026-06-14 09:11:38 +01:00
gronod 8fd11e50ab Replace .github/workflows/build.yml (#2)
Reviewed-on: #2
2026-06-14 09:07:47 +01:00
gronod 698c4e40ae Delete .github/workflows/build.yml (#1)
Build and Package Firmware / build (push) Failing after 11s
Reviewed-on: #1
2026-06-14 09:02:04 +01:00
gronod 1fd4ebf371 Add LICENSE.md
Build and Package Firmware / build (push) Failing after 10s
2026-06-14 08:45:27 +01:00
gronod bd4c176ca9 fix: update workflow container image to espressif/idf:v5.2.1
Build and Package Firmware / build (push) Failing after 3m46s
2026-06-12 15:46:52 +01:00
gronod 98d35a82e1 feat: add decoder module, host unit tests, and GitHub Actions CI pipeline
Build and Package Firmware / build (push) Failing after 2m0s
2026-06-12 14:04:58 +01:00
gronod f5ec189dc4 refactor: migrate firmware from Arduino to native ESP-IDF implementation with CMake configuration 2026-06-12 13:48:35 +01:00
14 changed files with 1069 additions and 333 deletions
+103
View File
@@ -0,0 +1,103 @@
name: Build and Package Firmware
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
container: espressif/idf:v5.2.1
permissions:
contents: write
steps:
- name: Install Modern Node.js and jq (Gitea Compatibility)
run: apt-get update && apt-get install -y ca-certificates curl gnupg jq && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure Safe Directory
run: git config --global --add safe.directory "*"
- name: Set Version Identifier
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "TAG=$TAG" >> $GITHUB_ENV
echo "$TAG" > version.txt
echo "Build version (tag): $TAG"
- name: Build and Run Host Unit Tests
run: . $IDF_PATH/export.sh && 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: |
echo "Packaging with TAG: $TAG"
tar -czvf "esp32-aldl-${TAG}.tar.gz" -C dist .
- name: Create Release and Upload Asset
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
API_URL: ${{ github.api_url }}
REPO: ${{ github.repository }}
TAG: ${{ env.TAG }}
run: |
FILE="esp32-aldl-${TAG}.tar.gz"
NAME="${TAG}"
echo "Creating release with FILE: $FILE, NAME: $NAME"
# 1. Create the release
RESP=$(curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$NAME\",\"body\":\"Release $TAG\"}" \
"$API_URL/repos/$REPO/releases")
# 2. Extract upload endpoint
UPLOAD_URL=$(echo "$RESP" | jq -r '.upload_url // empty')
RELEASE_ID=$(echo "$RESP" | jq -r '.id // empty')
# 3. Upload asset
if [ -n "$UPLOAD_URL" ]; then
# GitHub style (upload_url contains {?name,label})
UPLOAD_URL="${UPLOAD_URL%\{?name,label\}}"
curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @$FILE \
"${UPLOAD_URL}?name=${FILE}"
elif [ -n "$RELEASE_ID" ]; then
# Gitea style (POST to /releases/{id}/assets)
curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @$FILE \
"$API_URL/repos/$REPO/releases/$RELEASE_ID/assets?name=${FILE}"
else
echo "Failed to create release"
echo "$RESP"
exit 1
fi
+109
View File
@@ -0,0 +1,109 @@
name: Build and Package Firmware
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
container: espressif/idf:v5.2.1
permissions:
contents: write
steps:
- name: Install Modern Node.js and jq (Gitea Compatibility)
run: apt-get update && apt-get install -y ca-certificates curl gnupg jq && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure Safe Directory
run: git config --global --add safe.directory "*"
- name: Set Version Identifier
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "TAG=$TAG" >> $GITHUB_ENV
echo "$TAG" > version.txt
echo "Build version (tag): $TAG"
- name: Build and Run Host Unit Tests
run: . $IDF_PATH/export.sh && 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: |
echo "Packaging with TAG: $TAG"
tar -czvf "esp32-aldl-${TAG}.tar.gz" -C dist .
- name: Upload Firmware Package
uses: actions/upload-artifact@v4
with:
name: esp32-aldl-firmware-${{ github.sha }}
path: esp32-aldl-*.tar.gz
if-no-files-found: error
- name: Create Release and Upload Asset
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
API_URL: ${{ github.api_url }}
REPO: ${{ github.repository }}
TAG: ${{ env.TAG }}
run: |
FILE="esp32-aldl-${TAG}.tar.gz"
NAME="${TAG}"
echo "Creating release with FILE: $FILE, NAME: $NAME"
# 1. Create the release
RESP=$(curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$NAME\",\"body\":\"Release $TAG\"}" \
"$API_URL/repos/$REPO/releases")
# 2. Extract upload endpoint
UPLOAD_URL=$(echo "$RESP" | jq -r '.upload_url // empty')
RELEASE_ID=$(echo "$RESP" | jq -r '.id // empty')
# 3. Upload asset
if [ -n "$UPLOAD_URL" ]; then
# GitHub style (upload_url contains {?name,label})
UPLOAD_URL="${UPLOAD_URL%\{?name,label\}}"
curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @$FILE \
"${UPLOAD_URL}?name=${FILE}"
elif [ -n "$RELEASE_ID" ]; then
# Gitea style (POST to /releases/{id}/assets)
curl -s -H "Authorization: token $TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @$FILE \
"$API_URL/repos/$REPO/releases/$RELEASE_ID/assets?name=${FILE}"
else
echo "Failed to create release"
echo "$RESP"
exit 1
fi
+3
View File
@@ -0,0 +1,3 @@
build/
sdkconfig
sdkconfig.old
+3
View File
@@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32-aldl)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright Gordon Bolton (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+95 -21
View File
@@ -1,20 +1,36 @@
# ESP32 ALDL Wireless Bridge
**ESP32 firmware for reading GM 160-baud PWM ALDL data from a 19861988 Pontiac Fiero 2.8L V6 (1227170 ECM) and streaming it wirelessly over Bluetooth SPP.**
**ESP32 native C firmware using ESP-IDF for reading GM 160-baud PWM ALDL data from a 19861988 Pontiac Fiero 2.8L V6 (1227170 ECM) and streaming it wirelessly over Bluetooth SPP.**
This project turns an ESP32 into a reliable, low-latency wireless ALDL interface. It decodes the raw 160-baud PWM signal in real time and forwards clean 25-byte data frames over Bluetooth to the companion Android app ([esp32-aldl-android](https://git.i3omb.com/gronod/esp32-aldl-android)).
It is implemented as a native ESP-IDF application in C, removing the overhead of the Arduino framework.
---
## Features
- **Real-time 160-baud PWM ALDL decoding** — Custom bit-banged decoder running in a high-priority FreeRTOS task
- **Native C ESP-IDF Implementation** — Built directly on ESP-IDF v5.2+ for maximum performance and low memory footprint
- **Real-time 160-baud PWM ALDL decoding** — Custom bit-banged decoder running in a high-priority FreeRTOS task (`aldlDecodeTask`)
- **Robust pulse handling** — Handles glitches, merged pulses, and idle gaps gracefully
- **Hard frame synchronization** — Prepends `0xAA 0x55` header for reliable packet alignment on the receiving side
- **Bluetooth SPP (Serial Port Profile)** — Appears as a classic Bluetooth serial device named `ESP32-ALDL`
- **Decoupled architecture** — ISR Ring buffer Decoder task Bluetooth transmit queue
- **Status reporting** — Periodic status output over USB serial
- **Low CPU usage** on the decoder core while maintaining timing accuracy
- **Bluedroid Bluetooth Classic SPP** — Connects directly via Serial Port Profile (SPP) using "Just Works" Secure Simple Pairing (SSP) configuration
- **Decoupled RTOS Architecture** — ISR -> Ring Buffer -> Decoder task -> Bluetooth transmit queue -> BT TX task
- **Periodic status reporting** — Diagnostics reported over ESP-IDF log outputs (`ESP_LOGI`)
---
## Project Structure
```
esp32-aldl/
├── CMakeLists.txt # Top-level CMake build configuration
├── sdkconfig.defaults # Kconfig options (enabling BT, SPP, HZ=1000)
├── README.md # Project documentation
└── main/
├── CMakeLists.txt # Main component build script
└── main.c # C application logic (decoder and Bluetooth SPP)
```
---
@@ -22,34 +38,92 @@ This project turns an ESP32 into a reliable, low-latency wireless ALDL interface
- **ECM**: GM 1227170 (19861988 Pontiac Fiero 2.8L V6)
- **ALDL Mode**: `$24` / `$24A` mask (25-byte continuous broadcast frames)
- **Microcontroller**: ESP32 (any dev board with sufficient GPIO — tested on ESP32-WROOM-32 and ESP32-S3)
- **Bluetooth**: Classic Bluetooth (BR/EDR) — works with most Android devices
- **Microcontroller**: ESP32 (Classic ESP32 with Classic Bluetooth capability)
- **Bluetooth**: Classic Bluetooth (BR/EDR)
---
## Pin Connections
| Signal | ESP32 Pin | Notes |
|--------------|---------------|--------------------------------------------|
| ALDL Data In | **GPIO 4** | Connect to ALDL pin **M** (data line) |
| GND | GND | Must share ground with the car/ECU |
| Signal | ESP32 Pin | Notes |
|--------------|---------------|-----------------------------------------------|
| ALDL Data In | **GPIO 4** | Connect to ALDL pin **M** (data line) |
| GND | GND | Must share ground with the car/ECU |
| 5V / 3.3V | — | **Do not** power the ESP32 from the ALDL line |
> **Important**: The ALDL line is a 5V PWM signal. The ESP32 GPIO is 3.3V tolerant for input in most cases, but a simple voltage divider or level shifter (e.g., 1kΩ + 2kΩ) is recommended for long-term reliability.
> [!WARNING]
> The ALDL line is a 5V PWM signal. Since ESP32 GPIO pins are not 5V-tolerant, a simple voltage divider (e.g., 1kΩ + 2kΩ) or a bidirectional level shifter is required to step down the signal to 3.3V for long-term hardware reliability.
---
## Prerequisites & Installation
To build and compile the firmware, you need the official **ESP-IDF v5.2.1** toolchain installed.
1. Clone ESP-IDF (v5.2.1 stable release):
```bash
mkdir -p ~/esp
git clone -b v5.2.1 --recursive https://github.com/espressif/esp-idf.git ~/esp/esp-idf
```
2. Run the toolchain installation:
```bash
cd ~/esp/esp-idf
./install.sh esp32
```
3. Initialize the environment:
```bash
source ~/esp/esp-idf/export.sh
```
4. *(Optional)* If `cmake` or `ninja` are missing in your environment, install them inside the active Python virtual environment:
```bash
pip install cmake ninja
```
---
## Building and Flashing
Once the toolchain environment is loaded, execute the following commands in the `esp32-aldl` root workspace folder:
### 1. Set Build Target
```bash
idf.py set-target esp32
```
This command generates the build directories and loads settings from `sdkconfig.defaults` (enabling Bluetooth SPP and setting the FreeRTOS clock frequency to 1000 Hz).
### 2. Compile Project
```bash
idf.py build
```
This builds the bootloader, partition table, ESP-IDF drivers, Bluetooth stack, and the main application code, producing `build/esp32-aldl.bin`.
### 3. Flash & Monitor
Flash the firmware to the ESP32 and open the terminal console to view serial output (replace `/dev/ttyUSB0` with your target serial port):
```bash
idf.py -p /dev/ttyUSB0 flash monitor
```
To exit the serial monitor, press `Ctrl + ]`.
---
## How It Works
### Signal Decoding
The firmware uses an interrupt service routine (ISR) triggered on any edge of GPIO 4. Pulse widths are measured using the ESP32 high-resolution timer and pushed into a lock-free ring buffer.
The firmware registers an Interrupt Service Routine (ISR) triggered on any edge (both rising and falling) of **GPIO 4**. Pulse widths are calculated in microseconds using `esp_timer_get_time()` and pushed to a volatile ring buffer.
A dedicated decoder task (`aldlDecodeTask`) processes the pulse stream:
- Classifies pulses as `0`, `1`, glitch, merged pulse, or idle gap
- Reconstructs bytes using the standard 160-baud ALDL PWM encoding
- Assembles complete 25-byte frames
- Validates and enqueues valid frames
The high-priority `aldlDecodeTask` pops pulses from the ring buffer:
- Glitches under `300us` are discarded.
- Idle gaps over `13500us` reset the decoder.
- Pulses are classified into Logical `0` (approx. `1.11ms`), Logical `1` (approx. `4.16ms`), or Merged pulses.
- Decoded bits are reconstructed into 25-byte frames.
### Bluetooth Transmission
A separate task (`btTransmitTask`) pulls decoded frames from a FreeRTOS queue and transmits them over Bluetooth SPP with a 2-byte hard sync header:
When a 25-byte frame is fully received, it is pushed to `bt_queue`.
The `btTransmitTask` waits on the queue:
- Prepend the 2-byte hard-sync header (`0xAA 0x55`).
- Sends the packet (`27 bytes` total) over the Bluetooth Classic Serial Port Profile connection via the `esp_spp_write()` API.
- The device advertises itself under the classic Bluetooth name `ESP32-ALDL`.
-312
View File
@@ -1,312 +0,0 @@
/**
* =============================================================================
* ESP32 ALDL Wireless Bridge — GM 1227170 / Fiero 2.8L V6
* 160-baud PWM ALDL decoder + BluetoothSerial bridge
*
* REVISION 7 — 0xAA 0x55 Hard Sync Header
* =============================================================================
*/
#include <Arduino.h>
#include <BluetoothSerial.h>
#include "esp_timer.h"
#include "driver/gpio.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 DEBUG_LEVEL 2
#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;
};
static RingBuffer rb;
static DecoderContext ctx;
static BluetoothSerial SerialBT;
static QueueHandle_t bt_queue = nullptr;
#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 inline uint16_t rb_available() {
return (rb.head - rb.tail) & RB_MASK;
}
static volatile uint64_t isr_fall_us = 0;
static void IRAM_ATTR aldl_gpio_isr(void* /*arg*/) {
uint64_t now = (uint64_t)esp_timer_get_time();
if (gpio_get_level((gpio_num_t)ALDL_PIN) == 0) {
isr_fall_us = now;
} else {
if (isr_fall_us != 0) {
rb_push((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() {
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() {
BtFrame f;
memcpy(f.data, ctx.frame, PAYLOAD_BYTES);
f.len = PAYLOAD_BYTES;
if (xQueueSend(bt_queue, &f, 0) != pdTRUE) {
if (DEBUG_LEVEL >= 1) Serial.println(F("[WARN] BT queue full"));
}
}
static void print_frame() {
Serial.print(F("[FRAME #"));
Serial.print(ctx.frames_decoded);
Serial.print(F("] "));
for (uint8_t i = 0; i < PAYLOAD_BYTES; i++) {
if (ctx.frame[i] < 0x10) Serial.print('0');
Serial.print(ctx.frame[i], HEX);
if (i < PAYLOAD_BYTES - 1) Serial.print(' ');
}
Serial.println();
}
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++;
if (DEBUG_LEVEL >= 1) 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);
}
// ---------------------------------------------------------------------------
// ── BT transmit task (Core 0) ────────────────────────────────────────────────
// ---------------------------------------------------------------------------
static void btTransmitTask(void* /*pvParameters*/) {
BtFrame f;
// The 2-byte hard-sync header that ALDLDroid will lock onto
uint8_t tx_buffer[PAYLOAD_BYTES + 2];
tx_buffer[0] = 0xAA;
tx_buffer[1] = 0x55;
for (;;) {
if (xQueueReceive(bt_queue, &f, portMAX_DELAY) == pdTRUE) {
if (SerialBT.connected()) {
// Copy the 25 decoded bytes immediately after the header
memcpy(&tx_buffer[2], f.data, f.len);
// Transmit the 27-byte locked packet
SerialBT.write(tx_buffer, f.len + 2);
}
}
}
}
static void aldlDecodeTask(void* /*pvParameters*/) {
uint32_t pulse_us = 0;
for (;;) {
bool did_work = false;
while (rb_pop(pulse_us)) {
process_pulse(pulse_us);
did_work = true;
}
if (!did_work) vTaskDelay(1);
}
}
static void statusTask(void* /*pvParameters*/) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(5000));
Serial.print(F("[STATUS] frames="));
Serial.print(ctx.frames_decoded);
Serial.print(F(" bt="));
Serial.println(SerialBT.connected() ? F("UP") : F("waiting"));
}
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println(F("============================================"));
Serial.println(F(" ESP32 ALDL Bridge — GM 1227170 Fiero 2.8 "));
Serial.println(F(" 160-baud PWM — AA55 Hard Sync Active "));
Serial.println(F("============================================"));
memset(&rb, 0, sizeof(rb));
memset(&ctx, 0, sizeof(ctx));
ctx.state = DS_HUNT_SYNC;
gpio_config_t io = {};
io.intr_type = GPIO_INTR_ANYEDGE;
io.mode = GPIO_MODE_INPUT;
io.pin_bit_mask = (1ULL << ALDL_PIN);
io.pull_down_en = GPIO_PULLDOWN_DISABLE;
io.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io);
gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);
gpio_isr_handler_add((gpio_num_t)ALDL_PIN, aldl_gpio_isr, nullptr);
if (!SerialBT.begin(BT_DEVICE_NAME)) {
for (;;) delay(1000);
}
Serial.print(F("[BT] Advertising as: "));
Serial.println(F(BT_DEVICE_NAME));
bt_queue = xQueueCreate(BT_QUEUE_DEPTH, sizeof(BtFrame));
xTaskCreatePinnedToCore(aldlDecodeTask, "aldlDecode", 4096, nullptr, 3, nullptr, 0);
xTaskCreatePinnedToCore(btTransmitTask, "btTx", 4096, nullptr, 2, nullptr, 0);
xTaskCreatePinnedToCore(statusTask, "status", 2048, nullptr, 1, nullptr, 0);
}
void loop() {
vTaskDelay(pdMS_TO_TICKS(100));
}
+8
View File
@@ -0,0 +1,8 @@
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
+234
View File
@@ -0,0 +1,234 @@
#include <string.h>
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#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 BT_DEVICE_NAME "ESP32-ALDL"
#define BT_QUEUE_DEPTH 4u
#ifndef PROJECT_VER
#define PROJECT_VER "unknown"
#endif
static const char *TAG = "ALDL";
static struct RingBuffer rb;
static struct DecoderContext ctx;
static QueueHandle_t bt_queue = NULL;
static uint32_t spp_handle = 0;
static bool bt_connected = false;
static volatile uint64_t isr_fall_us = 0;
static void IRAM_ATTR aldl_gpio_isr(void* arg) {
uint64_t now = (uint64_t)esp_timer_get_time();
if (gpio_get_level((gpio_num_t)ALDL_PIN) == 0) {
isr_fall_us = now;
} else {
if (isr_fall_us != 0) {
rb_push(&rb, (uint32_t)(now - isr_fall_us));
isr_fall_us = 0;
}
}
}
// 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, frame_data, len);
f.len = len;
if (xQueueSend(bt_queue, &f, 0) != pdTRUE) {
ESP_LOGW(TAG, "BT queue full");
}
}
// 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 ", frame_data[i]);
}
ESP_LOGI(TAG, "[FRAME #%lu] %s", (unsigned long)frames_decoded, hex_str);
}
static void btTransmitTask(void* pvParameters) {
struct BtFrame f;
// The 2-byte hard-sync header that ALDLDroid will lock onto
uint8_t tx_buffer[PAYLOAD_BYTES + 2];
tx_buffer[0] = 0xAA;
tx_buffer[1] = 0x55;
for (;;) {
if (xQueueReceive(bt_queue, &f, portMAX_DELAY) == pdTRUE) {
if (bt_connected && spp_handle != 0) {
// Copy the 25 decoded bytes immediately after the header
memcpy(&tx_buffer[2], f.data, f.len);
// Transmit the 27-byte locked packet
esp_spp_write(spp_handle, f.len + 2, tx_buffer);
}
}
}
}
static void aldlDecodeTask(void* pvParameters) {
uint32_t pulse_us = 0;
for (;;) {
bool did_work = false;
while (rb_pop(&rb, &pulse_us)) {
process_pulse(&ctx, pulse_us);
did_work = true;
}
if (!did_work) vTaskDelay(1);
}
}
static void statusTask(void* pvParameters) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(5000));
ESP_LOGI(TAG, "[STATUS] frames=%lu bt=%s",
(unsigned long)ctx.frames_decoded,
bt_connected ? "UP" : "waiting");
}
}
static void esp_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
switch (event) {
case ESP_SPP_INIT_EVT:
ESP_LOGI(TAG, "ESP_SPP_INIT_EVT");
esp_spp_start_srv(ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, "SPP_SERVER");
break;
case ESP_SPP_DISCOVERY_COMP_EVT:
ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT");
break;
case ESP_SPP_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_OPEN_EVT");
break;
case ESP_SPP_CLOSE_EVT:
ESP_LOGI(TAG, "ESP_SPP_CLOSE_EVT");
spp_handle = 0;
bt_connected = false;
break;
case ESP_SPP_START_EVT:
ESP_LOGI(TAG, "ESP_SPP_START_EVT");
esp_bt_dev_set_device_name(BT_DEVICE_NAME);
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
break;
case ESP_SPP_CL_INIT_EVT:
ESP_LOGI(TAG, "ESP_SPP_CL_INIT_EVT");
break;
case ESP_SPP_DATA_IND_EVT:
ESP_LOGI(TAG, "ESP_SPP_DATA_IND_EVT len=%d, handle=%lu",
param->data_ind.len, (unsigned long)param->data_ind.handle);
break;
case ESP_SPP_CONG_EVT:
ESP_LOGI(TAG, "ESP_SPP_CONG_EVT");
break;
case ESP_SPP_WRITE_EVT:
break;
case ESP_SPP_SRV_OPEN_EVT:
ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT");
spp_handle = param->srv_open.handle;
bt_connected = true;
break;
case ESP_SPP_SRV_STOP_EVT:
ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
break;
default:
break;
}
}
void app_main(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if ((ret = esp_bt_controller_init(&bt_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT)) != ESP_OK) {
ESP_LOGE(TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
if ((ret = esp_bluedroid_init_with_cfg(&bluedroid_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s initialize bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_bluedroid_enable()) != ESP_OK) {
ESP_LOGE(TAG, "%s enable bluedroid failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
if ((ret = esp_spp_register_callback(esp_spp_cb)) != ESP_OK) {
ESP_LOGE(TAG, "%s spp register failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
esp_spp_cfg_t spp_cfg = {
.mode = ESP_SPP_MODE_CB,
.enable_l2cap_ertm = true,
.tx_buffer_size = 0,
};
if ((ret = esp_spp_enhanced_init(&spp_cfg)) != ESP_OK) {
ESP_LOGE(TAG, "%s spp init failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
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));
// 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;
io.mode = GPIO_MODE_INPUT;
io.pin_bit_mask = (1ULL << ALDL_PIN);
io.pull_down_en = GPIO_PULLDOWN_DISABLE;
io.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io);
gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);
gpio_isr_handler_add((gpio_num_t)ALDL_PIN, aldl_gpio_isr, NULL);
bt_queue = xQueueCreate(BT_QUEUE_DEPTH, sizeof(struct BtFrame));
xTaskCreatePinnedToCore(aldlDecodeTask, "aldlDecode", 4096, NULL, 3, NULL, 0);
xTaskCreatePinnedToCore(btTransmitTask, "btTx", 4096, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(statusTask, "status", 2048, NULL, 1, NULL, 0);
}
+4
View File
@@ -0,0 +1,4 @@
CONFIG_BT_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_SPP_ENABLED=y
CONFIG_FREERTOS_HZ=1000
+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;
}