Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 918ec047ee | |||
| 16cbe1207d | |||
| f5249b8ed2 | |||
| fb815e3d39 | |||
| 447c172b30 | |||
| c188969182 | |||
| f6a1b91d73 | |||
| 05b8a77205 | |||
| 44a1fe8d79 | |||
| 0573c39010 | |||
| f32d8ee9b1 | |||
| 010c7df01f | |||
| 8fd11e50ab | |||
| 698c4e40ae | |||
| 1fd4ebf371 | |||
| bd4c176ca9 | |||
| 98d35a82e1 | |||
| f5ec189dc4 |
@@ -1,4 +1,4 @@
|
||||
name: Build and Package Arduino Firmware
|
||||
name: Build and Package Firmware
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,53 +8,56 @@ on:
|
||||
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 "*"
|
||||
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: Install Arduino CLI
|
||||
run: |
|
||||
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||
mv bin/arduino-cli /usr/local/bin/
|
||||
arduino-cli config init
|
||||
arduino-cli config add board_manager.additional_urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
|
||||
arduino-cli core update-index
|
||||
arduino-cli core install esp32:esp32
|
||||
- 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: Compile Arduino Sketch
|
||||
run: |
|
||||
mkdir -p build
|
||||
arduino-cli compile --fqbn esp32:esp32:esp32 --output-dir build .
|
||||
|
||||
- name: Stage Firmware Binaries
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp build/*.bin dist/
|
||||
- name: Build Firmware
|
||||
run: . $IDF_PATH/export.sh && idf.py build
|
||||
|
||||
- name: Create Flashing Instructions Document
|
||||
run: |
|
||||
cat << 'EOF' > dist/README_FLASHING.txt
|
||||
ESP32 ALDL Bridge Arduino Firmware Flash Instructions
|
||||
===================================================
|
||||
mkdir -p dist && cat << 'EOF' > dist/README_FLASHING.txt
|
||||
ESP32 ALDL Bridge Firmware Flash Instructions
|
||||
=============================================
|
||||
|
||||
You can flash the compiled .bin file using esptool.py or any generic ESP32 flashing tool.
|
||||
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: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo "Packaging with TAG: $TAG"
|
||||
tar -czvf "esp32-aldl-${TAG}.tar.gz" -C dist .
|
||||
|
||||
- name: Create Release and Upload Asset
|
||||
@@ -62,10 +65,11 @@ jobs:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
API_URL: ${{ github.api_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ env.TAG }}
|
||||
run: |
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
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" \
|
||||
@@ -96,4 +100,4 @@ jobs:
|
||||
echo "$RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -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
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
build/
|
||||
sdkconfig
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -0,0 +1,3 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(esp32-aldl)
|
||||
@@ -1,20 +1,36 @@
|
||||
# ESP32 ALDL Wireless Bridge
|
||||
|
||||
**ESP32 firmware for reading GM 160-baud PWM ALDL data from a 1986–1988 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 1986–1988 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 (1986–1988 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
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_CLASSIC_ENABLED=y
|
||||
CONFIG_BT_SPP_ENABLED=y
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user