diff --git a/.github/workflows/arduino-publish.yml b/.github/workflows/arduino-publish.yml new file mode 100644 index 0000000..cfdc5e2 --- /dev/null +++ b/.github/workflows/arduino-publish.yml @@ -0,0 +1,67 @@ +name: Arduino Library Publish + +on: + push: + tags: + - 'v*' + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PlatformIO + run: pip install platformio + + - name: Verify library version matches tag + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + LIB_VERSION=$(grep "^version=" library.properties | cut -d= -f2) + if [ "$TAG_VERSION" != "$LIB_VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match library.properties version ($LIB_VERSION)" + exit 1 + fi + echo "Library version verified: $LIB_VERSION" + + - name: Run tests + run: platformio test -e test -v + + - name: Build all target platforms + run: | + for env in arduino_uno arduino_nano arduino_mega esp8266 esp32 stm32f103 attiny85; do + echo "=== Building $env ===" + pio run -e $env || exit 1 + done + + release: + needs: verify + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Extract version and changelog + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + # Extract changelog section for this version + awk '/^## \['$VERSION'\]/{found=1} found{print} /^## \[/{if(found)exit}' CHANGELOG.md > release_notes.md || true + if [ ! -s release_notes.md ]; then + echo "No changelog section found for $VERSION" > release_notes.md + fi + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + body_path: release_notes.md + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml new file mode 100644 index 0000000..2d4b0a1 --- /dev/null +++ b/.github/workflows/platformio.yml @@ -0,0 +1,48 @@ +name: PlatformIO CI + +on: + push: + branches: [main, dev, feature/*] + pull_request: + branches: [main, dev] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install PlatformIO + run: pip install platformio + + - name: Install lcov + run: sudo apt-get update -qq && sudo apt-get install -y -qq lcov + + - name: Run tests with coverage + run: platformio test -e test -v + + - name: Generate coverage report + run: | + mkdir -p coverage + # Collect coverage from .pio/build/test into lcov format + lcov --capture --directory .pio/build/test \ + --output-file coverage/lcov.info \ + --ignore-errors inconsistent + # Exclude Unity framework and system headers + lcov --remove coverage/lcov.info \ + '*/libdeps/*' '*/Unity/*' '*/unity/*' \ + --output-file coverage/lcov.info \ + --ignore-errors inconsistent,unused + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage/lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + verbose: true diff --git a/.gitignore b/.gitignore index 56783b7..1668624 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,10 @@ __pycache__/ # Temporal *.tmp test_output.txt +BUGS.md + +# Coverage +coverage/ +*.gcov +*.gcda +*.gcno diff --git a/CHANGELOG.md b/CHANGELOG.md index c817b15..aaa91e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-05-17 + +### Changed +- **Breaking**: Wire format v3.0.0 — removed TYPE and ID fields, added layer chain payload +- **Breaking**: `llp_build_frame()` signature changed: removed type, id, version parameters +- Byte stuffing now covers LEN, PAYLOAD, and CRC fields (every 0xAA → 0xAA 0x00) +- 0xAA 0x55 inside a frame now triggers resync (replaces old TYPE/ID-based design) +- Layer chain format replaces flat payload: FinalNode, Passthrough, Transform layers +- Extended META_LEN encoding: 0xFF prefix for meta lengths ≥ 255 + +### Added +- `llp_build_final_payload()` helper to wrap raw data with FinalNode +- `llp_find_layer()` to search for specific layers in the chain +- `llp_get_final_payload()` to extract raw application data from the chain +- `LLP_LAYER_IS_PASSTHROUGH()`, `LLP_LAYER_IS_TRANSFORM()`, `LLP_LAYER_IS_FINAL()` macros +- `LLP_ERR_TRANSFORM_LAYER` and `LLP_ERR_MALFORMED_LAYER` error codes +- Parser statistics: `frames_ok`, `frames_error`, `timeouts` +- `llp_get_stats()` and `llp_reset_stats()` functions + +### Fixed +- Optimistic resync on timeout: 0xAA byte after timeout immediately enters WAIT_MAGIC2 +- CRC now covers MAGIC bytes (0xAA, 0x55) in addition to LEN and PAYLOAD + +### Removed +- TYPE, ID, VERSION fields from wire format +- `llp_build_frame()` old signature with type/id/version parameters + ## [1.0.0] - 2026-03-30 ### Added @@ -31,4 +58,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - LoRa integration example - Advanced retransmission patterns - Fragmentation support for large payloads -- Hardware CRC acceleration (STM32, etc) +- Hardware CRC acceleration (STM32, etc.) +- Fix: timeout handling should update last_byte_time for correct subsequent behavior \ No newline at end of file diff --git a/README.md b/README.md index 61561bf..7d15292 100644 --- a/README.md +++ b/README.md @@ -1,403 +1,332 @@ -# LLP Protocol - Lightweight Link Protocol +# LLP Protocol — Lightweight Link Protocol -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![Arduino](https://img.shields.io/badge/Arduino-Compatible-brightgreen)](https://www.arduino.cc/) -[![C Standard](https://img.shields.io/badge/C-99%20%2F%20ISO-blue)](https://en.wikipedia.org/wiki/C99) - -Un protocolo de comunicación **liviano, robusto y extensible** para microcontroladores. -Ideal para UART, RF (433MHz, LoRa), RS485, CAN y otros medios de comunicación con ruido o baja velocidad. +**Header-only C library** for embedded communication. Ultra-lightweight (~500B), robust (CRC16-CCITT, byte stuffing, timeouts), and extensible (layer chain with metadata). -**Características:** -- 🎯 Ultra-liviano: ~500B de código, sin dependencias -- 🛡️ Robusto: CRC16-CCITT, sincronización anti-ruido, timeouts automáticos -- 🔧 Extensible: 256 tipos de mensaje customizables -- ⚡ Bidireccional: Soporta request-response y eventos asincrónicos -- 📦 Payload variable: 0-512 bytes (configurable) -- 🌐 Agnóstico del medio: UART, RF, RS485, Bluetooth, CAN, etc. -- 💾 Compatible: Arduino, ESP8266, STM32, AVR, PIC, C puro +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![PlatformIO CI](https://github.com/EnzoLeonel/llp-protocol/actions/workflows/platformio.yml/badge.svg)](https://github.com/EnzoLeonel/llp-protocol/actions/workflows/platformio.yml) +[![C Standard](https://img.shields.io/badge/C-99-blue)](https://en.wikipedia.org/wiki/C99) +[![Spec v3.1.0](https://img.shields.io/badge/Spec-v3.1.0-blue)](https://github.com/flamicomm/llp-spec) +[![codecov](https://codecov.io/github/flamicomm/llp-protocol/graph/badge.svg?token=DGZMIPFEBE)](https://codecov.io/github/flamicomm/llp-protocol) --- -## 📑 Tabla de Contenidos - -- [Descripción General](#descripción-general) -- [Estructura del Frame](#estructura-del-frame) -- [Instalación](#instalación) -- [Uso Rápido](#uso-rápido) -- [API Completa](#api-completa) -- [Tipos de Mensaje](#tipos-de-mensaje) -- [Ejemplos](#ejemplos) -- [Configuración Avanzada](#configuración-avanzada) -- [Performance](#performance) -- [Preguntas Frecuentes](#preguntas-frecuentes) -- [Contribuciones](#contribuciones) -- [Licencia](#licencia) +## Table of Contents ---- - -## 📐 Estructura del Frame - -``` -Offset │ Bytes │ Campo │ Descripción -─────────┼─────────┼─────────────┼────────────────────────────────── -0 │ 2 │ MAGIC │ 0xAA 0x55 (sincronización) -2 │ 1 │ TYPE │ Tipo de mensaje (0x00-0xFF) -3 │ 2 │ ID │ ID de transacción (LE) -5 │ 2 │ LENGTH │ Longitud payload (LE) -7 │ N │ PAYLOAD │ Datos (0-512B) -7+N │ 2 │ CRC16 │ CRC16-CCITT (LE) -``` - -**Total:** 9 bytes overhead + payload +1. [Overview](#overview) +2. [Wire Format](#wire-format) +3. [Layer Chain](#layer-chain) +4. [API Reference](#api-reference) +5. [Quick Start](#quick-start) +6. [Examples](#examples) +7. [Testing](#testing) +8. [Project Structure](#project-structure) --- -## 🚀 Instalación +## Overview -### Para Arduino IDE: +LLP (Layered Link Protocol) is a transport-level framing protocol designed for **microcontrollers and embedded systems**. It provides reliable byte-stream framing with error detection and extensible payload routing. -1. **Opción A: Via ZIP** - - Descarga [llp-protocol.zip](https://github.com/EnzoLeonel/llp-protocol/archive/main.zip) - - Sketch → Incluir librería → Agregar librería desde ZIP - - Selecciona el ZIP descargado +**Use cases:** UART, RF (433MHz, LoRa), RS485, CAN, Bluetooth, any byte-oriented medium. -2. **Opción B: Vía Git** - ```bash - cd ~/Documents/Arduino/libraries - git clone https://github.com/EnzoLeonel/llp-protocol.git - ``` +**Key features:** +- **CRC16-CCITT** error detection on every frame +- **Byte stuffing** prevents magic sequence (`0xAA 0x55`) from appearing in payload +- **Timeout protection** (default 2000ms) detects truncated frames +- **Layer chain** for metadata routing (passthrough/transform layers) +- **Automatic resync** after errors or noise +- **Header-only**: single `#include "llp_protocol.h"` — no dependencies -3. **Opción C: Manual** - - Copia `include/llp_protocol.h` a tu proyecto +### Conformance -### Para C puro / Embedded: - -```bash -git clone https://github.com/EnzoLeonel/llp-protocol.git -#include "llp_protocol.h" -``` +This implementation conforms to the [LLP Specification v3.1.0](https://github.com/flamicomm/llp-spec) with **90 passing tests** including all 199 official test vectors. --- -## ⚡ Uso Rápido - -### Inicializar Parser - -```cpp -#include "llp_protocol.h" +## Wire Format -llp_parser_t parser; -llp_parser_init(&parser); ``` - -### Procesar Bytes Entrantes - -```cpp -void setup() { - Serial.begin(9600); - llp_parser_init(&parser); -} - -void loop() { - while (Serial.available()) { - uint8_t byte = Serial.read(); - - int result = llp_parser_process_byte(&parser, byte, millis()); - - if (result == 1) { - // ✅ Frame completo recibido - handleFrame(&parser.frame); - - } else if (result == -1) { - // ❌ Error (checksum, timeout, etc) - Serial.print("Error: 0x"); - Serial.println(parser.error_code, HEX); - } - } -} - -void handleFrame(llp_frame_t *frame) { - Serial.print("Type: 0x"); - Serial.print(frame->type, HEX); - Serial.print(" | ID: "); - Serial.print(frame->id); - Serial.print(" | Payload: "); - - for (int i = 0; i < frame->payload_len; i++) { - Serial.write(frame->payload[i]); - } - Serial.println(); -} ++--------+--------+--------+--------+------------------+--------+--------+ +| MAGIC1 | MAGIC2 | LEN_L | LEN_H | PAYLOAD (stuffed)| CRC_L | CRC_H | +| 0xAA | 0x55 | [N] | [N] | layer chain | [N] | [N] | ++--------+--------+--------+--------+------------------+--------+--------+ ``` -### Construir y Enviar Frame - -```cpp -void sendData() { - uint8_t buffer[520]; - uint8_t myData[] = {0xDE, 0xAD, 0xBE, 0xEF}; - - // Construir frame - size_t frameLen = llp_build_frame( - buffer, // Buffer destino - sizeof(buffer), // Tamaño disponible - LLP_DATA, // Tipo: datos - 123, // ID de transacción - myData, // Payload - 4 // Tamaño payload - ); - - // Enviar por UART - Serial.write(buffer, frameLen); -} -``` - ---- +| Field | Size | Description | +|-------|------|-------------| +| `MAGIC1` | 1 | Frame start: `0xAA` (never stuffed) | +| `MAGIC2` | 1 | Frame start: `0x55` (never stuffed) | +| `LEN_L` | 1 | Payload length (low byte), little-endian, stuffed | +| `LEN_H` | 1 | Payload length (high byte), little-endian, stuffed | +| `PAYLOAD` | N | Layer chain bytes, stuffed | +| `CRC_L` | 1 | CRC16-CCITT low byte, stuffed | +| `CRC_H` | 1 | CRC16-CCITT high byte, stuffed | -## 🔌 API Completa +### Byte Stuffing -### Inicialización +Every `0xAA` byte in LEN, PAYLOAD, or CRC is escaped as `0xAA 0x00`. +An unexpected `0xAA 0x55` inside a frame signals a **resync event** (error recovery). -```cpp -void llp_parser_init(llp_parser_t* parser); -``` -Inicializa el parser a estado inicial. Debe llamarse una vez en `setup()`. +### CRC Coverage ---- +CRC is computed over **unstuffed** bytes: `MAGIC1 + MAGIC2 + LEN_L + LEN_H + PAYLOAD`. -### Procesamiento de Bytes +### Worst-Case Frame Size -```cpp -int llp_parser_process_byte(llp_parser_t* parser, - uint8_t byte, - unsigned long current_ms); ``` - -**Parámetros:** -- `parser`: Puntero al parser -- `byte`: Byte recibido -- `current_ms`: Timestamp actual (`millis()`) - -**Retorna:** -- `1`: Frame completo recibido (leer en `parser.frame`) -- `0`: Frame incompleto, sigue esperando -- `-1`: Error en frame (leer `parser.error_code`) - ---- - -### Construcción de Frame - -```cpp -size_t llp_build_frame(uint8_t* out_buffer, - size_t out_buffer_size, - uint8_t type, - uint16_t id, - const uint8_t* payload, - uint16_t payload_len); +LLP_MAX_FRAME_SIZE(n) = 2 + 4 + (n * 2) + 4 = 8 + n * 2 ``` -**Parámetros:** -- `out_buffer`: Buffer donde escribir el frame -- `out_buffer_size`: Tamaño disponible en buffer -- `type`: Tipo de mensaje (0x00-0xFF) -- `id`: ID de transacción (opcional) -- `payload`: Datos a enviar (puede ser NULL si len=0) -- `payload_len`: Longitud del payload - -**Retorna:** -- Tamaño total del frame construido -- `0` si hay error (payload muy largo, buffer insuficiente, etc) +Where `n` is the layer chain length. With `LLP_MAX_PAYLOAD=128`, worst case is 264 bytes. --- -### Estadísticas +## Layer Chain -```cpp -void llp_get_stats(llp_parser_t* parser, - uint32_t* frames_ok, - uint32_t* frames_error, - uint32_t* timeouts); -``` +The payload contains an ordered sequence of layer headers followed by raw application data: -Obtiene contadores de frames recibidos, errores y timeouts. - -```cpp -void llp_reset_stats(llp_parser_t* parser); +``` +[LAYER_ID][META_LEN][METADATA...]...[0x00][RAW DATA] ``` -Reinicia los contadores a cero. - ---- - -## 🏷️ Tipos de Mensaje +| ID Range | Type | Description | +|----------|------|-------------| +| `0x00` | **FinalNode** | End of chain; remaining bytes are raw application data | +| `0x01–0x7F` | **Passthrough** | Metadata can be skipped; payload is unchanged | +| `0x80–0xFE` | **Transform** | Payload was transformed (encrypted/compressed); cannot skip | +| `0xFF` | **Reserved** | Unknown layer ID | -| Constante | Valor | Propósito | -|----------------|-------|------------------------------------------| -| `LLP_PING` | 0x01 | Prueba de enlace (keep-alive) | -| `LLP_ACK` | 0x02 | Confirmación positiva (respuesta OK) | -| `LLP_NACK` | 0x03 | Confirmación negativa (respuesta error) | -| `LLP_DATA` | 0x10 | Datos genéricos (sensores, valores) | -| `LLP_CONFIG` | 0x11 | Parámetros y configuración remota | -| `LLP_STATUS` | 0x12 | Consulta/Reporte de estado del dispositivo| -| `LLP_COMMAND` | 0x13 | Comando a ejecutar en dispositivo remoto | -| `LLP_EVENT` | 0x14 | Evento asíncrono o externo | -| `LLP_ERROR` | 0x15 | Reporte de error específico | +### Meta Length Encoding -**Custom Types:** Usa valores 0x16-0xFF para tipos específicos de tu aplicación. +- **0–254**: 1 byte (direct value) +- **255+**: 3 bytes: `0xFF` + big-endian high/low --- -## 📚 Ejemplos +## API Reference -### Ejemplo 1: UART Simple (Arduino) +### Initialization -Ver [`examples/minimal_uart/minimal_uart.ino`](examples/minimal_uart/) - -Envía un PING cada 5 segundos y procesa respuestas. +```c +llp_parser_t parser; +llp_parser_init(&parser); +``` -### Ejemplo 2: Request-Response +### Processing -Ver [`examples/request_response/request_response.ino`](examples/request_response/) +```c +int result = llp_parser_process_byte(&parser, byte, millis()); -Implementa patrón request-response con timeout y reintentos. +// Returns: +// 1 → Frame complete, use parser.frame.payload +// 0 → Incomplete (more bytes needed) +// -1 → Error (check parser.error_code) +``` -### Ejemplo 3: Multi-node RS485 (próximamente) +### Extracting Data ---- +```c +// Build layer chain with FinalNode +uint8_t payload[LLP_MAX_PAYLOAD]; +size_t payload_len = llp_build_final_payload(payload, sizeof(payload), + data, data_len); -## ⚙️ Configuración Avanzada +// Build transport frame +uint8_t frame[LLP_MAX_FRAME_SIZE(payload_len)]; +size_t frame_len = llp_build_frame(frame, sizeof(frame), + payload, payload_len); -### Ajustar Payload Máximo +// Parse: extract layer chain +int result = llp_parser_process_byte(&parser, byte, millis()); -Para **Arduino UNO/Nano (2KB RAM)**: -```cpp -#define LLP_MAX_PAYLOAD 64 // En lugar de 512 +// Parse: extract raw application data (skip all layer headers) +uint8_t out[LLP_MAX_PAYLOAD]; +int out_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); +// Returns: bytes of raw data, or -1 if malformed (no FinalNode) ``` -Para **ESP8266/STM32 (> 32KB RAM)**: -```cpp -#define LLP_MAX_PAYLOAD 1024 // Más payload +### Error Codes + +```c +LLP_ERR_OK = 0x00 // No error +LLP_ERR_CHECKSUM = 0x01 // CRC mismatch +LLP_ERR_PAYLOAD_LEN = 0x02 // Length > LLP_MAX_PAYLOAD +LLP_ERR_TIMEOUT = 0x03 // Inter-byte timeout exceeded +LLP_ERR_SYNC = 0x04 // Invalid escape or resync +LLP_ERR_BUFFER_FULL = 0x05 // Internal buffer overflow +LLP_ERR_TRANSFORM_LAYER = 0x06 // Cannot traverse transform layer +LLP_ERR_MALFORMED_LAYER = 0x07 // Malformed layer chain ``` -### Modificar Timeout +### Statistics -```cpp -#define LLP_FRAME_TIMEOUT_MS 5000 // Para RF lento o latente +```c +uint32_t frames_ok, frames_error, timeouts; +llp_get_stats(&parser, &frames_ok, &frames_error, &timeouts); +llp_reset_stats(&parser); ``` -### Magic Bytes Personalizados +--- -```cpp -#define LLP_MAGIC_1 0xCC -#define LLP_MAGIC_2 0xDD -``` +## Quick Start ---- +### 1. Include the library -## 📊 Performance +```c +#include "llp_protocol.h" +``` -### Consumo de Memoria +### 2. Initialize parser -| Componente | Bytes | -|------------|-------| -| `llp_parser_t` | ~600-650 | -| `llp_frame_t` (con payload 512B) | ~520 | -| Código ejecutable | ~500 | -| **Total (mínimo)** | ~650 | +```c +llp_parser_t parser; +llp_parser_init(&parser); +``` -Para Arduino UNO (2KB RAM): ~30% de memoria disponible. +### 3. Process incoming bytes -### Velocidad +```c +void loop() { + while (Serial.available()) { + uint8_t byte = Serial.read(); + int result = llp_parser_process_byte(&parser, byte, millis()); + + if (result == 1) { + // Frame received successfully + uint8_t data[LLP_MAX_PAYLOAD]; + int len = llp_get_final_payload(&parser.frame, data, sizeof(data)); + if (len > 0) { + // data contains raw application payload + } + } else if (result == -1) { + // Error: check parser.error_code + } + } +} +``` -- **Parsing:** ~50 µs por byte (Arduino UNO, 16 MHz) -- **CRC16:** ~15 µs por byte -- **Frame builder:** ~100 µs (típico) +### 4. Send a response (optional) -### Throughput (UART 9600 baud) +```c +void send_response(const uint8_t* data, uint16_t len) { + uint8_t payload[LLP_MAX_PAYLOAD]; + size_t payload_len = llp_build_final_payload(payload, sizeof(payload), + data, len); -``` -8 bits/byte + 1 start + 1 stop = 10 bits/byte -9600 baud / 10 = 960 bytes/seg + uint8_t frame[LLP_MAX_FRAME_SIZE(payload_len)]; + size_t frame_len = llp_build_frame(frame, sizeof(frame), + payload, (uint16_t)payload_len); -Con 20B overhead + 100B payload = 120B/frame -960 / 120 ≈ 8 frames/segundo máximo + Serial.write(frame, frame_len); +} ``` --- -## 🛡️ Seguridad y Robustez +## Examples -| Característica | Descripción | -|---|---| -| **CRC16-CCITT** | Detecta bit-flips, inversiones, ráfagas de error comunes en RF | -| **Sincronización RF** | Maneja magic bytes consecutivos para recuperación automática | -| **Timeout** | Descarta frames incompletos tras 2 segundos (configurable) | -| **Validación de tamaño** | Rechaza payloads > LLP_MAX_PAYLOAD | -| **Defensiva con NULL** | Valida punteros en llp_build_frame y llp_get_stats | +### Minimal UART Echo ---- +Located at `examples/minimal_uart/minimal_uart.ino`: +- Receives frames via Serial +- Echoes received data back -## ❓ Preguntas Frecuentes +### Request-Response with Retries -**P: ¿Puedo usar esto en RF 433MHz?** -R: Sí. El protocolo está diseñado para RF. Recomendamos implementar retransmisión automática con ACK para mayor confiabilidad. +Located at `examples/request_response/request_response.ino`: +- Sends commands with ID and waits for ACK +- Retries up to 3 times on timeout +- Manages up to 5 pending requests -**P: ¿Funciona en UART, I2C, SPI?** -R: Funciona nativamente en UART. Para I2C/SPI, es viable pero menos natural (requiere polling). +--- -**P: ¿Hay encriptación?** -R: No incluida en el core. Puedes encriptar el payload antes de llamar `llp_build_frame()`. +## Testing -**P: ¿Cómo detectar comandos perdidos?** -R: Usa el campo `ID` para correlacionar. Si no recibes ACK en N milisegundos, retransmite. +### Run all tests -**P: ¿Soporta fragmentación?** -R: No automática. Para payloads > 512B, implementa en tu aplicación (e.g., ID + fragment counter en payload). +```bash +platformio test -e test -v +``` -**P: ¿Consume mucho flash en Arduino?** -R: ~500-700 bytes. Compatible con Arduino UNO/Nano. +**Expected output:** `90 Tests 0 Failures 0 Ignored OK` ---- +### Test categories -## 🤝 Contribuciones +| Suite | Tests | Description | +|-------|-------|-------------| +| `test_parser_init.c` | 5 | Parser initialization and reset | +| `test_frame_builder.c` | 11 | Frame construction and byte stuffing | +| `test_parser_process.c` | 11 | Byte-by-byte parsing and error handling | +| `test_crc.c` | 8 | CRC16-CCITT validation | +| `test_spec_vectors.c` | 55 | Spec conformance (199 vectors total) | -Las **contribuciones son bienvenidas**: +### Spec vector categories -- 🐛 Reporta bugs en [Issues](https://github.com/EnzoLeonel/llp-protocol/issues) -- 💡 Propón mejoras -- 📝 Agrega ejemplos (RF, RS485, etc) -- ✏️ Mejora documentación +- **Transport**: valid, crc, stuffing, resync, truncation, timeout +- **Layers**: passthrough, transform, malformed, traversal +- **Parser**: incremental, fragmented, recovery -### Cómo contribuir: +### Cross-language testing -1. Fork el repo -2. Crea una rama: `git checkout -b feature/mi-mejora` -3. Commit: `git commit -am 'Agrego soporte para XYZ'` -4. Push: `git push origin feature/mi-mejora` -5. Abre un Pull Request +```bash +gcc -std=c99 -I include -o /tmp/llp_cross_gen tools/cross_test_generate.c +/tmp/llp_cross_gen /tmp/llp_cross +``` --- -## 📜 Licencia - -MIT License - Ver [`LICENSE`](LICENSE) +## Project Structure -Uso, modificación y distribución libres sin restricciones. +``` +llp-protocol/ +├── include/ +│ └── llp_protocol.h # Single-header library (~470 lines) +├── src/ +│ └── llp_protocol.c # Standalone compilation verification +├── test/ +│ ├── test_main.c # Test runner (90 tests) +│ ├── test_spec_common.h # Spec test utilities +│ ├── test_spec_vectors.c # 199 spec conformance vectors +│ ├── test_parser_init.c # 5 tests +│ ├── test_frame_builder.c # 11 tests +│ ├── test_parser_process.c # 11 tests +│ └── test_crc.c # 8 tests +├── tools/ +│ └── cross_test_generate.c # Cross-language test vector generator +├── examples/ +│ ├── minimal_uart/ # Basic UART echo +│ └── request_response/ # Request-response with retries +├── docs/ +│ └── PROTOCOL.md # Protocol specification +├── platformio.ini # Multi-platform build config +├── library.properties # Arduino Library Manager metadata +├── CHANGELOG.md +├── README.md +├── STRUCTURE.md +├── TESTING.md +├── BUGS.md +└── LICENSE +``` --- -## ✍️ Autor +## Configuration -Creado por **EnzoLeonel** con la colaboración de amigos revisores. +Override defaults with build flags: + +```bash +-DLLP_MAX_PAYLOAD=256 # Default: 128 +-DLLP_FRAME_TIMEOUT_MS=3000 # Default: 2000 +``` --- -## 📞 Contacto / Soporte +## License -- 📧 [Crear Issue](https://github.com/EnzoLeonel/llp-protocol/issues) -- 💬 [Discussions](https://github.com/EnzoLeonel/llp-protocol/discussions) +MIT — See [LICENSE](LICENSE) ---- +LLP Specification v3.1.0 — Copyright © 2026 Flamingo Communications + +This specification is maintained as the authoritative reference for the LLP protocol. All implementations should reference this document as the canonical behaviour definition. -**Last updated:** 2026-03-30 -**Version:** 1.0.0 +--- \ No newline at end of file diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..7753ea8 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,87 @@ +# Project Structure — LLP Protocol v3.0.0 + +## Directory Tree + +``` +llp-protocol/ +│ +├── platformio.ini # PlatformIO configuration +│ +├── src/ +│ └── llp_protocol.c # Source (includes header-only lib) +│ +├── include/ +│ └── llp_protocol.h # Public API (single-header library) +│ +├── test/ +│ ├── test_main.c # Test runner — registers all test functions +│ ├── test_parser_init.c # 5 tests — Parser initialization +│ ├── test_frame_builder.c # 11 tests — Frame construction +│ ├── test_parser_process.c # 11 tests — Byte-by-byte processing +│ ├── test_crc.c # 8 tests — CRC16-CCITT validation +│ ├── test_spec_common.h # Utilities for spec vector tests +│ └── test_spec_vectors.c # ~199 vectors — Spec conformance tests +│ +├── tools/ +│ └── cross_test_generate.c # Cross-language test vector generator +│ +├── lib/ # External libraries (auto-installed) +│ └── (Unity via PIO lib-deps) +│ +├── examples/ +│ ├── minimal_uart/ +│ │ └── minimal_uart.ino # Minimal UART echo example +│ └── request_response/ +│ └── request_response.ino # Request-response with retries +│ +├── docs/ +│ └── PROTOCOL.md # Protocol specification v3.1.0 +│ +├── .github/ +│ └── workflows/ +│ └── platformio.yml # CI/CD via GitHub Actions +│ +├── .gitignore +├── CHANGELOG.md +├── LICENSE +├── README.md +├── STRUCTURE.md # This file +├── TESTING.md # Testing guide +�└── library.properties # Arduino Library Manager metadata +``` + +## File Purposes + +| File | Purpose | +|------|---------| +| `platformio.ini` | PlatformIO build/test configuration | +| `src/llp_protocol.c` | Verifies header compiles standalone | +| `include/llp_protocol.h` | Full protocol implementation (header-only) | +| `test/test_main.c` | Test runner with all RUN_TEST entries | +| `test/test_parser_init.c` | Parser initialization tests | +| `test/test_frame_builder.c` | Frame building and roundtrip tests | +| `test/test_parser_process.c` | Byte processing, error handling | +| `test/test_crc.c` | CRC16-CCITT validation tests | +| `test/test_spec_common.h` | Hex parsing and stream utilities for spec tests | +| `test/test_spec_vectors.c` | Spec conformance tests (~199 vectors from llp-spec) | +| `tools/cross_test_generate.c` | Cross-language (C/Java) test vector generator | +| `docs/PROTOCOL.md` | Protocol specification | +| `.github/workflows/platformio.yml` | Automated test execution on push/PR | + +## Test Summary + +| Test File | Tests | Focus | +|-----------|-------|-------| +| `test_parser_init.c` | 5 | Parser startup, state reset, stats | +| `test_frame_builder.c` | 11 | Frame construction, stuffing, roundtrip | +| `test_parser_process.c` | 11 | Byte parsing, errors, recovery, timeouts | +| `test_crc.c` | 8 | CRC16-CCITT, error detection, determinism | +| `test_spec_vectors.c` | 55 | Spec conformance (199 vectors total) | +| **Total** | **90** | | + +## Running Tests + +```bash +platformio test -e test -v # All tests verbose +platformio test -e test --filter test_crc # Specific suite +``` \ No newline at end of file diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..5ed4a49 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,96 @@ +# Testing Guide — LLP Protocol v3.0.0 + +## Prerequisites + +```bash +pip install platformio +``` + +## Run All Tests + +```bash +platformio test -e test -v +``` + +Expected output: +``` +90 Tests 0 Failures 0 Ignored OK +``` + +## Test Suites + +### Original Tests (35 tests) + +| Test File | Count | Focus | +|-----------|-------|-------| +| `test_parser_init.c` | 5 | Parser startup, state reset, stats | +| `test_frame_builder.c` | 11 | Frame construction, stuffing, roundtrip | +| `test_parser_process.c` | 11 | Byte parsing, errors, recovery, timeouts | +| `test_crc.c` | 8 | CRC16-CCITT, error detection, determinism | + +### Spec Conformance Tests (55 test functions, ~199 vectors) + +Based on the [LLP Specification v3.1.0](https://github.com/flamicomm/llp-spec) test vectors. + +| Test File | Category | Count | Vectors | +|-----------|----------|-------|---------| +| `test_spec_vectors.c` | Transport Valid Encode | 1 | 31 | +| `test_spec_vectors.c` | Transport Valid Decode | 1 | 31 | +| `test_spec_vectors.c` | Transport Valid Stream | 3 | 3 | +| `test_spec_vectors.c` | Transport CRC | 1 | 28 | +| `test_spec_vectors.c` | Transport Stuffing | 2 | 8 | +| `test_spec_vectors.c` | Transport Resync | 8 | 8 | +| `test_spec_vectors.c` | Transport Truncation | 8 | 7+2 | +| `test_spec_vectors.c` | Transport Timeout | 3 | 3 | +| `test_spec_vectors.c` | Layers Passthrough | 4 | 15+15+2 | +| `test_spec_vectors.c` | Layers Transform | 2 | 6+7 | +| `test_spec_vectors.c` | Layers Malformed | 4 | 4 | +| `test_spec_vectors.c` | Layers Traversal | 5 | 5 | +| `test_spec_vectors.c` | Parser Incremental | 3 | 3 | +| `test_spec_vectors.c` | Parser Fragmented | 3 | 3 | +| `test_spec_vectors.c` | Parser Recovery | 4 | 4 | + +### Cross-Language Test Generator + +The `tools/cross_test_generate.c` standalone program generates binary frames +from known test vectors and verifies interoperability with the Java implementation. + +Compile and run: +```bash +gcc -std=c99 -I include -o /tmp/llp_cross_gen tools/cross_test_generate.c +/tmp/llp_cross_gen [output_dir] +``` + +## Coverage + +The CI pipeline generates code coverage reports using GCOV/LCOV and uploads them to Codecov. +Coverage data is collected automatically on every push and pull request. + +### Local Coverage (optional) + +Requires `lcov`: +```bash +# Ubuntu/Debian +sudo apt-get install lcov + +# Run tests (coverage flags are already in platformio.ini) +platformio test -e test + +# Generate HTML report +mkdir -p coverage +lcov --capture --directory .pio/build/test --output-file coverage/lcov.info +lcov --remove coverage/lcov.info '/usr/*' '*/libdeps/*' '*/Unity/*' --output-file coverage/lcov.info +genhtml coverage/lcov.info --output-directory coverage/html +``` + +## Known Issues + +See [BUGS.md](BUGS.md) for known issues discovered through spec conformance testing. + +## Debugging + +```bash +platformio test -e test -vvv # Extra verbose with compiler output +platformio test -e test --filter test_crc # Run specific test file +pio test --verbose -e test # Alternative syntax +``` \ No newline at end of file diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 907c15f..5a6799b 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -1,173 +1,74 @@ -# Especificación LLP Protocol v1.0 +# Protocol Specification — LLP v3.1.0 -## Resumen Ejecutivo - -LLP (Lightweight Link Protocol) es un protocolo de nivel de enlace (Layer 2 OSI) -diseñado para ser ultra-liviano, robusto y extensible en microcontroladores. - -## Estructura del Frame +## Wire Format ``` - Offset │ Bytes │ Nombre │ Descripción -────────┼───────┼───────────┼───────────────────────────────── - 0 │ 2 │ MAGIC │ 0xAA 0x55 (Sincronización) - 2 │ 1 │ TYPE │ Tipo de mensaje (0x00-0xFF) - 3 │ 2 │ ID │ ID de transacción (Little Endian) - 5 │ 2 │ LENGTH │ Tamaño payload (Little Endian) - 7 │ N │ PAYLOAD │ Datos (0-512 bytes) - 7+N │ 2 │ CRC16 │ CRC16-CCITT (Little Endian) +[0xAA][0x55][LEN_L][LEN_H][PAYLOAD...][CRC_L][CRC_H] ``` -## Campos Detallados - -### MAGIC (2 bytes) -- **Propósito:** Sincronización y delimitación de frame -- **Valor:** 0xAA seguido de 0x55 -- **Recuperación:** Si llega 0xAA cuando se espera 0x55, pero llega otro 0xAA, - volvemos a esperar 0x55 (evita falsos positivos en RF ruidoso) - -### TYPE (1 byte) -- **Rango:** 0x00-0xFF -- **Definición:** Tipos base 0x01-0x15 en `llp_msg_type_t` -- **Custom:** Aplicaciones pueden usar 0x16-0xFF +- **MAGIC**: 0xAA 0x55 (synchronization marker) +- **LEN**: Payload length in little-endian (0–512 bytes, configurable via `LLP_MAX_PAYLOAD`) +- **PAYLOAD**: Layer chain containing application data +- **CRC16**: CRC16-CCITT (poly=0x1021, init=0xFFFF, no reflection) over MAGIC+LEN+PAYLOAD -### ID (2 bytes, Little Endian) -- **Propósito:** Correlacionar request-response -- **Formato:** LSB first, MSB second -- **Ejemplo:** ID=0x1234 → bytes [0x34, 0x12] -- **Uso:** Detectar frames duplicados o perdidos +### Byte Stuffing -### LENGTH (2 bytes, Little Endian) -- **Rango:** 0-512 (configurable en `#define LLP_MAX_PAYLOAD`) -- **Formato:** LSB first -- **Validación:** Rechaza si > LLP_MAX_PAYLOAD +Every 0xAA byte in LEN, PAYLOAD, or CRC is escaped as `0xAA 0x00`. +An unexpected `0xAA 0x55` sequence inside a frame signals a **resync event** (error recovery). -### PAYLOAD (Variable, 0-512 bytes) -- **Estructura:** Completamente aplicación-dependiente -- **Validación:** Ninguna (responsabilidad de la aplicación) -- **Ejemplo:** ASCII, binario, struct, JSON, etc. +## Layer Chain Format -### CRC16 (2 bytes, Little Endian) -- **Algoritmo:** CRC16-CCITT (polinomio 0x1021) -- **Inicial:** 0xFFFF -- **Cobertura:** Magic + Type + ID + Length + Payload -- **Excluye:** El mismo CRC16 -- **Formato:** LSB first +The payload contains a chain of layers, terminated by a FinalNode: -## Máquina de Estados del Parser - -``` -WAIT_MAGIC1 ─┐ - 0xAA recib.│ - ├─────→ WAIT_MAGIC2 ─┐ - │ 0x55 recib. │ - │ ├────────→ READ_TYPE - │ │ └→ READ_ID_L → READ_ID_H - │ │ └→ READ_LEN_L → READ_LEN_H - │ │ ├─ Si len=0 → READ_CRC_L - │ │ └─ Si len>0 → READ_PAYLOAD - │ │ ├─→ READ_CRC_L - │ │ - │ 0xAA recib. (otro) - │ └─ Quedarse en WAIT_MAGIC2 (robusto RF) - │ - Otro byte - └─────────→ Volver a WAIT_MAGIC1 - -READ_CRC_L → READ_CRC_H ─┐ - Validar CRC │ - ├─ OK ──────────────→ Return 1 (Frame completo) - └─ Error ──────────→ Return -1 + Volver a WAIT_MAGIC1 ``` - -## Códigos de Error - -```c -LLP_ERR_OK = 0x00 // Sin error -LLP_ERR_CHECKSUM = 0x01 // CRC no coincide -LLP_ERR_TYPE = 0x02 // Tipo inválido (obsoleto en v1.0) -LLP_ERR_PAYLOAD_LEN = 0x03 // Payload > LLP_MAX_PAYLOAD -LLP_ERR_TIMEOUT = 0x04 // Timeout sin completar frame -LLP_ERR_SYNC = 0x05 // Problema de sincronismo -LLP_ERR_BUFFER_FULL = 0x06 // Buffer desbordado +[LAYER_ID][META_LEN][METADATA...]...[0x00][RAW APPLICATION DATA] ``` -## Patrones de Comunicación +- **0x00** → FinalNode: end of chain, raw bytes follow +- **0x01–0x7F** → Passthrough layer: metadata can be skipped, payload unchanged +- **0x80–0xFE** → Transform layer: payload was modified (encrypt/compress), cannot skip +- **0xFF** → Reserved -### 1. Unidireccional (Fire-and-Forget) +### META_LEN Encoding -``` -Device A ──DATA──→ Device B - (sin esperar ACK) -``` +- **0–254**: 1 byte (direct value) +- **255+**: 3 bytes: `0xFF` followed by 2 bytes big-endian (e.g., `0xFF 0x01 0x00` = 256) -Ideal para telemetría pasiva donde no importa si se pierde un frame. +## Parser State Machine -### 2. Request-Response - -``` -Device A ──COMMAND──→ Device B -Device A ←───ACK──── Device B ``` - -Ideal para configuración remota, consultas de estado. - -### 3. Publish-Subscribe (vía coordinador) - -``` -Device A ──EVENT──→ Coordinador -Device B ←──EVENT─ Coordinador -``` - -Ideal para redes multi-dispositivo. - -## Recomendaciones - -### Para Enlace Confiable -1. Usar ID en frames -2. Implementar timeout + reintentos -3. Usar LLP_ACK/LLP_NACK explícitamente - -### Para RF Ruidoso -1. Usar LLP_MAX_PAYLOAD pequeño (64-128B) -2. Implementar Forward Error Correction (Hamming) en payload si es crítico -3. Multi-intentos antes de dar por perdido - -### Para Payload Grande (> 512B) -1. Fragmentar en múltiples frames -2. Usar campo ID con contador de fragmento en payload -3. Ejemplo: - ``` - Payload: [FRAG_ID (2B)] [FRAG_NUM (1B)] [FRAG_TOTAL (1B)] [DATA (N bytes)] - ``` - -## Ejemplos - -### Enviar Datos Simples - -```c -uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; -llp_build_frame(buf, 520, LLP_DATA, 1, data, 4); -// Result: [0xAA, 0x55, 0x10, 0x01, 0x00, 0x04, 0x00, -// 0xDE, 0xAD, 0xBE, 0xEF, CRC_L, CRC_H] +WAIT_MAGIC1 ──0xAA──→ WAIT_MAGIC2 ──0x55──→ READ_LEN_L + │ │ 0xAA │ + │ other bytes │ other bytes ↓ + └─────────────────────┴──────────→ READ_LEN_H + ↓ + READ_PAYLOAD (LEN > 0) + READ_CRC_L (LEN == 0) + ↓ + READ_CRC_H ──validate──→ FRAME OK (return 1) + │ + └──invalid──→ ERROR (return -1) + +Timeout: any byte arriving >LLP_FRAME_TIMEOUT_MS after the previous byte resets the parser. +Escape: 0xAA inside frame data → 0xAA 0x00 (escaped byte); 0xAA 0x55 → resync. +Invalid escape: 0xAA followed by any byte other than 0x00 or 0x55 → SYNC_ERROR. ``` -### Recibir y Procesar +## Error Codes ```c -while (serial.available()) { - int ret = llp_parser_process_byte(&parser, byte, millis()); - if (ret == 1) { - // parser.frame.type - // parser.frame.id - // parser.frame.payload_len - // parser.frame.payload[] - } -} +LLP_ERR_OK = 0x00 // No error +LLP_ERR_CHECKSUM = 0x01 // CRC mismatch +LLP_ERR_PAYLOAD_LEN = 0x02 // Payload exceeds LLP_MAX_PAYLOAD +LLP_ERR_TIMEOUT = 0x03 // Inter-byte timeout exceeded +LLP_ERR_SYNC = 0x04 // Invalid escape or resync +LLP_ERR_BUFFER_FULL = 0x05 // Buffer overflow +LLP_ERR_TRANSFORM_LAYER = 0x06 // Cannot traverse transform layer +LLP_ERR_MALFORMED_LAYER = 0x07 // Malformed layer chain ``` --- -**Documento:** Especificación técnica v1.0 -**Fecha:** 2026-03-30 -**Autor:** EnzoLeonel +**Document:** Protocol specification v3.1.0 +**Date:** 2026-05-17 +**Author:** EnzoLeonel \ No newline at end of file diff --git a/examples/minimal_uart/minimal_uart.ino b/examples/minimal_uart/minimal_uart.ino index 9a642bf..79d374f 100644 --- a/examples/minimal_uart/minimal_uart.ino +++ b/examples/minimal_uart/minimal_uart.ino @@ -1,241 +1,49 @@ /** - * LLP Protocol - Minimal UART Example (Arduino) - * - * Este ejemplo muestra la forma más simple de usar LLP: - * - Recibir frames por Serial (UART) - * - Enviar frames de respuesta - * - Procesar diferentes tipos de mensaje - * - * Hardware: Arduino UNO + Serial Monitor - * NOTA: Ajustar LLP_MAX_PAYLOAD del protocolo según microcontrolador - * Velocidad: 9600 baud + * LLP Protocol v3.0.0 — Minimal UART Example (Arduino) + * + * Demonstrates the simplest usage of LLP v3: + * - Receive frames via Serial (UART) + * - Echo received data back + * + * Hardware: Any Arduino-compatible board + * Baud rate: 115200 */ -#include "llp-protocol.h" +#include "llp_protocol.h" -// ============= GLOBALS ============= llp_parser_t parser; -uint8_t tx_buffer[LLP_HEADER_SIZE + LLP_MAX_PAYLOAD + 2]; +uint8_t tx_buffer[LLP_MAX_FRAME_SIZE(LLP_MAX_PAYLOAD)]; -static uint16_t next_cmd_id = 1; - -// ============= SETUP ============= -void setup() { - pinMode(LED_BUILTIN, OUTPUT); - Serial.begin(9600); - llp_parser_init(&parser); - - Serial.println(F("\n=== LLP Protocol - Minimal UART Example ===")); - Serial.println(F("Esperando frames...\n")); - - // Debug: enviar un PING cada 10s - Serial.println(F("Tip: Abre Serial Monitor (9600 baud) para enviar frames\n")); -} - -// ============= MAIN LOOP ============= -void loop() { - // Procesar bytes entrantes - while (Serial.available()) { - uint8_t byte = Serial.read(); - int result = llp_parser_process_byte(&parser, byte, millis()); - - if (result == 1) { - // ✅ Frame válido recibido - handleReceivedFrame(&parser.frame); - - } else if (result == -1) { - // ❌ Error en frame - printError(parser.error_code); - } - } - - // Enviar PING cada 10 segundos como demostración - static unsigned long lastPing = 0; - if (millis() - lastPing > 10000) { - lastPing = millis(); - sendPing(); - } - - // Mostrar estadísticas cada 30s - static unsigned long lastStats = 0; - if (millis() - lastStats > 30000) { - lastStats = millis(); - printStats(); - } +void setup(void) { + Serial.begin(115200); + llp_parser_init(&parser); } -// ============= FRAME HANDLERS ============= +void loop(void) { + while (Serial.available()) { + uint8_t byte = Serial.read(); -void handleReceivedFrame(llp_frame_t *frame) { - Serial.print(F("[RX] Type: 0x")); - Serial.print(frame->type, HEX); - Serial.print(F(" | ID: ")); - Serial.print(frame->id); - Serial.print(F(" | Len: ")); - Serial.print(frame->payload_len); - Serial.print(F(" | Data: ")); + int result = llp_parser_process_byte(&parser, byte, millis()); - // Mostrar payload - for (uint16_t i = 0; i < frame->payload_len; i++) { - if (frame->payload[i] >= 0x20 && frame->payload[i] < 0x7F) { - Serial.write(frame->payload[i]); - } else { - Serial.print(F("[0x")); - Serial.print(frame->payload[i], HEX); - Serial.print(F("]")); + if (result == 1) { + uint8_t data[LLP_MAX_PAYLOAD]; + int len = llp_get_final_payload(&parser.frame, data, sizeof(data)); + if (len > 0) { + send_data(data, (uint16_t)len); + } else { + send_data(NULL, 0); + } + } } - } - Serial.println(); - - // Procesar según tipo - switch (frame->type) { - case LLP_PING: - Serial.println(F(" → Recibido PING, enviando ACK...")); - sendAck(frame->id, LLP_ERR_OK); - break; - - case LLP_DATA: - Serial.println(F(" → Recibido DATA")); - sendAck(frame->id, LLP_ERR_OK); - break; - - case LLP_CONFIG: - Serial.println(F(" → Recibido CONFIG")); - sendAck(frame->id, LLP_ERR_OK); - break; - - case LLP_COMMAND: - Serial.println(F(" → Recibido COMMAND")); - handleCommand(frame); - break; - - case LLP_ACK: - Serial.println(F(" → Recibido ACK")); - break; - - case LLP_NACK: - Serial.println(F(" → Recibido NACK (error)")); - break; - - default: - Serial.print(F(" → Tipo desconocido (0x")); - Serial.print(frame->type, HEX); - Serial.println(F(")")); - break; - } -} - -void handleCommand(llp_frame_t *frame) { - // Ejemplo: si el payload es "LED_ON", activar LED - if (frame->payload_len == 6 && - strncmp((char*)frame->payload, "LED_ON", 6) == 0) { - Serial.println(F(" → Comando: LED_ON (dummy, no hay LED físico)")); - digitalWrite(LED_BUILTIN, HIGH); - delay(100); - digitalWrite(LED_BUILTIN, LOW); - } } -// ============= TX FUNCTIONS ============= - -void sendPing() { - size_t len = llp_build_frame( - tx_buffer, sizeof(tx_buffer), - LLP_PING, - next_cmd_id++, - NULL, 0 - ); - - Serial.print(F("[TX] Enviando PING (ID ")); - Serial.print(next_cmd_id - 1); - Serial.println(F(")...")); - - if (len > 0) { - Serial.write(tx_buffer, len); - } else { - Serial.println(F("[ERROR] Failed to build frame")); - } -} - -void sendAck(uint16_t id, uint8_t ack_code) { - uint8_t payload[] = {ack_code}; - size_t len = llp_build_frame( - tx_buffer, sizeof(tx_buffer), - LLP_ACK, - id, - payload, 1 - ); - - Serial.print(F("[TX] Enviando ACK (ID ")); - Serial.print(id); - Serial.print(F(", code: 0x")); - Serial.print(ack_code, HEX); - Serial.println(F(")")); - - if (len > 0) { - Serial.write(tx_buffer, len); - } else { - Serial.println(F("[ERROR] Failed to build frame")); - } -} - -void sendData(const uint8_t *data, uint16_t len) { - size_t frame_len = llp_build_frame( - tx_buffer, sizeof(tx_buffer), - LLP_DATA, - next_cmd_id++, - data, len - ); - - Serial.print(F("[TX] Enviando DATA (ID ")); - Serial.print(next_cmd_id - 1); - Serial.print(F(", ")); - Serial.print(len); - Serial.println(F(" bytes)")); - - if (len > 0) { - Serial.write(tx_buffer, len); - } else { - Serial.println(F("[ERROR] Failed to build frame")); - } -} - -// ============= UTILITIES ============= - -void printError(uint8_t error_code) { - Serial.print(F("[ERROR] 0x")); - Serial.print(error_code, HEX); - Serial.print(" - "); - - switch (error_code) { - case LLP_ERR_CHECKSUM: - Serial.println(F("CRC Inválido")); - break; - case LLP_ERR_TIMEOUT: - Serial.println(F("Timeout (frame incompleto)")); - break; - case LLP_ERR_PAYLOAD_LEN: - Serial.println(F("Payload demasiado largo")); - break; - case LLP_ERR_SYNC: - Serial.println(F("Problema de sincronismo")); - break; - default: - Serial.println(F("Desconocido")); - break; - } -} - -void printStats() { - uint32_t ok, err, timeout; - llp_get_stats(&parser, &ok, &err, &timeout); - - Serial.println(F("\n--- ESTADÍSTICAS ---")); - Serial.print(F("Frames OK: ")); - Serial.println(ok); - Serial.print(F("Frames Error: ")); - Serial.println(err); - Serial.print(F("Timeouts: ")); - Serial.println(timeout); - Serial.println(F("---\n")); +void send_data(const uint8_t* data, uint16_t len) { + uint8_t llp_payload[LLP_MAX_PAYLOAD]; + size_t payload_len = llp_build_final_payload(llp_payload, sizeof(llp_payload), + data, len); + size_t frame_len = llp_build_frame(tx_buffer, sizeof(tx_buffer), + llp_payload, (uint16_t)payload_len); + if (frame_len > 0) { + Serial.write(tx_buffer, frame_len); + } } diff --git a/examples/request_response/request_responde.ino b/examples/request_response/request_responde.ino deleted file mode 100644 index d83c0a4..0000000 --- a/examples/request_response/request_responde.ino +++ /dev/null @@ -1,172 +0,0 @@ -/** - * LLP Protocol - Request-Response Pattern - * - * Ejemplo de patrón request-response con timeout y reintentos. - * Útil para comunicación confiable donde necesitas garantizar - * que el otro lado recibió y procesó tu comando. - */ - -#include "llp_protocol.h" - -#define ACK_TIMEOUT_MS 1000 -#define MAX_RETRIES 3 - -// ============= STRUCTURES ============= - -struct PendingRequest { - uint16_t id; - unsigned long sent_time; - uint8_t retry_count; - uint8_t frame[520]; - size_t frame_len; - bool in_use; -}; - -// ============= GLOBALS ============= - -llp_parser_t parser; -uint8_t tx_buffer[520]; - -#define MAX_PENDING_REQUESTS 5 -struct PendingRequest pending[MAX_PENDING_REQUESTS]; - -static uint16_t next_cmd_id = 1; - -// ============= SETUP ============= - -void setup() { - Serial.begin(9600); - llp_parser_init(&parser); - - memset(pending, 0, sizeof(pending)); - - Serial.println("\n=== LLP Protocol - Request-Response ==="); - Serial.println("Enviando comando cada 5 segundos con reintentos\n"); -} - -// ============= MAIN LOOP ============= - -void loop() { - // Procesar bytes entrantes - while (Serial.available()) { - uint8_t byte = Serial.read(); - int result = llp_parser_process_byte(&parser, byte, millis()); - - if (result == 1) { - handleReceivedFrame(&parser.frame); - } - } - - // Verificar reintentos de solicitudes pendientes - checkTimeouts(); - - // Enviar comando de ejemplo cada 5 segundos - static unsigned long lastCmd = 0; - if (millis() - lastCmd > 5000) { - lastCmd = millis(); - sendCommandWithRetry("HELLO"); - } -} - -// ============= HANDLERS ============= - -void handleReceivedFrame(llp_frame_t *frame) { - Serial.print("[RX] Type: 0x"); - Serial.print(frame->type, HEX); - Serial.print(" ID: "); - Serial.println(frame->id); - - if (frame->type == LLP_ACK) { - // Encontrar request pendiente - for (int i = 0; i < MAX_PENDING_REQUESTS; i++) { - if (pending[i].in_use && pending[i].id == frame->id) { - Serial.print(" ✓ ACK recibido para comando ID "); - Serial.print(frame->id); - Serial.print(" después de "); - Serial.print(millis() - pending[i].sent_time); - Serial.println("ms"); - - pending[i].in_use = false; - return; - } - } - } -} - -void checkTimeouts() { - unsigned long now = millis(); - - for (int i = 0; i < MAX_PENDING_REQUESTS; i++) { - if (!pending[i].in_use) continue; - - unsigned long elapsed = now - pending[i].sent_time; - - if (elapsed > ACK_TIMEOUT_MS) { - if (pending[i].retry_count < MAX_RETRIES) { - // Reintentar - pending[i].retry_count++; - pending[i].sent_time = now; - - Serial.print(" ⟲ Reintento "); - Serial.print(pending[i].retry_count); - Serial.print("/"); - Serial.print(MAX_RETRIES); - Serial.print(" para comando ID "); - Serial.println(pending[i].id); - - Serial.write(pending[i].frame, pending[i].frame_len); - - } else { - // Falló permanentemente - Serial.print(" ✗ Comando ID "); - Serial.print(pending[i].id); - Serial.println(" falló después de reintentos"); - - pending[i].in_use = false; - } - } - } -} - -// ============= TX FUNCTIONS ============= - -void sendCommandWithRetry(const char *cmd) { - // Buscar slot disponible - int slot = -1; - for (int i = 0; i < MAX_PENDING_REQUESTS; i++) { - if (!pending[i].in_use) { - slot = i; - break; - } - } - - if (slot == -1) { - Serial.println("ERROR: Cola de solicitudes llena"); - return; - } - - uint16_t id = next_cmd_id++; - size_t len = llp_build_frame( - tx_buffer, sizeof(tx_buffer), - LLP_COMMAND, - id, - (uint8_t*)cmd, strlen(cmd) - ); - - // Guardar en pending - pending[slot].id = id; - pending[slot].sent_time = millis(); - pending[slot].retry_count = 0; - pending[slot].frame_len = len; - pending[slot].in_use = true; - memcpy(pending[slot].frame, tx_buffer, len); - - // Enviar inmediato - Serial.print("[TX] Comando: \""); - Serial.print(cmd); - Serial.print("\" (ID "); - Serial.print(id); - Serial.println(")"); - - Serial.write(tx_buffer, len); -} diff --git a/examples/request_response/request_response.ino b/examples/request_response/request_response.ino new file mode 100644 index 0000000..72cbbc1 --- /dev/null +++ b/examples/request_response/request_response.ino @@ -0,0 +1,115 @@ +/** + * LLP Protocol v3.0.0 — Request-Response Pattern (Arduino) + * + * Demonstrates a request-response pattern with timeout and retries. + * Since v3.0.0 removed type/id from the transport layer, this example + * implements a simple app-level protocol: + * FinalNode payload: [id_L][id_H][command_bytes...] + */ + +#include "llp_protocol.h" + +#define ACK_TIMEOUT_MS 1000 +#define MAX_RETRIES 3 +#define MAX_PENDING 5 + +typedef struct { + uint16_t id; + unsigned long sent_time; + uint8_t retries; + uint8_t frame[LLP_MAX_FRAME_SIZE(LLP_MAX_PAYLOAD)]; + size_t frame_len; + bool in_use; +} pending_req_t; + +llp_parser_t parser; +uint8_t tx_buffer[LLP_MAX_FRAME_SIZE(LLP_MAX_PAYLOAD)]; +pending_req_t pending[MAX_PENDING]; +static uint16_t next_id = 1; + +void setup(void) { + Serial.begin(9600); + llp_parser_init(&parser); + memset(pending, 0, sizeof(pending)); +} + +void loop(void) { + while (Serial.available()) { + uint8_t byte = Serial.read(); + int result = llp_parser_process_byte(&parser, byte, millis()); + if (result == 1) { + handle_frame(&parser.frame); + } + } + check_timeouts(); + + static unsigned long last_cmd = 0; + if (millis() - last_cmd > 5000) { + last_cmd = millis(); + send_request((const uint8_t*)"HELLO", 5); + } +} + +void handle_frame(llp_frame_t* frame) { + uint8_t data[LLP_MAX_PAYLOAD]; + int len = llp_get_final_payload(frame, data, sizeof(data)); + if (len <= 0) return; + + uint16_t rx_id = (uint16_t)data[0] | ((uint16_t)data[1] << 8); + + for (int i = 0; i < MAX_PENDING; i++) { + if (pending[i].in_use && pending[i].id == rx_id) { + pending[i].in_use = false; + break; + } + } +} + +void check_timeouts(void) { + unsigned long now = millis(); + for (int i = 0; i < MAX_PENDING; i++) { + if (!pending[i].in_use) continue; + if (now - pending[i].sent_time < ACK_TIMEOUT_MS) continue; + + if (pending[i].retries < MAX_RETRIES) { + pending[i].retries++; + pending[i].sent_time = now; + Serial.write(pending[i].frame, pending[i].frame_len); + } else { + pending[i].in_use = false; + } + } +} + +void send_request(const uint8_t* cmd, uint16_t cmd_len) { + int slot = -1; + for (int i = 0; i < MAX_PENDING; i++) { + if (!pending[i].in_use) { slot = i; break; } + } + if (slot < 0) return; + + uint16_t id = next_id++; + + uint8_t app_data[LLP_MAX_PAYLOAD]; + uint16_t app_len = cmd_len + 2; + if (app_len > LLP_MAX_PAYLOAD) return; + app_data[0] = (uint8_t)(id & 0xFF); + app_data[1] = (uint8_t)((id >> 8) & 0xFF); + memcpy(&app_data[2], cmd, cmd_len); + + uint8_t llp_payload[LLP_MAX_PAYLOAD]; + size_t payload_len = llp_build_final_payload(llp_payload, sizeof(llp_payload), + app_data, app_len); + size_t frame_len = llp_build_frame(tx_buffer, sizeof(tx_buffer), + llp_payload, (uint16_t)payload_len); + if (frame_len == 0) return; + + pending[slot].id = id; + pending[slot].sent_time = millis(); + pending[slot].retries = 0; + pending[slot].frame_len = frame_len; + pending[slot].in_use = true; + memcpy(pending[slot].frame, tx_buffer, frame_len); + + Serial.write(tx_buffer, frame_len); +} diff --git a/include/llp-protocol.h b/include/llp-protocol.h deleted file mode 100644 index 841e32a..0000000 --- a/include/llp-protocol.h +++ /dev/null @@ -1,334 +0,0 @@ -#ifndef LLP_PROTOCOL_H -#define LLP_PROTOCOL_H - -#include -#include - -// ================= CONFIG ================= -#define LLP_MAGIC_1 0xAA -#define LLP_MAGIC_2 0x55 - -#define LLP_HEADER_SIZE 7 // Magic(2) + Type(1) + ID(2) + Len(2) -#define LLP_MAX_PAYLOAD 128 // Ajustable según RAM disponible. Bajar a 64/128 para AVR (Arduino Uno/Nano) -#define LLP_FRAME_TIMEOUT_MS 2000 // Timeout para sincronismo - -// ================= MESSAGE TYPES ================= -// Base types. El usuario puede usar tipos hasta 0xFF en su aplicación. -typedef enum { - LLP_PING = 0x01, // Prueba de enlace - LLP_ACK = 0x02, // Confirmación positiva - LLP_NACK = 0x03, // Confirmación negativa (error) - LLP_DATA = 0x10, // Datos genéricos (sensores, control) - LLP_CONFIG = 0x11, // Configuración - LLP_STATUS = 0x12, // Estado del dispositivo - LLP_COMMAND = 0x13, // Comando a ejecutar - LLP_EVENT = 0x14, // Evento reportado - LLP_ERROR = 0x15 // Reporte de error -} llp_msg_type_t; - -// ================= ERROR CODES ================= -typedef enum { - LLP_ERR_OK = 0x00, - LLP_ERR_CHECKSUM = 0x01, - LLP_ERR_TYPE = 0x02, - LLP_ERR_PAYLOAD_LEN = 0x03, - LLP_ERR_TIMEOUT = 0x04, - LLP_ERR_SYNC = 0x05, - LLP_ERR_BUFFER_FULL = 0x06 -} llp_error_t; - -// ================= CRC16-CCITT ================= -static inline uint16_t llp_crc16_update(uint16_t crc, uint8_t data) { - crc ^= (uint16_t)data << 8; - for (int i = 0; i < 8; i++) { - if (crc & 0x8000) - crc = (crc << 1) ^ 0x1021; - else - crc <<= 1; - } - return crc; -} - -static inline uint16_t llp_crc16_buffer(const uint8_t* buf, size_t len) { - uint16_t crc = 0xFFFF; - for (size_t i = 0; i < len; i++) { - crc = llp_crc16_update(crc, buf[i]); - } - return crc; -} - -// ================= FRAME STRUCTURE ================= -typedef struct { - uint8_t type; // Tipo de mensaje - uint16_t id; // ID de transacción (opcional) - uint16_t payload_len; - uint8_t payload[LLP_MAX_PAYLOAD]; - uint16_t crc; // CRC16 calculado -} llp_frame_t; - -// ================= PARSER ================= -typedef enum { - LLP_STATE_WAIT_MAGIC1, - LLP_STATE_WAIT_MAGIC2, - LLP_STATE_READ_TYPE, - LLP_STATE_READ_ID_L, - LLP_STATE_READ_ID_H, - LLP_STATE_READ_LEN_L, - LLP_STATE_READ_LEN_H, - LLP_STATE_READ_PAYLOAD, - LLP_STATE_READ_CRC_L, - LLP_STATE_READ_CRC_H -} llp_parser_state_t; - -typedef struct { - llp_parser_state_t state; - - uint8_t header_buf[LLP_HEADER_SIZE]; - - uint16_t payload_idx; - - uint16_t crc_received; - uint16_t crc_calculated; - - unsigned long last_byte_time; - - // Frame completado (Único buffer para ahorrar RAM) - llp_frame_t frame; - uint8_t error_code; - - // Estadísticas - uint32_t frames_ok; - uint32_t frames_error; - uint32_t timeouts; - -} llp_parser_t; - -// ================= FUNCIONES PÚBLICAS ================= - -/** - * Inicializa el parser - */ -static inline void llp_parser_init(llp_parser_t* parser) { - parser->crc_calculated = 0xFFFF; - parser->last_byte_time = 0; - parser->state = LLP_STATE_WAIT_MAGIC1; - parser->payload_idx = 0; - parser->error_code = LLP_ERR_OK; - parser->frames_ok = 0; - parser->frames_error = 0; - parser->timeouts = 0; -} - -/** - * Procesa un byte recibido - * Retorna: 1 si frame completo, 0 si sigue recibiendo, -1 si error - */ -static inline int llp_parser_process_byte(llp_parser_t* parser, uint8_t byte, - unsigned long current_ms) { - - // Timeout protection - if (parser->state != LLP_STATE_WAIT_MAGIC1) { - if (current_ms - parser->last_byte_time > LLP_FRAME_TIMEOUT_MS) { - parser->error_code = LLP_ERR_TIMEOUT; - parser->timeouts++; - parser->state = LLP_STATE_WAIT_MAGIC1; - return -1; - } - } - parser->last_byte_time = current_ms; - - switch (parser->state) { - - case LLP_STATE_WAIT_MAGIC1: - if (byte == LLP_MAGIC_1) { - parser->header_buf[0] = byte; - parser->state = LLP_STATE_WAIT_MAGIC2; - } - break; - - case LLP_STATE_WAIT_MAGIC2: - if (byte == LLP_MAGIC_2) { - parser->header_buf[1] = byte; - parser->crc_calculated = 0xFFFF; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, LLP_MAGIC_1); - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, LLP_MAGIC_2); - parser->state = LLP_STATE_READ_TYPE; - } else if (byte == LLP_MAGIC_1) { - // CORRECCIÓN RF: Si recibimos otro MAGIC_1, nos quedamos en MAGIC_2 - // porque podríamos estar en medio de ruido seguido del preámbulo real. - parser->state = LLP_STATE_WAIT_MAGIC2; - } else { - parser->state = LLP_STATE_WAIT_MAGIC1; - } - break; - - case LLP_STATE_READ_TYPE: - parser->header_buf[2] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - - // NOTA: Eliminamos la restricción de validación de tipo para permitir - // mensajes customizados > 0x15 en aplicaciones genéricas. - parser->frame.type = byte; - parser->state = LLP_STATE_READ_ID_L; - break; - - case LLP_STATE_READ_ID_L: - parser->header_buf[3] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - parser->state = LLP_STATE_READ_ID_H; - break; - - case LLP_STATE_READ_ID_H: - parser->header_buf[4] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - parser->frame.id = parser->header_buf[3] | (parser->header_buf[4] << 8); - parser->state = LLP_STATE_READ_LEN_L; - break; - - case LLP_STATE_READ_LEN_L: - parser->header_buf[5] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - parser->state = LLP_STATE_READ_LEN_H; - break; - - case LLP_STATE_READ_LEN_H: - parser->header_buf[6] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - parser->frame.payload_len = parser->header_buf[5] | (parser->header_buf[6] << 8); - - // Validar tamaño - if (parser->frame.payload_len > LLP_MAX_PAYLOAD) { - parser->error_code = LLP_ERR_PAYLOAD_LEN; - parser->frames_error++; - parser->state = LLP_STATE_WAIT_MAGIC1; - return -1; - } - - parser->payload_idx = 0; - - // Si payload es 0, pasar a lectura de CRC - if (parser->frame.payload_len == 0) { - parser->state = LLP_STATE_READ_CRC_L; - } else { - parser->state = LLP_STATE_READ_PAYLOAD; - } - break; - - case LLP_STATE_READ_PAYLOAD: - // Guardamos directo en el frame final, evitando copias. - parser->frame.payload[parser->payload_idx] = byte; - parser->crc_calculated = llp_crc16_update(parser->crc_calculated, byte); - parser->payload_idx++; - - if (parser->payload_idx == parser->frame.payload_len) { - parser->state = LLP_STATE_READ_CRC_L; - } - break; - - case LLP_STATE_READ_CRC_L: - parser->crc_received = byte; - parser->state = LLP_STATE_READ_CRC_H; - break; - - case LLP_STATE_READ_CRC_H: - parser->crc_received |= (byte << 8); - - // Validar CRC - if (parser->crc_received != parser->crc_calculated) { - parser->error_code = LLP_ERR_CHECKSUM; - parser->frames_error++; - parser->state = LLP_STATE_WAIT_MAGIC1; - return -1; - } - - parser->frame.crc = parser->crc_calculated; - parser->frames_ok++; - - parser->state = LLP_STATE_WAIT_MAGIC1; - - return 1; // Frame validado - - default: - parser->state = LLP_STATE_WAIT_MAGIC1; - break; - } - - return 0; // Sigue recibiendo -} - -// ================= FRAME BUILDER ================= - -/** - * Construye un frame en un buffer - * Retorna: tamaño del frame construido, 0 si error - */ -static inline size_t llp_build_frame( - uint8_t* out_buffer, - size_t out_buffer_size, - uint8_t type, - uint16_t id, - const uint8_t* payload, - uint16_t payload_len -) { - // Validaciones - if (payload_len > LLP_MAX_PAYLOAD) return 0; - if (out_buffer_size < LLP_HEADER_SIZE + payload_len + 2) return 0; - - size_t idx = 0; - - // Magic - out_buffer[idx++] = LLP_MAGIC_1; - out_buffer[idx++] = LLP_MAGIC_2; - - // Type - out_buffer[idx++] = type; - - // ID (Little Endian) - out_buffer[idx++] = (id & 0xFF); - out_buffer[idx++] = ((id >> 8) & 0xFF); - - // Length (Little Endian) - out_buffer[idx++] = (payload_len & 0xFF); - out_buffer[idx++] = ((payload_len >> 8) & 0xFF); - - // Payload - if (payload_len > 0 && payload != NULL) { - memcpy(&out_buffer[idx], payload, payload_len); - idx += payload_len; - } - - // Calcular CRC16 (sobre todo menos el CRC mismo) - uint16_t crc = llp_crc16_buffer(out_buffer, idx); - - // CRC (Little Endian) - out_buffer[idx++] = (crc & 0xFF); - out_buffer[idx++] = ((crc >> 8) & 0xFF); - - return idx; -} - -// ================= HELPERS ================= - -/** - * Obtiene estadísticas del parser - */ -static inline void llp_get_stats(llp_parser_t* parser, - uint32_t* frames_ok, - uint32_t* frames_error, - uint32_t* timeouts) { - if(frames_ok) *frames_ok = parser->frames_ok; - if(frames_error) *frames_error = parser->frames_error; - if(timeouts) *timeouts = parser->timeouts; -} - -/** - * Reinicia las estadísticas - */ -static inline void llp_reset_stats(llp_parser_t* parser) { - parser->frames_ok = 0; - parser->frames_error = 0; - parser->timeouts = 0; -} - -#endif - diff --git a/include/llp_protocol.h b/include/llp_protocol.h new file mode 100644 index 0000000..c537090 --- /dev/null +++ b/include/llp_protocol.h @@ -0,0 +1,474 @@ +/** + * llp_protocol.h — LLP (Layered Link Protocol) v3.0.0 + * + * Single-header C implementation compatible with LLP Java library v3.0.0. + * Designed for Arduino IDE and any C99-compatible embedded environment. + * + * Wire format (transport frame): + * [0xAA][0x55][LEN_L][LEN_H][PAYLOAD...][CRC_L][CRC_H] + * + * Byte stuffing: every 0xAA byte in LEN, PAYLOAD or CRC is written as 0xAA 0x00. + * An unexpected 0xAA 0x55 sequence inside a frame signals a resync event. + * + * Payload format (layer chain): + * [LAYER_ID][META_LEN][METADATA...] ... [0x00][RAW APPLICATION DATA] + * + * Layer ID rules: + * 0x00 -> FinalNode: no more layers, raw bytes follow + * 0x01-0x7F -> Passthrough layer: metadata can be skipped, payload is unchanged + * 0x80-0xFE -> Transform layer: payload was modified (encrypt/compress), cannot skip + * 0xFF -> Reserved + * + * META_LEN encoding: + * 0-254 -> 1 byte (direct value) + * 255+ -> 3 bytes: 0xFF followed by 2 bytes big-endian + * + * MIGRATION FROM v2.x: + * - llp_build_frame() signature changed: removed type, id, version parameters. + * Use llp_build_final_payload() to wrap raw data, then pass to llp_build_frame(). + * - Output buffer must be sized with LLP_MAX_FRAME_SIZE(payload_len) due to stuffing. + * - llp_parser_t no longer exposes frame.type / frame.id / frame.version. + * - After a successful parse, use llp_get_final_payload() or llp_find_layer() to + * navigate the received layer chain. + */ + +#ifndef LLP_PROTOCOL_H +#define LLP_PROTOCOL_H + +#include +#include + +// ============================================================================= +// CONFIG +// ============================================================================= + +#define LLP_MAGIC_1 ((uint8_t)0xAA) +#define LLP_MAGIC_2 ((uint8_t)0x55) + +/** Maximum payload (layer chain) size in bytes. Reduce to 64 for AVR boards. */ +#ifndef LLP_MAX_PAYLOAD +#define LLP_MAX_PAYLOAD 128 +#endif + +/** Inter-byte timeout in milliseconds before the parser resets. */ +#ifndef LLP_FRAME_TIMEOUT_MS +#define LLP_FRAME_TIMEOUT_MS 2000 +#endif + +/** + * Worst-case output buffer size for llp_build_frame(). + * Every byte in LEN, PAYLOAD and CRC can be stuffed, doubling its cost. + * Magic(2) + StuffedLen(4) + StuffedPayload(n*2) + StuffedCRC(4) + */ +#define LLP_MAX_FRAME_SIZE(payload_len) (2u + 4u + ((payload_len) * 2u) + 4u) + +// ============================================================================= +// LAYER ID HELPERS +// ============================================================================= + +#define LLP_LAYER_ID_FINAL ((uint8_t)0x00) +#define LLP_LAYER_ID_RESERVED ((uint8_t)0xFF) + +/** Returns 1 if the layer ID is passthrough (safe to skip without a handler). */ +#define LLP_LAYER_IS_PASSTHROUGH(id) ((id) >= 0x01 && (id) <= 0x7F) + +/** Returns 1 if the layer ID signals a payload transformation (cannot skip). */ +#define LLP_LAYER_IS_TRANSFORM(id) ((id) >= 0x80 && (id) <= 0xFE) + +/** Returns 1 if this is the FinalNode marker (end of the layer chain). */ +#define LLP_LAYER_IS_FINAL(id) ((id) == LLP_LAYER_ID_FINAL) + +// ============================================================================= +// ERROR CODES +// ============================================================================= + +typedef enum { + LLP_ERR_OK = 0x00, + LLP_ERR_CHECKSUM = 0x01, + LLP_ERR_PAYLOAD_LEN = 0x02, + LLP_ERR_TIMEOUT = 0x03, + LLP_ERR_SYNC = 0x04, + LLP_ERR_BUFFER_FULL = 0x05, + LLP_ERR_TRANSFORM_LAYER = 0x06, + LLP_ERR_MALFORMED_LAYER = 0x07 +} llp_error_t; + +// ============================================================================= +// CRC16-CCITT +// ============================================================================= + +static inline uint16_t llp_crc16_update(uint16_t crc, uint8_t data) { + crc ^= (uint16_t)data << 8; + for (int i = 0; i < 8; i++) { + crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1); + } + return crc; +} + +// ============================================================================= +// DATA STRUCTURES +// ============================================================================= + +typedef struct { + uint8_t payload[LLP_MAX_PAYLOAD]; + uint16_t payload_len; + uint16_t crc; +} llp_frame_t; + +typedef struct { + uint8_t layer_id; + uint16_t meta_offset; + uint16_t meta_len; + uint16_t payload_offset; + uint16_t payload_len; +} llp_layer_info_t; + +// ============================================================================= +// PARSER STATE MACHINE +// ============================================================================= + +typedef enum { + LLP_STATE_WAIT_MAGIC1, + LLP_STATE_WAIT_MAGIC2, + LLP_STATE_READ_LEN_L, + LLP_STATE_READ_LEN_H, + LLP_STATE_READ_PAYLOAD, + LLP_STATE_READ_CRC_L, + LLP_STATE_READ_CRC_H +} llp_parser_state_t; + +typedef struct { + llp_parser_state_t state; + + uint8_t escape_pending; + + uint16_t payload_idx; + uint16_t crc_received; + uint16_t crc_calculated; + + unsigned long last_byte_time; + + llp_frame_t frame; + uint8_t error_code; + + uint32_t frames_ok; + uint32_t frames_error; + uint32_t timeouts; +} llp_parser_t; + +// ============================================================================= +// PARSER — INTERNAL HELPERS +// ============================================================================= + +static inline void _llp_parser_reset(llp_parser_t* p) { + p->state = LLP_STATE_WAIT_MAGIC1; + p->escape_pending = 0; + p->payload_idx = 0; + p->crc_calculated = 0xFFFF; +} + +// ============================================================================= +// PARSER — PUBLIC API +// ============================================================================= + +static inline void llp_parser_init(llp_parser_t* p) { + _llp_parser_reset(p); + p->crc_received = 0; + p->last_byte_time = 0; + p->error_code = LLP_ERR_OK; + p->frames_ok = 0; + p->frames_error = 0; + p->timeouts = 0; + p->frame.payload_len = 0; +} + +static inline int llp_parser_process_byte(llp_parser_t* p, uint8_t byte, + unsigned long current_ms) { + if (p->state != LLP_STATE_WAIT_MAGIC1) { + if (current_ms - p->last_byte_time > LLP_FRAME_TIMEOUT_MS) { + p->error_code = LLP_ERR_TIMEOUT; + p->timeouts++; + _llp_parser_reset(p); + p->last_byte_time = current_ms; + if (byte == LLP_MAGIC_1) p->state = LLP_STATE_WAIT_MAGIC2; + return -1; + } + } + p->last_byte_time = current_ms; + + if (p->state != LLP_STATE_WAIT_MAGIC1 && + p->state != LLP_STATE_WAIT_MAGIC2) { + + if (p->escape_pending) { + p->escape_pending = 0; + + if (byte == LLP_MAGIC_2) { + p->error_code = LLP_ERR_SYNC; + p->frames_error++; + + p->crc_calculated = 0xFFFF; + p->crc_calculated = llp_crc16_update(p->crc_calculated, LLP_MAGIC_1); + p->crc_calculated = llp_crc16_update(p->crc_calculated, LLP_MAGIC_2); + p->payload_idx = 0; + p->state = LLP_STATE_READ_LEN_L; + return -1; + + } else if (byte == 0x00) { + byte = LLP_MAGIC_1; + + } else { + p->error_code = LLP_ERR_SYNC; + p->frames_error++; + _llp_parser_reset(p); + return -1; + } + + } else if (byte == LLP_MAGIC_1) { + p->escape_pending = 1; + return 0; + } + } + + switch (p->state) { + + case LLP_STATE_WAIT_MAGIC1: + if (byte == LLP_MAGIC_1) { + p->state = LLP_STATE_WAIT_MAGIC2; + } + break; + + case LLP_STATE_WAIT_MAGIC2: + if (byte == LLP_MAGIC_2) { + p->crc_calculated = 0xFFFF; + p->crc_calculated = llp_crc16_update(p->crc_calculated, LLP_MAGIC_1); + p->crc_calculated = llp_crc16_update(p->crc_calculated, LLP_MAGIC_2); + p->state = LLP_STATE_READ_LEN_L; + } else if (byte == LLP_MAGIC_1) { + p->state = LLP_STATE_WAIT_MAGIC2; + } else { + p->state = LLP_STATE_WAIT_MAGIC1; + } + break; + + case LLP_STATE_READ_LEN_L: + p->frame.payload_len = byte; + p->crc_calculated = llp_crc16_update(p->crc_calculated, byte); + p->state = LLP_STATE_READ_LEN_H; + break; + + case LLP_STATE_READ_LEN_H: + p->frame.payload_len |= ((uint16_t)byte << 8); + p->crc_calculated = llp_crc16_update(p->crc_calculated, byte); + + if (p->frame.payload_len > LLP_MAX_PAYLOAD) { + p->error_code = LLP_ERR_PAYLOAD_LEN; + p->frames_error++; + _llp_parser_reset(p); + return -1; + } + + p->payload_idx = 0; + p->state = (p->frame.payload_len == 0) + ? LLP_STATE_READ_CRC_L + : LLP_STATE_READ_PAYLOAD; + break; + + case LLP_STATE_READ_PAYLOAD: + p->frame.payload[p->payload_idx++] = byte; + p->crc_calculated = llp_crc16_update(p->crc_calculated, byte); + if (p->payload_idx == p->frame.payload_len) { + p->state = LLP_STATE_READ_CRC_L; + } + break; + + case LLP_STATE_READ_CRC_L: + p->crc_received = byte; + p->state = LLP_STATE_READ_CRC_H; + break; + + case LLP_STATE_READ_CRC_H: + p->crc_received |= ((uint16_t)byte << 8); + + if (p->crc_received != p->crc_calculated) { + p->error_code = LLP_ERR_CHECKSUM; + p->frames_error++; + _llp_parser_reset(p); + return -1; + } + + p->frame.crc = p->crc_calculated; + p->frames_ok++; + _llp_parser_reset(p); + return 1; + + default: + _llp_parser_reset(p); + break; + } + + return 0; +} + +// ============================================================================= +// LAYER CHAIN HELPERS — INTERNAL +// ============================================================================= + +static inline uint8_t _llp_read_meta_len(const uint8_t* buf, uint16_t buf_len, + uint16_t offset, uint16_t* out_meta_len) { + if (offset >= buf_len) return 0; + + if (buf[offset] < 0xFF) { + *out_meta_len = buf[offset]; + return 1; + } + + if ((uint16_t)(offset + 3) > buf_len) return 0; + *out_meta_len = ((uint16_t)buf[offset + 1] << 8) | buf[offset + 2]; + return 3; +} + +// ============================================================================= +// LAYER CHAIN HELPERS — PUBLIC API +// ============================================================================= + +static inline int llp_find_layer(const llp_frame_t* frame, uint8_t target_id, + llp_layer_info_t* out) { + const uint8_t* buf = frame->payload; + const uint16_t total = frame->payload_len; + uint16_t pos = 0; + + while (pos < total) { + uint8_t layer_id = buf[pos++]; + + if (LLP_LAYER_IS_FINAL(layer_id)) return 0; + + uint16_t meta_len = 0; + uint8_t len_bytes = _llp_read_meta_len(buf, total, pos, &meta_len); + if (len_bytes == 0) return -1; + pos += len_bytes; + if (pos + meta_len > total) return -1; + + if (layer_id == target_id) { + if (out) { + out->layer_id = layer_id; + out->meta_offset = pos; + out->meta_len = meta_len; + out->payload_offset = pos + meta_len; + out->payload_len = total - (pos + meta_len); + } + return 1; + } + + if (LLP_LAYER_IS_TRANSFORM(layer_id)) return -1; + + pos += meta_len; + } + + return 0; +} + +static inline int llp_get_final_payload(const llp_frame_t* frame, + uint8_t* out_buf, uint16_t out_buf_size) { + const uint8_t* buf = frame->payload; + const uint16_t total = frame->payload_len; + uint16_t pos = 0; + + while (pos < total) { + uint8_t layer_id = buf[pos++]; + + if (LLP_LAYER_IS_FINAL(layer_id)) { + uint16_t raw_len = total - pos; + if (raw_len > out_buf_size) return -1; + if (raw_len > 0) memcpy(out_buf, &buf[pos], raw_len); + return (int)raw_len; + } + + uint16_t meta_len = 0; + uint8_t len_bytes = _llp_read_meta_len(buf, total, pos, &meta_len); + if (len_bytes == 0) return -1; + pos += len_bytes; + if (pos + meta_len > total) return -1; + + if (LLP_LAYER_IS_TRANSFORM(layer_id)) return -1; + + pos += meta_len; + } + + return -1; +} + +// ============================================================================= +// FRAME BUILDER — INTERNAL +// ============================================================================= + +static inline void _llp_write_stuffed(uint8_t* buf, size_t* idx, + uint8_t byte, uint16_t* crc) { + if (crc) *crc = llp_crc16_update(*crc, byte); + buf[(*idx)++] = byte; + if (byte == LLP_MAGIC_1) { + buf[(*idx)++] = 0x00; + } +} + +// ============================================================================= +// FRAME BUILDER — PUBLIC API +// ============================================================================= + +static inline size_t llp_build_final_payload(uint8_t* out_buf, size_t out_buf_size, + const uint8_t* raw_data, uint16_t raw_len) { + if (out_buf_size < (size_t)raw_len + 1u) return 0; + out_buf[0] = LLP_LAYER_ID_FINAL; + if (raw_len > 0 && raw_data != NULL) { + memcpy(&out_buf[1], raw_data, raw_len); + } + return (size_t)raw_len + 1u; +} + +static inline size_t llp_build_frame(uint8_t* out_buf, size_t out_buf_size, + const uint8_t* llp_payload, uint16_t llp_payload_len) { + if (llp_payload_len > LLP_MAX_PAYLOAD) return 0; + + size_t worst_case = LLP_MAX_FRAME_SIZE(llp_payload_len); + if (out_buf_size < worst_case) return 0; + + size_t idx = 0; + uint16_t crc = 0xFFFF; + + out_buf[idx++] = LLP_MAGIC_1; + out_buf[idx++] = LLP_MAGIC_2; + crc = llp_crc16_update(crc, LLP_MAGIC_1); + crc = llp_crc16_update(crc, LLP_MAGIC_2); + + _llp_write_stuffed(out_buf, &idx, (uint8_t)(llp_payload_len & 0xFF), &crc); + _llp_write_stuffed(out_buf, &idx, (uint8_t)(llp_payload_len >> 8), &crc); + + for (uint16_t i = 0; i < llp_payload_len; i++) { + _llp_write_stuffed(out_buf, &idx, llp_payload[i], &crc); + } + + _llp_write_stuffed(out_buf, &idx, (uint8_t)(crc & 0xFF), NULL); + _llp_write_stuffed(out_buf, &idx, (uint8_t)(crc >> 8), NULL); + + return idx; +} + +// ============================================================================= +// STATISTICS +// ============================================================================= + +static inline void llp_get_stats(const llp_parser_t* p, + uint32_t* frames_ok, + uint32_t* frames_error, + uint32_t* timeouts) { + if (frames_ok) *frames_ok = p->frames_ok; + if (frames_error) *frames_error = p->frames_error; + if (timeouts) *timeouts = p->timeouts; +} + +static inline void llp_reset_stats(llp_parser_t* p) { + p->frames_ok = 0; + p->frames_error = 0; + p->timeouts = 0; +} + +#endif /* LLP_PROTOCOL_H */ diff --git a/library.properties b/library.properties index afcc1d3..b6ed8b6 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ -LLP Protocol -version=1.0.0 -author=EnzoLeonel +name=LLP Protocol +version=3.1.0 +author=flamicomm maintainer=EnzoLeonel sentence=Lightweight Link Protocol for microcontrollers paragraph=A robust, extensible serial protocol for Arduino, ESP8266, STM32 and embedded systems. Supports UART, RF 433MHz, RS485, LoRa and other communication mediums. category=Communication -url=https://github.com/EnzoLeonel/llp-protocol +url=https://github.com/flamicomm/llp-protocol architectures=* includes=llp_protocol.h diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..2572b93 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,127 @@ +[platformio] +src_dir = src +lib_dir = lib +include_dir = include +test_dir = test +default_envs = test + +# ================================================================ +# TESTING ENVIRONMENT +# ================================================================ +[env:test] +platform = native +test_framework = unity +build_flags = + -Wall + -Wextra + -std=c99 + -pedantic + -coverage + -DLLP_MAX_PAYLOAD=512 + -DLLP_FRAME_TIMEOUT_MS=2000 +lib_deps = + throwtheswitch/Unity +extra_scripts = + scripts/platformio_coverage.py + +# ================================================================ +# ARDUINO ENVIRONMENTS +# ================================================================ + +[env:arduino_uno] +platform = atmelavr +board = uno +framework = arduino +monitor_speed = 9600 +upload_speed = 115200 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=64 +lib_ignore = + Unity + +[env:arduino_nano] +platform = atmelavr +board = nanoatmega328 +framework = arduino +monitor_speed = 9600 +upload_speed = 115200 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=64 +lib_ignore = + Unity + +[env:arduino_mega] +platform = atmelavr +board = megaatmega2560 +framework = arduino +monitor_speed = 115200 +upload_speed = 115200 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=512 +lib_ignore = + Unity + +# ================================================================ +# ESP8266 ENVIRONMENT +# ================================================================ + +[env:esp8266] +platform = espressif8266 +board = esp8266 +framework = arduino +monitor_speed = 115200 +upload_speed = 921600 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=256 +lib_ignore = + Unity + +# ================================================================ +# ESP32 ENVIRONMENT +# ================================================================ + +[env:esp32] +platform = espressif32 +board = esp32doit-devkit-v1 +framework = arduino +monitor_speed = 115200 +upload_speed = 921600 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=512 +lib_ignore = + Unity + +# ================================================================ +# STM32 ENVIRONMENT (Nucleo) +# ================================================================ + +[env:stm32f103] +platform = ststm32 +board = nucleo_f103rb +framework = stm32cube +monitor_speed = 9600 +upload_speed = 115200 +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=256 +lib_ignore = + Unity + +# ================================================================ +# ATTINY ENVIRONMENT +# ================================================================ + +[env:attiny85] +platform = atmelavr +board = attiny85 +framework = arduino +build_flags = + -Wall + -DLLP_MAX_PAYLOAD=32 +lib_ignore = + Unity diff --git a/scripts/platformio_coverage.py b/scripts/platformio_coverage.py new file mode 100644 index 0000000..b67cb6e --- /dev/null +++ b/scripts/platformio_coverage.py @@ -0,0 +1,4 @@ +Import("env") + +# Add gcov library to support coverage instrumentation +env.Append(LIBS=["gcov"]) diff --git a/src/llp_protocol.c b/src/llp_protocol.c new file mode 100644 index 0000000..a2e7913 --- /dev/null +++ b/src/llp_protocol.c @@ -0,0 +1 @@ +#include "llp_protocol.h" diff --git a/test/test_crc.c b/test/test_crc.c new file mode 100644 index 0000000..6b498a4 --- /dev/null +++ b/test/test_crc.c @@ -0,0 +1,92 @@ +#include +#include +#include "llp_protocol.h" + +static uint16_t crc_compute(const uint8_t* data, uint16_t len) +{ + uint16_t crc = 0xFFFF; + for (uint16_t i = 0; i < len; i++) { + crc = llp_crc16_update(crc, data[i]); + } + return crc; +} + +void test_crc_known_values(void) +{ + uint16_t crc = llp_crc16_update(0xFFFF, 0x00); + TEST_ASSERT_NOT_EQUAL_INT(0, crc); + + crc = llp_crc16_update(0xFFFF, 0xAA); + uint16_t crc_aa = crc; + crc = llp_crc16_update(crc, 0x55); + TEST_ASSERT_NOT_EQUAL(crc_aa, crc); + + const uint8_t test_vect[] = {0x31, 0x32, 0x33, 0x34, + 0x35, 0x36, 0x37, 0x38, 0x39}; + crc = crc_compute(test_vect, sizeof(test_vect)); + TEST_ASSERT_EQUAL_HEX16(0x29B1, crc); +} + +void test_crc_differs_for_different_payloads(void) +{ + uint16_t crc_a = crc_compute((const uint8_t*)"HELLO", 5); + uint16_t crc_b = crc_compute((const uint8_t*)"WORLD", 5); + TEST_ASSERT_NOT_EQUAL(crc_a, crc_b); +} + +void test_crc_detects_bit_flip(void) +{ + uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; + uint16_t crc_original = crc_compute(data, sizeof(data)); + + data[2] ^= 0x01; + uint16_t crc_flipped = crc_compute(data, sizeof(data)); + TEST_ASSERT_NOT_EQUAL(crc_original, crc_flipped); +} + +void test_crc_detects_burst_error(void) +{ + uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + uint16_t crc_original = crc_compute(data, sizeof(data)); + + data[3] ^= 0xFF; + data[4] ^= 0xFF; + data[5] ^= 0xFF; + uint16_t crc_burst = crc_compute(data, sizeof(data)); + TEST_ASSERT_NOT_EQUAL(crc_original, crc_burst); +} + +void test_crc_deterministic(void) +{ + uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + uint16_t crc1 = crc_compute(data, sizeof(data)); + uint16_t crc2 = crc_compute(data, sizeof(data)); + TEST_ASSERT_EQUAL_HEX16(crc1, crc2); +} + +void test_crc_update_chain(void) +{ + uint16_t crc = 0xFFFF; + crc = llp_crc16_update(crc, 0xAA); + crc = llp_crc16_update(crc, 0x55); + crc = llp_crc16_update(crc, 0x01); + crc = llp_crc16_update(crc, 0x00); + + uint8_t all[] = {0xAA, 0x55, 0x01, 0x00}; + uint16_t crc_bulk = crc_compute(all, sizeof(all)); + + TEST_ASSERT_EQUAL_HEX16(crc_bulk, crc); +} + +void test_crc_order_matters(void) +{ + uint16_t crc_ab = crc_compute((const uint8_t*)"AB", 2); + uint16_t crc_ba = crc_compute((const uint8_t*)"BA", 2); + TEST_ASSERT_NOT_EQUAL(crc_ab, crc_ba); +} + +void test_crc_empty_input(void) +{ + uint16_t crc = crc_compute(NULL, 0); + TEST_ASSERT_EQUAL_HEX16(0xFFFF, crc); +} diff --git a/test/test_frame_builder.c b/test/test_frame_builder.c new file mode 100644 index 0000000..151f34f --- /dev/null +++ b/test/test_frame_builder.c @@ -0,0 +1,162 @@ +#include +#include +#include "llp_protocol.h" + +void test_fb_final_payload_empty(void) +{ + uint8_t buf[10]; + size_t len = llp_build_final_payload(buf, sizeof(buf), NULL, 0); + TEST_ASSERT_EQUAL_INT(1, len); + TEST_ASSERT_EQUAL_HEX8(LLP_LAYER_ID_FINAL, buf[0]); +} + +void test_fb_final_payload_with_data(void) +{ + uint8_t buf[10]; + uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + size_t len = llp_build_final_payload(buf, sizeof(buf), data, 4); + TEST_ASSERT_EQUAL_INT(5, len); + TEST_ASSERT_EQUAL_HEX8(LLP_LAYER_ID_FINAL, buf[0]); + TEST_ASSERT_EQUAL_HEX8(0xDE, buf[1]); + TEST_ASSERT_EQUAL_HEX8(0xAD, buf[2]); + TEST_ASSERT_EQUAL_HEX8(0xBE, buf[3]); + TEST_ASSERT_EQUAL_HEX8(0xEF, buf[4]); +} + +void test_fb_final_payload_small_buffer(void) +{ + uint8_t buf[2]; + uint8_t data[] = {0x01, 0x02}; + size_t len = llp_build_final_payload(buf, sizeof(buf), data, 2); + TEST_ASSERT_EQUAL_INT(0, len); +} + +void test_fb_magic_bytes(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL}; + uint8_t buf[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), payload, sizeof(payload)); + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); + TEST_ASSERT_EQUAL_HEX8(LLP_MAGIC_1, buf[0]); + TEST_ASSERT_EQUAL_HEX8(LLP_MAGIC_2, buf[1]); +} + +void test_fb_payload_length_field(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL, 0x01, 0x02, 0x03}; + uint8_t buf[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), payload, sizeof(payload)); + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); + TEST_ASSERT_EQUAL_HEX8(sizeof(payload) & 0xFF, buf[2]); + TEST_ASSERT_EQUAL_HEX8((sizeof(payload) >> 8) & 0xFF, buf[3]); +} + +void test_fb_roundtrip(void) +{ + uint8_t original_data[] = {0x01, 0x02, 0x03, 0x04}; + uint8_t llp_payload[LLP_MAX_PAYLOAD]; + size_t llp_payload_len = llp_build_final_payload( + llp_payload, sizeof(llp_payload), + original_data, sizeof(original_data)); + + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(llp_payload_len)]; + size_t frame_len = llp_build_frame(frame_buf, sizeof(frame_buf), + llp_payload, llp_payload_len); + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + TEST_ASSERT_EQUAL_INT(4, extracted_len); + TEST_ASSERT_EQUAL_HEX8(0x01, extracted[0]); + TEST_ASSERT_EQUAL_HEX8(0x02, extracted[1]); + TEST_ASSERT_EQUAL_HEX8(0x03, extracted[2]); + TEST_ASSERT_EQUAL_HEX8(0x04, extracted[3]); +} + +void test_fb_small_buffer(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL}; + uint8_t buf[2]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), payload, sizeof(payload)); + TEST_ASSERT_EQUAL_INT(0, frame_len); +} + +void test_fb_oversized_payload(void) +{ + uint8_t big_payload[LLP_MAX_PAYLOAD + 1]; + memset(big_payload, 0xAA, sizeof(big_payload)); + big_payload[0] = LLP_LAYER_ID_FINAL; + + uint8_t buf[LLP_MAX_FRAME_SIZE(LLP_MAX_PAYLOAD + 1)]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), + big_payload, LLP_MAX_PAYLOAD + 1); + TEST_ASSERT_EQUAL_INT(0, frame_len); +} + +void test_fb_stuffing(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL, 0xAA}; + uint8_t buf[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), payload, sizeof(payload)); + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + TEST_ASSERT_EQUAL_INT(1, extracted_len); + TEST_ASSERT_EQUAL_HEX8(0xAA, extracted[0]); +} + +void test_fb_deterministic(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL, 0x42}; + uint8_t buf1[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + uint8_t buf2[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + + size_t len1 = llp_build_frame(buf1, sizeof(buf1), payload, sizeof(payload)); + size_t len2 = llp_build_frame(buf2, sizeof(buf2), payload, sizeof(payload)); + + TEST_ASSERT_EQUAL_INT(len1, len2); + TEST_ASSERT_EQUAL_INT8(0, memcmp(buf1, buf2, len1)); +} + +void test_fb_empty_payload_chain(void) +{ + uint8_t payload[] = {LLP_LAYER_ID_FINAL}; + uint8_t buf[LLP_MAX_FRAME_SIZE(sizeof(payload))]; + size_t frame_len = llp_build_frame(buf, sizeof(buf), payload, sizeof(payload)); + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + + uint8_t extracted[1]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + TEST_ASSERT_EQUAL_INT(0, extracted_len); +} diff --git a/test/test_main.c b/test/test_main.c new file mode 100644 index 0000000..578accc --- /dev/null +++ b/test/test_main.c @@ -0,0 +1,271 @@ +#include + +void setUp(void) {} +void tearDown(void) {} + +/* ParserInit */ +void test_init_sets_state_wait_magic1(void); +void test_init_clears_all_fields(void); +void test_init_clears_frame_payload_len(void); +void test_init_can_be_reinitialized(void); +void test_init_resets_stats(void); + +/* FrameBuilder */ +void test_fb_final_payload_empty(void); +void test_fb_final_payload_with_data(void); +void test_fb_final_payload_small_buffer(void); +void test_fb_magic_bytes(void); +void test_fb_payload_length_field(void); +void test_fb_roundtrip(void); +void test_fb_small_buffer(void); +void test_fb_oversized_payload(void); +void test_fb_stuffing(void); +void test_fb_deterministic(void); +void test_fb_empty_payload_chain(void); + +/* ParserProcess */ +void test_pp_incomplete_frame_returns_zero(void); +void test_pp_complete_frame_returns_one(void); +void test_pp_extracts_correct_data(void); +void test_pp_crc_error_returns_negative_one(void); +void test_pp_timeout_returns_negative_one(void); +void test_pp_recovers_from_noise(void); +void test_pp_multiple_frames(void); +void test_pp_increments_error_counter(void); +void test_pp_stuffed_byte(void); +void test_pp_sync_error_on_aa55_inside_frame(void); +void test_pp_payload_len_error(void); + +/* Crc */ +void test_crc_known_values(void); +void test_crc_differs_for_different_payloads(void); +void test_crc_detects_bit_flip(void); +void test_crc_detects_burst_error(void); +void test_crc_deterministic(void); +void test_crc_update_chain(void); +void test_crc_order_matters(void); +void test_crc_empty_input(void); + +/* Spec: Transport Valid - Encode */ +void test_spec_transport_valid_encode(void); + +/* Spec: Transport Valid - Decode */ +void test_spec_transport_valid_decode(void); + +/* Spec: Transport Valid - Stream */ +void test_spec_transport_valid_stream_two_empty(void); +void test_spec_transport_valid_stream_empty_then_hello(void); +void test_spec_transport_valid_stream_three_mixed(void); + +/* Spec: Transport CRC */ +void test_spec_transport_crc(void); + +/* Spec: Transport Stuffing */ +void test_spec_transport_stuffing_valid(void); +void test_spec_transport_stuffing_invalid(void); + +/* Spec: Transport Resync */ +void test_spec_transport_resync_noise_before_frame(void); +void test_spec_transport_resync_noise_between_frames(void); +void test_spec_transport_resync_corrupt_magic1(void); +void test_spec_transport_resync_corrupt_magic2(void); +void test_spec_transport_resync_aa_no_false_resync(void); +void test_spec_transport_resync_aa55_no_false_resync(void); +void test_spec_transport_resync_invalid_escape_then_valid(void); +void test_spec_transport_resync_garbage_three_frames(void); + +/* Spec: Transport Truncation */ +void test_spec_transport_truncation_after_magic1(void); +void test_spec_transport_truncation_after_magic2(void); +void test_spec_transport_truncation_after_len_l(void); +void test_spec_transport_truncation_after_len_h(void); +void test_spec_transport_truncation_mid_payload(void); +void test_spec_transport_truncation_mid_crc_low(void); +void test_spec_transport_truncation_empty_stream(void); +void test_spec_transport_truncation_magic_only(void); + +/* Spec: Transport Timeout */ +void test_spec_transport_timeout_mid_frame(void); +void test_spec_transport_timeout_then_valid(void); +void test_spec_transport_timeout_between_frames(void); + +/* Spec: Layers Passthrough */ +void test_spec_layers_passthrough_encode(void); +void test_spec_layers_passthrough_decode(void); +void test_spec_layers_passthrough_extended_meta_zero(void); +void test_spec_layers_passthrough_extended_meta_255(void); + +/* Spec: Layers Transform */ +void test_spec_layers_transform_encode(void); +void test_spec_layers_transform_decode(void); + +/* Spec: Layers Malformed */ +void test_spec_layers_malformed_truncated_metadata(void); +void test_spec_layers_malformed_empty_payload(void); +void test_spec_layers_malformed_extended_meta_truncated(void); +void test_spec_layers_malformed_reserved_id_FF(void); + +/* Spec: Layers Traversal */ +void test_spec_layers_traversal_three_passthrough(void); +void test_spec_layers_traversal_single_passthrough(void); +void test_spec_layers_traversal_direct_finalnode(void); +void test_spec_layers_traversal_empty_final_payload(void); +void test_spec_layers_traversal_transform_blocked(void); + +/* Spec: Parser Incremental */ +void test_spec_parser_incremental_byte_by_byte_hello(void); +void test_spec_parser_incremental_two_bytes(void); +void test_spec_parser_incremental_mixed_chunks(void); + +/* Spec: Parser Fragmented */ +void test_spec_parser_fragmented_after_magic1(void); +void test_spec_parser_fragmented_mid_stuffing(void); +void test_spec_parser_fragmented_at_crc_boundary(void); + +/* Spec: Parser Recovery */ +void test_spec_parser_recovery_after_crc_error(void); +void test_spec_parser_recovery_after_sync_error(void); +void test_spec_parser_recovery_garbage_then_two_frames(void); +void test_spec_parser_recovery_multiple_errors_then_valid(void); + +/* Bug regression tests */ +void test_bug_timeout_does_not_update_last_byte_time(void); +void test_bug_timeout_between_frames(void); +void test_bug_final_payload_returns_zero_without_final_node(void); + +int main(void) +{ + UNITY_BEGIN(); + + /* ParserInit (5 tests) */ + RUN_TEST(test_init_sets_state_wait_magic1); + RUN_TEST(test_init_clears_all_fields); + RUN_TEST(test_init_clears_frame_payload_len); + RUN_TEST(test_init_can_be_reinitialized); + RUN_TEST(test_init_resets_stats); + + /* FrameBuilder (11 tests) */ + RUN_TEST(test_fb_final_payload_empty); + RUN_TEST(test_fb_final_payload_with_data); + RUN_TEST(test_fb_final_payload_small_buffer); + RUN_TEST(test_fb_magic_bytes); + RUN_TEST(test_fb_payload_length_field); + RUN_TEST(test_fb_roundtrip); + RUN_TEST(test_fb_small_buffer); + RUN_TEST(test_fb_oversized_payload); + RUN_TEST(test_fb_stuffing); + RUN_TEST(test_fb_deterministic); + RUN_TEST(test_fb_empty_payload_chain); + + /* ParserProcess (11 tests) */ + RUN_TEST(test_pp_incomplete_frame_returns_zero); + RUN_TEST(test_pp_complete_frame_returns_one); + RUN_TEST(test_pp_extracts_correct_data); + RUN_TEST(test_pp_crc_error_returns_negative_one); + RUN_TEST(test_pp_timeout_returns_negative_one); + RUN_TEST(test_pp_recovers_from_noise); + RUN_TEST(test_pp_multiple_frames); + RUN_TEST(test_pp_increments_error_counter); + RUN_TEST(test_pp_stuffed_byte); + RUN_TEST(test_pp_sync_error_on_aa55_inside_frame); + RUN_TEST(test_pp_payload_len_error); + + /* Crc (8 tests) */ + RUN_TEST(test_crc_known_values); + RUN_TEST(test_crc_differs_for_different_payloads); + RUN_TEST(test_crc_detects_bit_flip); + RUN_TEST(test_crc_detects_burst_error); + RUN_TEST(test_crc_deterministic); + RUN_TEST(test_crc_update_chain); + RUN_TEST(test_crc_order_matters); + RUN_TEST(test_crc_empty_input); + + /* Spec: Transport Valid Encode (31 vectors) */ + RUN_TEST(test_spec_transport_valid_encode); + + /* Spec: Transport Valid Decode (31 vectors) */ + RUN_TEST(test_spec_transport_valid_decode); + + /* Spec: Transport Valid Stream (3 vectors) */ + RUN_TEST(test_spec_transport_valid_stream_two_empty); + RUN_TEST(test_spec_transport_valid_stream_empty_then_hello); + RUN_TEST(test_spec_transport_valid_stream_three_mixed); + + /* Spec: Transport CRC (28 vectors) */ + RUN_TEST(test_spec_transport_crc); + + /* Spec: Transport Stuffing (4 valid + 4 invalid) */ + RUN_TEST(test_spec_transport_stuffing_valid); + RUN_TEST(test_spec_transport_stuffing_invalid); + + /* Spec: Transport Resync (8 vectors) */ + RUN_TEST(test_spec_transport_resync_noise_before_frame); + RUN_TEST(test_spec_transport_resync_noise_between_frames); + RUN_TEST(test_spec_transport_resync_corrupt_magic1); + RUN_TEST(test_spec_transport_resync_corrupt_magic2); + RUN_TEST(test_spec_transport_resync_aa_no_false_resync); + RUN_TEST(test_spec_transport_resync_aa55_no_false_resync); + RUN_TEST(test_spec_transport_resync_invalid_escape_then_valid); + RUN_TEST(test_spec_transport_resync_garbage_three_frames); + + /* Spec: Transport Truncation (8 vectors) */ + RUN_TEST(test_spec_transport_truncation_after_magic1); + RUN_TEST(test_spec_transport_truncation_after_magic2); + RUN_TEST(test_spec_transport_truncation_after_len_l); + RUN_TEST(test_spec_transport_truncation_after_len_h); + RUN_TEST(test_spec_transport_truncation_mid_payload); + RUN_TEST(test_spec_transport_truncation_mid_crc_low); + RUN_TEST(test_spec_transport_truncation_empty_stream); + RUN_TEST(test_spec_transport_truncation_magic_only); + + /* Spec: Transport Timeout (3 vectors) */ + RUN_TEST(test_spec_transport_timeout_mid_frame); + RUN_TEST(test_spec_transport_timeout_then_valid); + RUN_TEST(test_spec_transport_timeout_between_frames); + + /* Spec: Layers Passthrough (15 encode + 15 decode + 2 extended) */ + RUN_TEST(test_spec_layers_passthrough_encode); + RUN_TEST(test_spec_layers_passthrough_decode); + RUN_TEST(test_spec_layers_passthrough_extended_meta_zero); + RUN_TEST(test_spec_layers_passthrough_extended_meta_255); + + /* Spec: Layers Transform (6 encode + 7 decode) */ + RUN_TEST(test_spec_layers_transform_encode); + RUN_TEST(test_spec_layers_transform_decode); + + /* Spec: Layers Malformed (4 vectors) */ + RUN_TEST(test_spec_layers_malformed_truncated_metadata); + RUN_TEST(test_spec_layers_malformed_empty_payload); + RUN_TEST(test_spec_layers_malformed_extended_meta_truncated); + RUN_TEST(test_spec_layers_malformed_reserved_id_FF); + + /* Spec: Layers Traversal (5 vectors) */ + RUN_TEST(test_spec_layers_traversal_three_passthrough); + RUN_TEST(test_spec_layers_traversal_single_passthrough); + RUN_TEST(test_spec_layers_traversal_direct_finalnode); + RUN_TEST(test_spec_layers_traversal_empty_final_payload); + RUN_TEST(test_spec_layers_traversal_transform_blocked); + + /* Spec: Parser Incremental (3 vectors) */ + RUN_TEST(test_spec_parser_incremental_byte_by_byte_hello); + RUN_TEST(test_spec_parser_incremental_two_bytes); + RUN_TEST(test_spec_parser_incremental_mixed_chunks); + + /* Spec: Parser Fragmented (3 vectors) */ + RUN_TEST(test_spec_parser_fragmented_after_magic1); + RUN_TEST(test_spec_parser_fragmented_mid_stuffing); + RUN_TEST(test_spec_parser_fragmented_at_crc_boundary); + + /* Spec: Parser Recovery (4 vectors) */ + RUN_TEST(test_spec_parser_recovery_after_crc_error); + RUN_TEST(test_spec_parser_recovery_after_sync_error); + RUN_TEST(test_spec_parser_recovery_garbage_then_two_frames); + RUN_TEST(test_spec_parser_recovery_multiple_errors_then_valid); + + /* Bug tests */ + RUN_TEST(test_bug_timeout_does_not_update_last_byte_time); + RUN_TEST(test_bug_timeout_between_frames); + RUN_TEST(test_bug_final_payload_returns_zero_without_final_node); + + return UNITY_END(); +} \ No newline at end of file diff --git a/test/test_parser_init.c b/test/test_parser_init.c new file mode 100644 index 0000000..14213cc --- /dev/null +++ b/test/test_parser_init.c @@ -0,0 +1,55 @@ +#include +#include "llp_protocol.h" + +void test_init_sets_state_wait_magic1(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + TEST_ASSERT_EQUAL_INT(LLP_STATE_WAIT_MAGIC1, parser.state); +} + +void test_init_clears_all_fields(void) +{ + llp_parser_t parser; + memset(&parser, 0xFF, sizeof(parser)); + + llp_parser_init(&parser); + + TEST_ASSERT_EQUAL_INT(LLP_STATE_WAIT_MAGIC1, parser.state); + TEST_ASSERT_EQUAL_INT(0, parser.escape_pending); + TEST_ASSERT_EQUAL_INT(0, parser.payload_idx); + TEST_ASSERT_EQUAL_INT(LLP_ERR_OK, parser.error_code); + TEST_ASSERT_EQUAL_HEX16(0xFFFF, parser.crc_calculated); + TEST_ASSERT_EQUAL_INT(0, parser.crc_received); + TEST_ASSERT_EQUAL_INT(0, parser.last_byte_time); +} + +void test_init_clears_frame_payload_len(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + TEST_ASSERT_EQUAL_INT(0, parser.frame.payload_len); +} + +void test_init_can_be_reinitialized(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + llp_parser_process_byte(&parser, 0xAA, 0); + llp_parser_process_byte(&parser, 0x55, 0); + + llp_parser_init(&parser); + TEST_ASSERT_EQUAL_INT(LLP_STATE_WAIT_MAGIC1, parser.state); + TEST_ASSERT_EQUAL_INT(0, parser.frames_ok); +} + +void test_init_resets_stats(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + TEST_ASSERT_EQUAL_INT(0, parser.frames_ok); + TEST_ASSERT_EQUAL_INT(0, parser.frames_error); + TEST_ASSERT_EQUAL_INT(0, parser.timeouts); +} diff --git a/test/test_parser_process.c b/test/test_parser_process.c new file mode 100644 index 0000000..9f47d1e --- /dev/null +++ b/test/test_parser_process.c @@ -0,0 +1,219 @@ +#include +#include +#include "llp_protocol.h" + +static size_t build_simple_frame(uint8_t* buf, size_t buf_size, + const uint8_t* data, uint16_t data_len) +{ + uint8_t llp_payload[LLP_MAX_PAYLOAD]; + size_t payload_len = llp_build_final_payload(llp_payload, sizeof(llp_payload), + data, data_len); + return llp_build_frame(buf, buf_size, llp_payload, payload_len); +} + +void test_pp_incomplete_frame_returns_zero(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + int result = llp_parser_process_byte(&parser, LLP_MAGIC_1, 0); + TEST_ASSERT_EQUAL_INT(0, result); + + result = llp_parser_process_byte(&parser, LLP_MAGIC_2, 0); + TEST_ASSERT_EQUAL_INT(0, result); +} + +void test_pp_complete_frame_returns_one(void) +{ + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), NULL, 0); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); +} + +void test_pp_extracts_correct_data(void) +{ + uint8_t data[] = {0xCA, 0xFE, 0xBA, 0xBE}; + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(sizeof(data) + 1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), + data, sizeof(data)); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + TEST_ASSERT_EQUAL_INT(4, extracted_len); + TEST_ASSERT_EQUAL_HEX8(0xCA, extracted[0]); + TEST_ASSERT_EQUAL_HEX8(0xFE, extracted[1]); + TEST_ASSERT_EQUAL_HEX8(0xBA, extracted[2]); + TEST_ASSERT_EQUAL_HEX8(0xBE, extracted[3]); +} + +void test_pp_crc_error_returns_negative_one(void) +{ + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), NULL, 0); + + frame_buf[frame_len - 1] ^= 0xFF; + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + int r = llp_parser_process_byte(&parser, frame_buf[i], 0); + if (r != 0) result = r; + } + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_CHECKSUM, parser.error_code); +} + +void test_pp_timeout_returns_negative_one(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + llp_parser_process_byte(&parser, LLP_MAGIC_1, 0); + llp_parser_process_byte(&parser, LLP_MAGIC_2, 0); + + int result = llp_parser_process_byte(&parser, 0x00, + LLP_FRAME_TIMEOUT_MS + 1); + + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_pp_recovers_from_noise(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + llp_parser_process_byte(&parser, 0xFF, 0); + llp_parser_process_byte(&parser, 0x00, 0); + + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), NULL, 0); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); +} + +void test_pp_multiple_frames(void) +{ + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), NULL, 0); + + llp_parser_t parser; + llp_parser_init(&parser); + + int frames_received = 0; + for (int f = 0; f < 3; f++) { + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + frames_received++; + } + + TEST_ASSERT_EQUAL_INT(3, frames_received); + TEST_ASSERT_EQUAL_INT(3, parser.frames_ok); +} + +void test_pp_increments_error_counter(void) +{ + uint8_t good[LLP_MAX_FRAME_SIZE(1)]; + size_t good_len = build_simple_frame(good, sizeof(good), NULL, 0); + + uint8_t bad[LLP_MAX_FRAME_SIZE(1)]; + size_t bad_len = build_simple_frame(bad, sizeof(bad), NULL, 0); + bad[bad_len - 1] ^= 0xFF; + + llp_parser_t parser; + llp_parser_init(&parser); + + for (size_t i = 0; i < bad_len; i++) { + llp_parser_process_byte(&parser, bad[i], 0); + } + for (size_t i = 0; i < good_len; i++) { + llp_parser_process_byte(&parser, good[i], 0); + } + + TEST_ASSERT_EQUAL_INT(1, parser.frames_ok); + TEST_ASSERT_EQUAL_INT(1, parser.frames_error); +} + +void test_pp_stuffed_byte(void) +{ + uint8_t data[] = {0xAA}; + uint8_t frame_buf[LLP_MAX_FRAME_SIZE(sizeof(data) + 1)]; + size_t frame_len = build_simple_frame(frame_buf, sizeof(frame_buf), + data, sizeof(data)); + + llp_parser_t parser; + llp_parser_init(&parser); + + int result = 0; + for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, frame_buf[i], 0); + } + TEST_ASSERT_EQUAL_INT(1, result); + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + TEST_ASSERT_EQUAL_INT(1, extracted_len); + TEST_ASSERT_EQUAL_HEX8(0xAA, extracted[0]); +} + +void test_pp_sync_error_on_aa55_inside_frame(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + llp_parser_process_byte(&parser, 0xAA, 0); + llp_parser_process_byte(&parser, 0x55, 0); + llp_parser_process_byte(&parser, 0x01, 0); + llp_parser_process_byte(&parser, 0x00, 0); + + int r = llp_parser_process_byte(&parser, 0xAA, 0); + TEST_ASSERT_EQUAL_INT(0, r); + + r = llp_parser_process_byte(&parser, 0x55, 0); + TEST_ASSERT_EQUAL_INT(-1, r); + TEST_ASSERT_EQUAL_INT(LLP_ERR_SYNC, parser.error_code); +} + +void test_pp_payload_len_error(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + + llp_parser_process_byte(&parser, 0xAA, 0); + llp_parser_process_byte(&parser, 0x55, 0); + + int r = llp_parser_process_byte(&parser, (LLP_MAX_PAYLOAD + 1) & 0xFF, 0); + TEST_ASSERT_EQUAL_INT(0, r); + + r = llp_parser_process_byte(&parser, (LLP_MAX_PAYLOAD + 1) >> 8, 0); + TEST_ASSERT_EQUAL_INT(-1, r); + TEST_ASSERT_EQUAL_INT(LLP_ERR_PAYLOAD_LEN, parser.error_code); +} diff --git a/test/test_spec_common.h b/test/test_spec_common.h new file mode 100644 index 0000000..04e83de --- /dev/null +++ b/test/test_spec_common.h @@ -0,0 +1,124 @@ +#ifndef TEST_SPEC_COMMON_H +#define TEST_SPEC_COMMON_H + +#include +#include +#include +#include +#include "llp_protocol.h" + +#define SPEC_MAX_FRAME 4096 +#define SPEC_MAX_EVENTS 32 + +typedef struct { + int type; + int error_code; + uint8_t payload[SPEC_MAX_FRAME]; + uint16_t payload_len; +} spec_event_t; + +static size_t spec_hex_to_bytes(const char *hex, uint8_t *buf, size_t buf_size) +{ + if (!hex || !buf) return 0; + size_t slen = strlen(hex); + if (slen == 0) return 0; + if (slen % 2 != 0) return 0; + size_t count = slen / 2; + if (count > buf_size) return 0; + for (size_t i = 0; i < count; i++) { + unsigned int b; + sscanf(hex + 2 * i, "%02x", &b); + buf[i] = (uint8_t)b; + } + return count; +} + +static int spec_feed_frame(llp_parser_t *p, const uint8_t *data, size_t len, + int *last_result) +{ + int result = 0; + for (size_t i = 0; i < len; i++) { + result = llp_parser_process_byte(p, data[i], 0); + if (result != 0) { + if (last_result) *last_result = result; + } + } + return result; +} + +static int spec_feed_stream(llp_parser_t *p, const uint8_t *data, size_t len, + spec_event_t *events, int *event_count, + int max_events) +{ + *event_count = 0; + for (size_t i = 0; i < len; i++) { + int result = llp_parser_process_byte(p, data[i], 0); + if (result == 1) { + if (*event_count < max_events) { + events[*event_count].type = 1; + memcpy(events[*event_count].payload, + p->frame.payload, p->frame.payload_len); + events[*event_count].payload_len = p->frame.payload_len; + events[*event_count].error_code = 0; + (*event_count)++; + } + } else if (result == -1) { + if (*event_count < max_events) { + events[*event_count].type = -1; + events[*event_count].error_code = p->error_code; + events[*event_count].payload_len = 0; + (*event_count)++; + } + } + } + return *event_count; +} + +static int spec_feed_stream_timed(llp_parser_t *p, + const uint8_t *bytes, + const unsigned long *times, + size_t len, + spec_event_t *events, + int *event_count, + int max_events) +{ + *event_count = 0; + for (size_t i = 0; i < len; i++) { + int result = llp_parser_process_byte(p, bytes[i], times[i]); + if (result == 1) { + if (*event_count < max_events) { + events[*event_count].type = 1; + memcpy(events[*event_count].payload, + p->frame.payload, p->frame.payload_len); + events[*event_count].payload_len = p->frame.payload_len; + events[*event_count].error_code = 0; + (*event_count)++; + } + } else if (result == -1) { + if (*event_count < max_events) { + events[*event_count].type = -1; + events[*event_count].error_code = p->error_code; + events[*event_count].payload_len = 0; + (*event_count)++; + } + } + } + return *event_count; +} + +static void spec_concat_chunks(const char **chunks, int chunk_count, + uint8_t *out, size_t *out_len, + size_t max_len) +{ + *out_len = 0; + for (int c = 0; c < chunk_count; c++) { + uint8_t tmp[SPEC_MAX_FRAME]; + size_t tmp_len = spec_hex_to_bytes(chunks[c], tmp, sizeof(tmp)); + if (*out_len + tmp_len <= max_len) { + memcpy(out + *out_len, tmp, tmp_len); + *out_len += tmp_len; + } + } +} + +#endif \ No newline at end of file diff --git a/test/test_spec_vectors.c b/test/test_spec_vectors.c new file mode 100644 index 0000000..154a626 --- /dev/null +++ b/test/test_spec_vectors.c @@ -0,0 +1,1046 @@ +#include +#include +#include +#include "test_spec_common.h" + +static void verify_encode(const char *name, const char *input_hex, + const char *expected_hex) +{ + uint8_t input[SPEC_MAX_FRAME], expected[SPEC_MAX_FRAME], output[SPEC_MAX_FRAME]; + size_t input_len = spec_hex_to_bytes(input_hex, input, sizeof(input)); + size_t exp_len = spec_hex_to_bytes(expected_hex, expected, sizeof(expected)); + TEST_ASSERT_GREATER_THAN_size_t_MESSAGE(0, exp_len, name); + size_t out_len = llp_build_frame(output, sizeof(output), input, (uint16_t)input_len); + TEST_ASSERT_GREATER_THAN_size_t_MESSAGE(0, out_len, name); + TEST_ASSERT_EQUAL_size_t_MESSAGE(exp_len, out_len, name); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(expected, output, exp_len, name); +} + +static void verify_decode_frame(const char *name, const char *frame_hex, + const char *expected_payload_hex) +{ + uint8_t frame[SPEC_MAX_FRAME], expected[SPEC_MAX_FRAME]; + size_t frame_len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + size_t exp_len = spec_hex_to_bytes(expected_payload_hex, expected, sizeof(expected)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, frame_len, NULL); + TEST_ASSERT_EQUAL_INT_MESSAGE(1, result, name); + TEST_ASSERT_EQUAL_UINT16_MESSAGE((uint16_t)exp_len, parser.frame.payload_len, name); + TEST_ASSERT_EQUAL_UINT8_ARRAY_MESSAGE(expected, parser.frame.payload, exp_len, name); +} + +static void verify_decode_error(const char *name, const char *frame_hex, + int expected_error) +{ + uint8_t frame[SPEC_MAX_FRAME]; + size_t frame_len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int last_result = 0; + spec_feed_frame(&parser, frame, frame_len, &last_result); + TEST_ASSERT_EQUAL_INT_MESSAGE(-1, last_result, name); + TEST_ASSERT_EQUAL_INT_MESSAGE(expected_error, parser.error_code, name); +} + +typedef struct { + const char *name; + const char *input_hex; + const char *expected_hex; +} encode_vector_t; + +typedef struct { + const char *name; + const char *frame_hex; + const char *payload_hex; +} decode_frame_vector_t; + +typedef struct { + const char *name; + const char *frame_hex; + int error_code; +} decode_error_vector_t; + +static const encode_vector_t transport_valid_encode[] = { + {"empty_payload", "00", "AA550100008883"}, + {"single_byte_42", "0042", "AA5502000042B1DA"}, + {"hello_world", "0048656C6C6F", "AA5506000048656C6C6F3798"}, + {"payload_null_byte", "0000", "AA550200000037B2"}, + {"payload_ff_byte", "00FF", "AA55020000FFC7AC"}, + {"payload_aa_byte", "00AA", "AA55020000AA0097A6"}, + {"payload_aa55", "00AA55", "AA55030000AA00552DE2"}, + {"payload_triple_aa", "00AAAAAA", "AA55040000AA00AA00AA00722F"}, + {"payload_mixed_aa", "0001AA02AA03", "AA5506000001AA0002AA0003DBD7"}, + {"payload_zeros_16", "0000000000000000000000000000000000", "AA551100000000000000000000000000000000000036A3"}, + {"payload_ones_16", "0001010101010101010101010101010101", "AA551100000101010101010101010101010101010161B6"}, + {"payload_incremental", "00000102030405060708090A0B0C0D0E0F", "AA55110000000102030405060708090A0B0C0D0E0F0BF2"}, + {"payload_all_ff_16", "00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "AA55110000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77A3"}, + {"payload_aa_prefix", "00AA42", "AA55030000AA0042FB80"}, + {"payload_aa_suffix", "0042AA", "AA5503000042AA00C665"}, + {"payload_aa_boundary", "00AAAA00AA", "AA55050000AA00AA0000AA00F9F9"}, + {"payload_alternating", "00AA00AA00AA", "AA55060000AA0000AA0000AA00D651"}, + {"payload_seq_32", "00000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", "AA55210000000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F9845"}, + {"payload_55_byte", "0055", "AA550200005567B8"}, + {"payload_byte_0x7F", "007F", "AA550200007F4F3D"}, + {"payload_byte_0x80", "0080", "AA5502000080BF23"}, + {"payload_byte_0xFE", "00FE", "AA55020000FEE6BC"}, + {"payload_ascii_test", "0054657374", "AA550500005465737491B9"}, + {"payload_ascii_fox", "0054686520717569636B2062726F776E20666F78", "AA5514000054686520717569636B2062726F776E20666F78BB2C"}, + {"payload_eight_bytes", "000001020304050607", "AA5509000000010203040506079D89"}, + {"payload_fifteen_null", "00000000000000000000000000000000", "AA55100000000000000000000000000000000000EC09"}, + {"payload_thirty_null", "00000000000000000000000000000000000000000000000000000000000000", "AA551F00000000000000000000000000000000000000000000000000000000000000006AC1"}, + {"payload_sixtyfour_seq", "00000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F", "AA55410000000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F07DF"}, + {"payload_repeated_55", "005555555555555555", "AA5509000055555555555555556E3D"}, + {"payload_aa_55_pairs", "00AA55AA55AA55", "AA55070000AA0055AA0055AA0055FCFE"}, + {"payload_mixed_55_aa", "0055AA55AA55AA55AA", "AA5509000055AA0055AA0055AA0055AA002313"}, + {"payload_double_zero", "000000", "AA550300000000C81A"}, + {"payload_ten_AS", "0041414141414141414141", "AA550B00004141414141414141414125FF"}, +}; + +void test_spec_transport_valid_encode(void) +{ + for (size_t i = 0; i < sizeof(transport_valid_encode) / sizeof(transport_valid_encode[0]); i++) { + const encode_vector_t *v = &transport_valid_encode[i]; + verify_encode(v->name, v->input_hex, v->expected_hex); + } +} + +static const decode_frame_vector_t transport_valid_decode[] = { + {"empty_payload", "AA550100008883", "00"}, + {"single_byte_42", "AA5502000042B1DA", "0042"}, + {"hello_world", "AA5506000048656C6C6F3798", "0048656C6C6F"}, + {"payload_null_byte", "AA550200000037B2", "0000"}, + {"payload_ff_byte", "AA55020000FFC7AC", "00FF"}, + {"payload_aa_byte", "AA55020000AA0097A6", "00AA"}, + {"payload_aa55", "AA55030000AA00552DE2", "00AA55"}, + {"payload_triple_aa", "AA55040000AA00AA00AA00722F", "00AAAAAA"}, + {"payload_mixed_aa", "AA5506000001AA0002AA0003DBD7", "0001AA02AA03"}, + {"payload_zeros_16", "AA551100000000000000000000000000000000000036A3", "0000000000000000000000000000000000"}, + {"payload_ones_16", "AA551100000101010101010101010101010101010161B6", "0001010101010101010101010101010101"}, + {"payload_incremental", "AA55110000000102030405060708090A0B0C0D0E0F0BF2", "00000102030405060708090A0B0C0D0E0F"}, + {"payload_all_ff_16", "AA55110000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77A3", "00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"}, + {"payload_aa_prefix", "AA55030000AA0042FB80", "00AA42"}, + {"payload_aa_suffix", "AA5503000042AA00C665", "0042AA"}, + {"payload_aa_boundary", "AA55050000AA00AA0000AA00F9F9", "00AAAA00AA"}, + {"payload_alternating", "AA55060000AA0000AA0000AA00D651", "00AA00AA00AA"}, + {"payload_seq_32", "AA55210000000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F9845", "00000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"}, + {"payload_55_byte", "AA550200005567B8", "0055"}, + {"payload_byte_0x7F", "AA550200007F4F3D", "007F"}, + {"payload_byte_0x80", "AA5502000080BF23", "0080"}, + {"payload_byte_0xFE", "AA55020000FEE6BC", "00FE"}, + {"payload_ascii_test", "AA550500005465737491B9", "0054657374"}, + {"payload_ascii_fox", "AA5514000054686520717569636B2062726F776E20666F78BB2C", "0054686520717569636B2062726F776E20666F78"}, + {"payload_eight_bytes", "AA5509000000010203040506079D89", "000001020304050607"}, + {"payload_fifteen_null", "AA55100000000000000000000000000000000000EC09", "00000000000000000000000000000000"}, + {"payload_thirty_null", "AA551F00000000000000000000000000000000000000000000000000000000000000006AC1", "00000000000000000000000000000000000000000000000000000000000000"}, + {"payload_sixtyfour_seq", "AA55410000000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F07DF", "00000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F"}, +#if LLP_MAX_PAYLOAD >= 65 + {"payload_repeated_55", "AA5509000055555555555555556E3D", "005555555555555555"}, + {"payload_aa_55_pairs", "AA55070000AA0055AA0055AA0055FCFE", "00AA55AA55AA55"}, +#endif + {"payload_mixed_55_aa", "AA5509000055AA0055AA0055AA0055AA002313", "0055AA55AA55AA55AA"}, + {"payload_double_zero", "AA550300000000C81A", "000000"}, + {"payload_ten_AS", "AA550B00004141414141414141414125FF", "0041414141414141414141"}, +}; + +void test_spec_transport_valid_decode(void) +{ + for (size_t i = 0; i < sizeof(transport_valid_decode) / sizeof(transport_valid_decode[0]); i++) { + const decode_frame_vector_t *v = &transport_valid_decode[i]; + verify_decode_frame(v->name, v->frame_hex, v->payload_hex); + } +} + +void test_spec_transport_valid_stream_two_empty(void) +{ + const char *hex = "AA550100008883AA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT16(1, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[1].payload, 1); +} + +void test_spec_transport_valid_stream_empty_then_hello(void) +{ + const char *hex = "AA550100008883AA5506000048656C6C6F3798"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + TEST_ASSERT_EQUAL_INT(1, events[1].type); + uint8_t exp1[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_UINT16(6, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp1, events[1].payload, 6); +} + +void test_spec_transport_valid_stream_three_mixed(void) +{ + const char *hex = "AA55020000AA0097A6AA550100008883AA5506000048656C6C6F3798"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(3, event_count); + uint8_t exp_aa[] = {0x00, 0xAA}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(2, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp_aa, events[0].payload, 2); + uint8_t exp_empty[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT16(1, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp_empty, events[1].payload, 1); + uint8_t exp_hello[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[2].type); + TEST_ASSERT_EQUAL_UINT16(6, events[2].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp_hello, events[2].payload, 6); +} + +static const decode_error_vector_t transport_crc_vectors[] = { + {"corrupted_last_byte_empty_payload", "AA55010000887C", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_single_byte_42", "AA5502000042B125", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_hello_world", "AA5506000048656C6C6F3767", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_null_byte", "AA5502000000374D", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_ff_byte", "AA55020000FFC753", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_aa_byte", "AA55020000AA009759", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_aa55", "AA55030000AA00552D1D", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_triple_aa", "AA55040000AA00AA00AA0072D0", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_mixed_aa", "AA5506000001AA0002AA0003DB28", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_zeros_16", "AA5511000000000000000000000000000000000000365C", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_ones_16", "AA55110000010101010101010101010101010101016149", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_incremental", "AA55110000000102030405060708090A0B0C0D0E0F0B0D", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_all_ff_16", "AA55110000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF775C", LLP_ERR_CHECKSUM}, + {"corrupted_last_byte_payload_aa_prefix", "AA55030000AA0042FB7F", LLP_ERR_CHECKSUM}, + {"corrupted_both_crc_bytes", "AA5506000048656C6C6FC867", LLP_ERR_CHECKSUM}, + {"crc_all_zero", "AA5506000048656C6C6F0000", LLP_ERR_CHECKSUM}, + {"crc_all_ones", "AA5506000048656C6C6FFFFF", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_0", "AA5506000048656C6C6F3698", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_1", "AA5506000048656C6C6F3598", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_2", "AA5506000048656C6C6F3398", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_3", "AA5506000048656C6C6F3F98", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_4", "AA5506000048656C6C6F2798", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_5", "AA5506000048656C6C6F1798", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_6", "AA5506000048656C6C6F7798", LLP_ERR_CHECKSUM}, + {"crc_bit_flip_pos_7", "AA5506000048656C6C6FB798", LLP_ERR_CHECKSUM}, + {"crc_from_different_frame", "AA5506000048656C6C6FB1DA", LLP_ERR_CHECKSUM}, + {"crc_swapped_bytes", "AA5506000048656C6C6F9837", LLP_ERR_CHECKSUM}, + {"corrupted_payload_byte", "AA55060000489A6C6C6F3798", LLP_ERR_CHECKSUM}, +}; + +void test_spec_transport_crc(void) +{ + for (size_t i = 0; i < sizeof(transport_crc_vectors) / sizeof(transport_crc_vectors[0]); i++) { + const decode_error_vector_t *v = &transport_crc_vectors[i]; + verify_decode_error(v->name, v->frame_hex, v->error_code); + } +} + +static const decode_frame_vector_t transport_stuffing_valid[] = { + {"valid_single_aa", "AA55020000AA0097A6", "00AA"}, + {"valid_magic_overlap", "AA55030000AA00552DE2", "00AA55"}, + {"valid_triple_aa", "AA55040000AA00AA00AA00722F", "00AAAAAA"}, + {"valid_mixed_aa", "AA5506000001AA0002AA0003DBD7", "0001AA02AA03"}, +}; + +void test_spec_transport_stuffing_valid(void) +{ + for (size_t i = 0; i < sizeof(transport_stuffing_valid) / sizeof(transport_stuffing_valid[0]); i++) { + const decode_frame_vector_t *v = &transport_stuffing_valid[i]; + verify_decode_frame(v->name, v->frame_hex, v->payload_hex); + } +} + +static const decode_error_vector_t transport_stuffing_invalid[] = { + {"invalid_escape_0x01", "AA55020000AA0197A6", LLP_ERR_SYNC}, + {"invalid_escape_0xFF", "AA55020000AAFF97A6", LLP_ERR_SYNC}, + {"invalid_escape_0xAA", "AA55020000AAAA97A6", LLP_ERR_SYNC}, + {"raw_aa_unescaped", "AA55020000AA97A6", LLP_ERR_SYNC}, +}; + +void test_spec_transport_stuffing_invalid(void) +{ + for (size_t i = 0; i < sizeof(transport_stuffing_invalid) / sizeof(transport_stuffing_invalid[0]); i++) { + const decode_error_vector_t *v = &transport_stuffing_invalid[i]; + verify_decode_error(v->name, v->frame_hex, v->error_code); + } +} + +void test_spec_transport_resync_noise_before_frame(void) +{ + const char *hex = "FFFFFFAA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t exp[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[0].payload, 1); +} + +void test_spec_transport_resync_noise_between_frames(void) +{ + const char *hex = "AA550100008883DEADAA5506000048656C6C6F3798"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + uint8_t exp1[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT16(6, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp1, events[1].payload, 6); +} + +void test_spec_transport_resync_corrupt_magic1(void) +{ + const char *hex = "BB550100008883AA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); +} + +void test_spec_transport_resync_corrupt_magic2(void) +{ + const char *hex = "AA440100008883AA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(1, events[0].payload_len); +} + +void test_spec_transport_resync_aa_no_false_resync(void) +{ + const char *hex = "AA55020000AA0097A6"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t exp[] = {0x00, 0xAA}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(2, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[0].payload, 2); +} + +void test_spec_transport_resync_aa55_no_false_resync(void) +{ + const char *hex = "AA55030000AA00552DE2"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t exp[] = {0x00, 0xAA, 0x55}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(3, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[0].payload, 3); +} + +void test_spec_transport_resync_invalid_escape_then_valid(void) +{ + const char *hex = "AA55020000AA9997A6AA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_SYNC, events[0].error_code); + uint8_t exp[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT16(1, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[1].payload, 1); +} + +void test_spec_transport_resync_garbage_three_frames(void) +{ + const char *hex = "AA550100008883FFAA5506000048656C6C6F3798AABBAA550100008883"; + uint8_t data[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(hex, data, sizeof(data)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, data, len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(3, event_count); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + uint8_t exp1[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp1, events[1].payload, 6); + uint8_t exp2[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[2].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp2, events[2].payload, 1); +} + +void test_spec_transport_truncation_after_magic1(void) +{ + uint8_t data[] = {0xAA}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_after_magic2(void) +{ + uint8_t data[] = {0xAA, 0x55}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_after_len_l(void) +{ + uint8_t data[] = {0xAA, 0x55, 0x06}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_after_len_h(void) +{ + uint8_t data[] = {0xAA, 0x55, 0x06, 0x00}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_mid_payload(void) +{ + uint8_t data[] = {0xAA, 0x55, 0x06, 0x00, 0x00, 0x48}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_mid_crc_low(void) +{ + uint8_t data[] = {0xAA, 0x55, 0x06, 0x00, 0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x37}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_truncation_empty_stream(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + TEST_ASSERT_EQUAL_INT(LLP_STATE_WAIT_MAGIC1, parser.state); +} + +void test_spec_transport_truncation_magic_only(void) +{ + uint8_t data[] = {0xAA, 0x55}; + llp_parser_t parser; + llp_parser_init(&parser); + int result = 0; + for (size_t i = 0; i < sizeof(data); i++) + result = llp_parser_process_byte(&parser, data[i], 0); + TEST_ASSERT_EQUAL_INT(0, result); + result = llp_parser_process_byte(&parser, 0x00, LLP_FRAME_TIMEOUT_MS + 1); + TEST_ASSERT_EQUAL_INT(-1, result); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, parser.error_code); +} + +void test_spec_transport_timeout_mid_frame(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + uint8_t bytes[] = {0xAA, 0x55, 0x06, 0x00}; + unsigned long times[] = {0, 1, 2, 5000}; + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream_timed(&parser, bytes, times, 4, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, events[0].error_code); +} + +void test_spec_transport_timeout_then_valid(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + uint8_t bytes[] = {0xAA, 0x55, 0xAA, 0x55, 0x01, 0x00, 0x00, 0x88, 0x83}; + unsigned long times[] = {0, 1, 5000, 5001, 5002, 5003, 5004, 5005, 5006}; + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream_timed(&parser, bytes, times, 9, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, events[0].error_code); + uint8_t exp[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[1].payload, 1); +} + +void test_spec_transport_timeout_between_frames(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + uint8_t bytes[] = {0xAA,0x55,0x01,0x00,0x00,0x88,0x83, 0xAA,0x55,0x01,0x00,0x00,0x88,0x83}; + unsigned long times[] = {0,1,2,3,4,5,6, 5000,5001,5002,5003,5004,5005,5006}; + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream_timed(&parser, bytes, times, 14, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_INT(1, events[1].type); +} + +static const encode_vector_t layers_passthrough_encode[] = { + {"empty_chain", "00", "AA550100008883"}, + {"final_then_ff", "00FF", "AA55020000FFC7AC"}, + {"final_then_aa55", "00AA55", "AA55030000AA00552DE2"}, + {"final_hello", "0048656C6C6F", "AA5506000048656C6C6F3798"}, + {"single_passthrough", "01031020300048656C6C6F", "AA550B0001031020300048656C6C6F6191"}, + {"two_passthrough", "0101AA0202BBCC0042", "AA5509000101AA000202BBCC0042822E"}, + {"unknown_layer_id", "FF01000064617461", "AA550800FF010000646174615B24"}, + {"max_passthrough", "7F02F00F0078797A", "AA5508007F02F00F0078797ACA6E"}, + {"max_transform", "FE01A500010203", "AA550700FE01A5000102030750"}, + {"three_nested", "0101010201020301030064656570", "AA550E000101010201020301030064656570F451"}, + {"stuffing_metadata", "0104AA00AA55004F4B", "AA5509000104AA0000AA0055004F4BC83C"}, + {"zero_meta_len", "01000064617461", "AA55070001000064617461B65F"}, + {"four_nested", "010002000300040000656E64", "AA550C00010002000300040000656E643755"}, + {"passthrough_7F_zero", "7F00004142", "AA5505007F00004142DD3B"}, + {"missing_final_node", "014243", "AA550300014243F13E"}, +}; + +void test_spec_layers_passthrough_encode(void) +{ + for (size_t i = 0; i < sizeof(layers_passthrough_encode) / sizeof(layers_passthrough_encode[0]); i++) { + const encode_vector_t *v = &layers_passthrough_encode[i]; + verify_encode(v->name, v->input_hex, v->expected_hex); + } +} + +static const decode_frame_vector_t layers_passthrough_decode[] = { + {"empty_chain", "AA550100008883", "00"}, + {"final_then_ff", "AA55020000FFC7AC", "00FF"}, + {"final_then_aa55", "AA55030000AA00552DE2", "00AA55"}, + {"final_hello", "AA5506000048656C6C6F3798", "0048656C6C6F"}, + {"single_passthrough", "AA550B0001031020300048656C6C6F6191", "01031020300048656C6C6F"}, + {"two_passthrough", "AA5509000101AA000202BBCC0042822E", "0101AA0202BBCC0042"}, + {"unknown_layer_id", "AA550800FF010000646174615B24", "FF01000064617461"}, + {"max_passthrough", "AA5508007F02F00F0078797ACA6E", "7F02F00F0078797A"}, + {"max_transform", "AA550700FE01A5000102030750", "FE01A500010203"}, + {"three_nested", "AA550E000101010201020301030064656570F451", "0101010201020301030064656570"}, + {"stuffing_metadata", "AA5509000104AA0000AA0055004F4BC83C", "0104AA00AA55004F4B"}, + {"zero_meta_len", "AA55070001000064617461B65F", "01000064617461"}, + {"four_nested", "AA550C00010002000300040000656E643755", "010002000300040000656E64"}, + {"passthrough_7F_zero", "AA5505007F00004142DD3B", "7F00004142"}, + {"missing_final_node", "AA550300014243F13E", "014243"}, +}; + +void test_spec_layers_passthrough_decode(void) +{ + for (size_t i = 0; i < sizeof(layers_passthrough_decode) / sizeof(layers_passthrough_decode[0]); i++) { + const decode_frame_vector_t *v = &layers_passthrough_decode[i]; + verify_decode_frame(v->name, v->frame_hex, v->payload_hex); + } +} + +void test_spec_layers_passthrough_extended_meta_zero(void) +{ +#if LLP_MAX_PAYLOAD >= 264 + const char *frame_hex = "AA55090001FF000000646174612057"; + const char *payload_hex = "01FF00000064617461"; + verify_decode_frame("extended_meta_zero", frame_hex, payload_hex); +#endif +} + +void test_spec_layers_passthrough_extended_meta_255(void) +{ +#if LLP_MAX_PAYLOAD >= 262 + uint8_t payload[SPEC_MAX_FRAME]; + size_t pidx = 0; + payload[pidx++] = 0x01; + payload[pidx++] = 0xFF; + payload[pidx++] = 0x00; + payload[pidx++] = 0xFF; + for (int i = 0; i < 255; i++) payload[pidx++] = 0xAA; + payload[pidx++] = 0x00; + payload[pidx++] = 0x61; + payload[pidx++] = 0x62; + + uint8_t frame[SPEC_MAX_FRAME]; + size_t frame_len = llp_build_frame(frame, sizeof(frame), payload, (uint16_t)pidx); + TEST_ASSERT_GREATER_THAN_size_t_MESSAGE(0, frame_len, "extended_meta_255 build_frame"); + + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, frame_len, NULL); + TEST_ASSERT_EQUAL_INT_MESSAGE(1, result, "extended_meta_255 parse"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE((uint16_t)pidx, parser.frame.payload_len, + "extended_meta_255 payload_len"); + + uint8_t out[LLP_MAX_PAYLOAD]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + (void)final_len; +#endif +} + +static const encode_vector_t layers_transform_encode[] = { + {"transform_FE_meta5", "FE05010203040500FF", "AA550900FE05010203040500FF8C62"}, + {"layers_aa_meta", "0102AA00AA0203AABBCC006465616462656566", "AA5513000102AA0000AA000203AA00BBCC0064656164626565661481"}, + {"deep_nested_5", "0100010001000100010000656E64", "AA550E000100010001000100010000656E64A8D3"}, + {"zero_meta_then_final", "010000006162", "AA550600010000006162BE62"}, + {"transform_layer", "8004DEADBEEF004F4B", "AA5509008004DEADBEEF004F4BB396"}, + {"mixed_layers", "010211228101FF0055AA01", "AA550B00010211228101FF0055AA0001C753"}, +}; + +void test_spec_layers_transform_encode(void) +{ + for (size_t i = 0; i < sizeof(layers_transform_encode) / sizeof(layers_transform_encode[0]); i++) { + const encode_vector_t *v = &layers_transform_encode[i]; + verify_encode(v->name, v->input_hex, v->expected_hex); + } +} + +static const decode_frame_vector_t layers_transform_decode[] = { + {"transform_FE_meta5", "AA550900FE05010203040500FF8C62", "FE05010203040500FF"}, + {"layers_aa_meta", "AA5513000102AA0000AA000203AA00BBCC0064656164626565661481", "0102AA00AA0203AABBCC006465616462656566"}, + {"deep_nested_5", "AA550E000100010001000100010000656E64A8D3", "0100010001000100010000656E64"}, + {"zero_meta_then_final", "AA550600010000006162BE62", "010000006162"}, + {"transform_layer", "AA5509008004DEADBEEF004F4BB396", "8004DEADBEEF004F4B"}, + {"mixed_layers", "AA550B00010211228101FF0055AA0001C753", "010211228101FF0055AA01"}, + {"transform_no_handler_decode", "AA5509008004DEADBEEF004F4BB396", "8004DEADBEEF004F4B"}, +}; + +void test_spec_layers_transform_decode(void) +{ + for (size_t i = 0; i < sizeof(layers_transform_decode) / sizeof(layers_transform_decode[0]); i++) { + const decode_frame_vector_t *v = &layers_transform_decode[i]; + verify_decode_frame(v->name, v->frame_hex, v->payload_hex); + } +} + +void test_spec_layers_malformed_truncated_metadata(void) +{ + const char *frame_hex = "AA550800010A10203000486535BD"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[512]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-1, final_len); +} + +void test_spec_layers_malformed_empty_payload(void) +{ + verify_decode_frame("empty_payload", "AA550100008883", "00"); +} + +void test_spec_layers_malformed_extended_meta_truncated(void) +{ + const char *frame_hex = "AA55040001FF0000ED0A"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[512]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-1, final_len); +} + +void test_spec_layers_malformed_reserved_id_FF(void) +{ + verify_decode_frame("reserved_id_FF", "AA550800FF010000646174615B24", "FF01000064617461"); +} + +void test_spec_layers_traversal_three_passthrough(void) +{ + const char *frame_hex = "AA550E000101010201020301030064656570F451"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[256]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(4, final_len); + uint8_t expected[] = {0x64, 0x65, 0x65, 0x70}; + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, 4); +} + +void test_spec_layers_traversal_single_passthrough(void) +{ + const char *frame_hex = "AA550B0001031020300048656C6C6F6191"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[256]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(5, final_len); + uint8_t expected[] = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, 5); +} + +void test_spec_layers_traversal_direct_finalnode(void) +{ + const char *frame_hex = "AA5502000042B1DA"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[256]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(1, final_len); + uint8_t expected[] = {0x42}; + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, out, 1); +} + +void test_spec_layers_traversal_empty_final_payload(void) +{ + const char *frame_hex = "AA550100008883"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[256]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(0, final_len); +} + +void test_spec_layers_traversal_transform_blocked(void) +{ + const char *frame_hex = "AA5509008004DEADBEEF004F4BB396"; + uint8_t frame[SPEC_MAX_FRAME]; + size_t len = spec_hex_to_bytes(frame_hex, frame, sizeof(frame)); + llp_parser_t parser; + llp_parser_init(&parser); + int result = spec_feed_frame(&parser, frame, len, NULL); + TEST_ASSERT_EQUAL_INT(1, result); + uint8_t out[256]; + int final_len = llp_get_final_payload(&parser.frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-1, final_len); +} + +void test_spec_parser_incremental_byte_by_byte_hello(void) +{ + const char *chunks[] = {"AA","55","06","00","00","48","65","6C","6C","6F","37","98"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 12, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t expected[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(6, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, events[0].payload, 6); +} + +void test_spec_parser_incremental_two_bytes(void) +{ + const char *chunks[] = {"AA55","0100","0088","83AA","5502","0000","AA00","97A6"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 8, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + uint8_t exp1[] = {0x00, 0xAA}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp1, events[1].payload, 2); +} + +void test_spec_parser_incremental_mixed_chunks(void) +{ + const char *chunks[] = {"AA5501","00008883","AA55060000","48656C6C6F3798"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 4, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); +} + +void test_spec_parser_fragmented_after_magic1(void) +{ + const char *chunks[] = {"AA","55020000AA0097A6"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 2, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t exp[] = {0x00, 0xAA}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(2, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[0].payload, 2); +} + +void test_spec_parser_fragmented_mid_stuffing(void) +{ + const char *chunks[] = {"AA55020000AA","0097A6"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 2, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + uint8_t exp[] = {0x00, 0xAA}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(2, events[0].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[0].payload, 2); +} + +void test_spec_parser_fragmented_at_crc_boundary(void) +{ + const char *chunks[] = {"AA55020000AA0097","A6"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 2, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(1, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT16(2, events[0].payload_len); +} + +void test_spec_parser_recovery_after_crc_error(void) +{ + const char *chunks[] = {"AA55010000887C","AA5506000048656C6C6F3798"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 2, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_CHECKSUM, events[0].error_code); + uint8_t exp[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT16(6, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[1].payload, 6); +} + +void test_spec_parser_recovery_after_sync_error(void) +{ + const char *chunks[] = {"AA55020000AA9997A6","AA550100008883"}; + uint8_t stream[SPEC_MAX_FRAME]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 2, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_SYNC, events[0].error_code); + uint8_t exp[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[1].payload, 1); +} + +void test_spec_parser_recovery_garbage_then_two_frames(void) +{ + const char *chunks[] = {"DEADBEEF","AA550100008883","AA5506000048656C6C6F3798"}; + uint8_t stream[SPEC_MAX_FRAME * 2]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 3, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + uint8_t exp0[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[0].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp0, events[0].payload, 1); + uint8_t exp1[] = {0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + TEST_ASSERT_EQUAL_INT(1, events[1].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp1, events[1].payload, 6); +} + +void test_spec_parser_recovery_multiple_errors_then_valid(void) +{ + const char *chunks[] = {"AA55010000887C","AA5506000048656C6C6F3767","AA550100008883"}; + uint8_t stream[SPEC_MAX_FRAME * 2]; + size_t stream_len = 0; + spec_concat_chunks(chunks, 3, stream, &stream_len, sizeof(stream)); + llp_parser_t parser; + llp_parser_init(&parser); + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream(&parser, stream, stream_len, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(3, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_CHECKSUM, events[0].error_code); + TEST_ASSERT_EQUAL_INT(-1, events[1].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_CHECKSUM, events[1].error_code); + uint8_t exp[] = {0x00}; + TEST_ASSERT_EQUAL_INT(1, events[2].type); + TEST_ASSERT_EQUAL_UINT8_ARRAY(exp, events[2].payload, 1); +} + +void test_bug_timeout_does_not_update_last_byte_time(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + uint8_t bytes[] = {0xAA, 0x55, 0xAA, 0x55, 0x01, 0x00, 0x00, 0x88, 0x83}; + unsigned long times[] = {0, 1, 5000, 5001, 5002, 5003, 5004, 5005, 5006}; + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream_timed(&parser, bytes, times, 9, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(-1, events[0].type); + TEST_ASSERT_EQUAL_INT(LLP_ERR_TIMEOUT, events[0].error_code); + TEST_ASSERT_EQUAL_INT(1, events[1].type); + uint8_t expected[] = {0x00}; + TEST_ASSERT_EQUAL_UINT16(1, events[1].payload_len); + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, events[1].payload, 1); +} + +void test_bug_timeout_between_frames(void) +{ + llp_parser_t parser; + llp_parser_init(&parser); + uint8_t bytes[] = {0xAA,0x55,0x01,0x00,0x00,0x88,0x83, 0xAA,0x55,0x01,0x00,0x00,0x88,0x83}; + unsigned long times[] = {0,1,2,3,4,5,6, 5000,5001,5002,5003,5004,5005,5006}; + spec_event_t events[SPEC_MAX_EVENTS]; + int event_count = 0; + spec_feed_stream_timed(&parser, bytes, times, 14, events, &event_count, SPEC_MAX_EVENTS); + TEST_ASSERT_EQUAL_INT(2, event_count); + TEST_ASSERT_EQUAL_INT(1, events[0].type); + uint8_t expected0[] = {0x00}; + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected0, events[0].payload, 1); + TEST_ASSERT_EQUAL_INT(1, events[1].type); + uint8_t expected1[] = {0x00}; + TEST_ASSERT_EQUAL_UINT8_ARRAY(expected1, events[1].payload, 1); +} + +void test_bug_final_payload_returns_zero_without_final_node(void) +{ + uint8_t payload[] = {0x01, 0xFF, 0x00, 0x00}; + llp_frame_t frame; + frame.payload_len = 4; + memcpy(frame.payload, payload, 4); + uint8_t out[64]; + int result = llp_get_final_payload(&frame, out, sizeof(out)); + TEST_ASSERT_EQUAL_INT(-1, result); +} \ No newline at end of file