From ceaa2e82cabc022eff4ef47ac61495c5442c463f Mon Sep 17 00:00:00 2001 From: Enzo Sanchez <80921527+EnzoLeonel@users.noreply.github.com> Date: Sun, 10 May 2026 20:08:28 -0300 Subject: [PATCH 1/9] Add PlatformIO configuration for v3.0.0 with test support --- platformio.ini | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 platformio.ini diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..eed1ee1 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,124 @@ +[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 + -DLLP_MAX_PAYLOAD=512 + -DLLP_FRAME_TIMEOUT_MS=2000 +lib_deps = + throwtheswitch/Unity + +# ================================================================ +# 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 From d823fe531f4c4535082bd03722317fa00e962568 Mon Sep 17 00:00:00 2001 From: Enzo Sanchez <80921527+EnzoLeonel@users.noreply.github.com> Date: Sun, 10 May 2026 20:10:31 -0300 Subject: [PATCH 2/9] Add project structure documentation --- STRUCTURE.md | 402 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 STRUCTURE.md diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..d943b11 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,402 @@ +# Project Structure - LLP Protocol v3.0.0 + +## Directory Tree + +``` +llp-protocol/ +│ +├── 📄 platformio.ini # Configuración central de PlatformIO +│ # Define entornos, compiladores, flags +│ +├── 📁 src/ # Código fuente principal +│ └── llp_protocol.c # Implementación del protocolo LLP +│ +├── 📁 include/ # Headers públicos +│ └── llp_protocol.h # API pública del protocolo +│ +├── 📁 test/ # Suite de tests con Unity +│ ├── test_parser_init.c # 5 tests - Inicialización +│ ├── test_frame_builder.c # 10 tests - Construcción de frames +│ ├── test_parser_process.c # 9 tests - Procesamiento byte-a-byte +│ └── test_crc.c # 9 tests - Validación de CRC16-CCITT +│ +├── 📁 lib/ # Librerías externas +│ └── (unity instalado automáticamente) +│ +├── 📁 examples/ # Sketches y ejemplos +│ ├── minimal_uart/ +│ │ └── minimal_uart.ino # Ejemplo: UART simple +│ └── request_response/ +│ └── request_response.ino # Ejemplo: Request-Response +│ +├── 📁 docs/ # Documentación +│ ├── (specs detalladas del protocolo) +│ +├── 📁 .github/ +│ └── workflows/ +│ └── platformio.yml # CI/CD automatizado en GitHub Actions +│ +├── 📄 .gitignore # Archivos ignorados por Git +├── 📄 README.md # Documentación principal +├── 📄 TESTING.md # Guía de testing (este archivo) +├── 📄 STRUCTURE.md # Explicación de estructura +├── 📄 CHANGELOG.md # Historial de cambios +├── 📄 LICENSE # MIT License +└── 📄 library.properties # Metadata para Arduino Library Manager +``` + +## File Purposes + +### Core Files + +| Archivo | Propósito | Tamaño Típico | +|---------|-----------|---------------| +| `platformio.ini` | Configuración central | ~2.5 KB | +| `src/llp_protocol.c` | Implementación | ~3-5 KB | +| `include/llp_protocol.h` | API pública | ~1-2 KB | +| `lib/unity/` | Framework de testing | ~100 KB | + +### Test Files + +| Archivo | Descripción | Tests | Cobertura | +|---------|-------------|-------|-----------| +| `test/test_parser_init.c` | Inicialización del parser | 5 | Estado inicial, variables | +| `test/test_frame_builder.c` | Construcción de frames | 10 | Magic, tipo, ID, payload, CRC | +| `test/test_parser_process.c` | Procesamiento byte-a-byte | 9 | Parsing, state machine, recovery | +| `test/test_crc.c` | Validación CRC16-CCITT | 9 | Checksum, detección de errores | + +**Total: 33 tests** + +### GitHub Actions + +| Archivo | Propósito | +|---------|-----------| +| `.github/workflows/platformio.yml` | Ejecuta tests en cada push/PR | + +### Documentation + +| Archivo | Contenido | +|---------|----------| +| `README.md` | Documentación principal, instalación, API | +| `TESTING.md` | Guía completa de testing | +| `STRUCTURE.md` | Este archivo - explicación de estructura | +| `CHANGELOG.md` | Historial de versiones | +| `docs/` | Especificaciones detalladas del protocolo | + +## Configuration: platformio.ini + +### Section: [platformio] + +Define comportamiento global de PlatformIO: + +```ini +[platformio] +src_dir = src # Dónde está el código principal +lib_dir = lib # Dónde van las librerías externas +include_dir = include # Path de headers +test_dir = test # Dónde están los tests +default_envs = test # Entorno por defecto +``` + +### Environments + +Cada `[env:nombre]` define cómo compilar para una plataforma: + +```ini +[env:test] +platform = native # Compilar con gcc nativo (no Arduino) +test_framework = unity # Usar Unity para tests +build_flags = # Flags del compilador C + -Wall + -DLLP_MAX_PAYLOAD=512 +``` + +### Available Environments + +```bash +platformio test -e test # Tests (compilación nativa) +platformio run -e arduino_uno # Arduino UNO +platformio run -e arduino_nano # Arduino Nano +platformio run -e arduino_mega # Arduino Mega +platformio run -e esp8266 # ESP8266 +platformio run -e esp32 # ESP32 +platformio run -e stm32f103 # STM32 +platformio run -e attiny85 # ATtiny85 +``` + +## Build Process + +### Step 1: Compilation + +``` +Source Files (.c, .h) + ↓ +Preprocessor (macros, includes) + ↓ +C Compiler (gcc/arm-gcc) + ↓ +Object Files (.o) +``` + +### Step 2: Linking + +``` +Object Files (.o) + ↓ +Unity Framework Library + ↓ +Linker + ↓ +Executable +``` + +### Step 3: Execution (Tests) + +``` +Executable + ↓ +Run Tests + ↓ +Compare Results + ↓ +Pass/Fail Report +``` + +## Memory Layout (Arduino UNO) + +``` +Total RAM: 2 KB (2048 bytes) + +llp_parser_t struct: ~600 bytes +llp_frame_t struct: ~520 bytes +Local variables: ~100 bytes +Stack overhead: ~100 bytes +─────────────────────────── +Available for application: ~730 bytes (36% of RAM) +``` + +## Build Artifacts + +PlatformIO crea automáticamente: + +``` +.pio/ +├── build/ +│ ├── test/ # Compilación de tests +│ │ ├── firmware.elf +│ │ └── (archivos objeto) +│ ├── arduino_uno/ +│ │ ├── firmware.hex # Imagen para cargar en Arduino +│ │ └── (archivos objeto) +│ └── ... +├── build_cache/ +└── ... +``` + +## Adding New Features + +### Scenario: Añadir nueva función `llp_validate_frame()` + +#### 1. Declare en Header + +```c +// include/llp_protocol.h +bool llp_validate_frame(const llp_frame_t* frame); +``` + +#### 2. Implement en Source + +```c +// src/llp_protocol.c +bool llp_validate_frame(const llp_frame_t* frame) { + if (!frame) return false; + if (frame->payload_len > LLP_MAX_PAYLOAD) return false; + return true; +} +``` + +#### 3. Write Tests + +```c +// test/test_validation.c +void test_validate_frame_returns_true_for_valid_frame(void) { + llp_frame_t frame = { + .type = LLP_DATA, + .id = 1, + .payload_len = 10 + }; + TEST_ASSERT_TRUE(llp_validate_frame(&frame)); +} + +void test_validate_frame_returns_false_for_null(void) { + TEST_ASSERT_FALSE(llp_validate_frame(NULL)); +} +``` + +#### 4. Run Tests + +```bash +platformio test -e test -v +``` + +#### 5. Commit + +```bash +git add include/llp_protocol.h src/llp_protocol.c test/test_validation.c +git commit -m "Add frame validation function" +``` + +## Version Management + +### Current Version Structure + +``` +Version 3.0.0 +│ +├── Major (3) - Breaking changes +├── Minor (0) - New features (backwards compatible) +└── Patch (0) - Bug fixes +``` + +Update in: +1. `library.properties` - Arduino Library Manager +2. `README.md` - Documentation +3. `CHANGELOG.md` - Release notes +4. GitHub Tags - `git tag v3.0.0` + +## Git Workflow + +### Feature Branch + +```bash +git checkout -b feature/v3.0.0 +# ... hacer cambios ... +git add . +git commit -m "Add testing infrastructure" +git push origin feature/v3.0.0 +``` + +### Create PR + +- Push to GitHub +- Open Pull Request +- CI/CD runs automatically +- Review and merge + +### Release + +```bash +git checkout main +git merge feature/v3.0.0 +git tag v3.0.0 +git push origin main --tags +``` + +## Performance Considerations + +### Code Size + +| Component | Flash | RAM | +|-----------|-------|-----| +| llp_protocol.c | ~1-2 KB | ~600 B | +| Unity tests | ~0 B | ~1 KB | +| Arduino Framework | ~2 KB | ~400 B | + +### Execution Time + +- **Parsing 1 byte**: ~50 µs +- **Building frame**: ~100 µs +- **CRC calculation**: ~15 µs per byte + +## Troubleshooting + +### Issue: Tests don't compile + +```bash +# Verificar path de includes +platformio test -e test -vvv + +# Limpiar y reconstruir +platformio run -t clean +platformio test -e test +``` + +### Issue: Can't find Unity framework + +```bash +# Reinstalar dependencias +pio lib update +platformio run --update-all +``` + +### Issue: Code compiles for test but not for Arduino + +Verificar: +1. Includes en `platformio.ini` +2. Archivos de configuración de Arduino no compilados en test +3. Usar `#ifdef` para código específico de plataforma + +```c +#ifdef ARDUINO + Serial.println("Arduino only"); +#endif +``` + +## Best Practices + +### 1. Organize Tests Logically + +Separate test files por funcionalidad: +- `test_parser_*.c` para parser +- `test_frame_*.c` para frame building +- `test_crc_*.c` para CRC + +### 2. Keep Tests Independent + +Cada test debe funcionar sin depender de otros. + +✅ Bueno: +```c +void test_feature_a(void) { + // Setup completo + llp_parser_t parser; + llp_parser_init(&parser); + // Test +} +``` + +### 3. Use Meaningful Names + +```c +// ✅ Descriptive +void test_parser_recovers_from_corrupted_magic_bytes(void) { } + +// ❌ Vague +void test_parser_recovery(void) { } +``` + +### 4. Document Complex Tests + +```c +// Test que verifica sincronización RF con ruido simulado +void test_parser_syncs_with_rf_noise(void) { + // Enviar bytes de ruido + llp_parser_process_byte(&parser, 0xFF, 0); + llp_parser_process_byte(&parser, 0xFF, 0); + + // Enviar frame válido + // Debe recuperarse y parsear correctamente +} +``` + +## Next Steps + +1. **Implementar `src/llp_protocol.c`** según v3.0.0 +2. **Ejecutar tests** - `platformio test -e test -v` +3. **Compilar ejemplos** - `platformio run -e arduino_uno` +4. **Validar hardware** - Cargar en Arduino/ESP32 +5. **Crear PR** - Abrir pull request a `main` + +--- + +**For more details, see `TESTING.md` and `README.md`** From 7d8e9757be978414385bc09c841b2334e403331d Mon Sep 17 00:00:00 2001 From: Enzo Sanchez <80921527+EnzoLeonel@users.noreply.github.com> Date: Sun, 10 May 2026 20:19:55 -0300 Subject: [PATCH 3/9] Add comprehensive testing guide --- TESTING.md | 418 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..0943ff9 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,418 @@ +# Testing Guide - LLP Protocol v3.0.0 + +## Quick Start + +### Prerequisites + +Instala PlatformIO: + +```bash +pip install platformio +``` + +### Run All Tests + +```bash +cd llp-protocol +git checkout feature/v3.0.0 +platformio test -e test -v +``` + +Expected output: +``` +========= [PASSED] ========= +33 passed in 0.42s +``` + +--- + +## Test Structure + +### 4 Test Suites + +| Suite | File | Tests | Focus | +|-------|------|-------|-------| +| **Initialization** | `test/test_parser_init.c` | 5 | Parser startup, state reset | +| **Frame Building** | `test/test_frame_builder.c` | 10 | Frame construction, CRC | +| **Processing** | `test/test_parser_process.c` | 9 | Byte parsing, state machine | +| **CRC Validation** | `test/test_crc.c` | 9 | CRC16-CCITT, error detection | + +**Total: 33 tests** + +--- + +## Test Execution Examples + +### Run Tests Verbosely + +```bash +platformio test -e test -v +``` + +Output: +``` +Running test_parser_init_sets_state_to_waiting_magic ... [PASSED] +Running test_parser_init_initializes_all_fields ... [PASSED] +... +``` + +### Run Specific Test File + +```bash +platformio test -e test --filter test_frame_builder +``` + +### Run Tests with Extra Verbosity + +```bash +platformio test -e test -vvv +``` + +Shows compiler warnings, full linking output, etc. + +--- + +## Test File Breakdown + +### test_parser_init.c + +**Purpose:** Validar que el parser se inicialice correctamente + +**Tests:** +1. `test_parser_init_sets_state_to_waiting_magic` - Estado inicial correcto +2. `test_parser_init_initializes_all_fields` - Todos los campos inicializados +3. `test_parser_init_clears_frame_buffer` - Buffer vacío +4. `test_parser_can_be_reinitialized` - Reset funciona +5. `test_parser_init_resets_stats` - Estadísticas a cero + +**Example assertion:** +```c +TEST_ASSERT_EQUAL_INT(0, parser.state); +``` + +### test_frame_builder.c + +**Purpose:** Validar construcción correcta de frames + +**Tests:** +1. `test_build_frame_with_empty_payload` - Frame sin payload +2. `test_build_frame_sets_correct_magic_bytes` - 0xAA, 0x55 +3. `test_build_frame_sets_type_field` - Campo tipo correcto +4. `test_build_frame_stores_id_little_endian` - ID en LE +5. `test_build_frame_sets_payload_length` - Longitud correcta +6. `test_build_frame_copies_payload_correctly` - Datos copiados +7. `test_build_frame_includes_crc16` - CRC presente +8. `test_build_frame_returns_zero_for_small_buffer` - Validación de buffer +9. `test_build_frame_returns_zero_for_oversized_payload` - Payload máximo +10. `test_build_frame_works_with_all_message_types` - Todos los tipos + +**Example:** +```c +uint8_t buffer[520]; +size_t frame_len = llp_build_frame( + buffer, sizeof(buffer), + LLP_PING, 123, + NULL, 0 +); +TEST_ASSERT_EQUAL_HEX8(0xAA, buffer[0]); +``` + +### test_parser_process.c + +**Purpose:** Validar procesamiento byte por byte + +**Tests:** +1. `test_process_byte_returns_zero_for_incomplete_frame` - Retorna 0 si incompleto +2. `test_process_byte_returns_one_for_complete_frame` - Retorna 1 si completo +3. `test_process_byte_extracts_correct_frame_data` - Datos extraídos correctamente +4. `test_process_byte_returns_negative_one_for_crc_error` - Retorna -1 si error +5. `test_process_byte_recovers_from_bad_magic_bytes` - Recuperación de ruido +6. `test_process_byte_handles_multiple_frames` - Múltiples frames +7. `test_process_byte_increments_frames_ok_counter` - Contador de frames OK +8. `test_process_byte_handles_maximum_payload` - Payload máximo +9. `test_process_byte_handles_all_message_types` - Todos los tipos + +**Example:** +```c +llp_parser_t parser; +llp_parser_init(&parser); + +// Procesar frame byte por byte +for (size_t i = 0; i < frame_len; i++) { + result = llp_parser_process_byte(&parser, buffer[i], 0); +} + +TEST_ASSERT_EQUAL_INT(1, result); // Frame completo +``` + +### test_crc.c + +**Purpose:** Validar CRC16-CCITT + +**Tests:** +1. `test_crc16_calculated_for_empty_frame` - CRC incluso sin payload +2. `test_crc16_differs_for_different_payloads` - CRC varía con payload +3. `test_crc16_differs_for_different_types` - CRC varía con tipo +4. `test_crc16_differs_for_different_ids` - CRC varía con ID +5. `test_crc16_detects_single_bit_flip_in_payload` - Detecta 1 bit cambiado +6. `test_crc16_detects_burst_error` - Detecta múltiples bits cambiados +7. `test_crc16_stored_in_correct_byte_positions` - Posición correcta +8. `test_crc16_valid_for_maximum_payload` - Válido con payload máximo +9. `test_crc16_is_deterministic` - Resultado consistente + +--- + +## Understanding Test Results + +### Success Output + +``` +Platform native [100%] ✔ +Test ✔ 33 passed in 0.42s +========= [PASSED] ========= +``` + +**Meaning:** Todos los 33 tests pasaron. + +### Failure Output + +``` +Running test_build_frame_sets_correct_magic_bytes ... [FAILED] + +Expected 0xAA but was 0xFF +``` + +**Meaning:** El primer byte del frame no es 0xAA. Revisa `llp_build_frame()`. + +### Compilation Error + +``` +src/llp_protocol.c:15:5: error: conflicting types for 'llp_parser_init' +``` + +**Meaning:** Hay un error en el código. Verifica los tipos en la declaración vs definición. + +--- + +## Common Test Patterns + +### Pattern 1: Basic Functionality Test + +```c +void test_feature_works_correctly(void) { + // Setup + llp_parser_t parser; + llp_parser_init(&parser); + + // Action + int result = llp_parser_process_byte(&parser, 0xAA, 0); + + // Assert + TEST_ASSERT_EQUAL_INT(0, result); // Incompleto +} +``` + +### Pattern 2: Error Handling Test + +```c +void test_feature_handles_error(void) { + uint8_t buffer[4]; // Muy pequeño + + size_t frame_len = llp_build_frame( + buffer, sizeof(buffer), + LLP_DATA, 1, + NULL, 0 + ); + + TEST_ASSERT_EQUAL_INT(0, frame_len); // Error +} +``` + +### Pattern 3: Integration Test + +```c +void test_build_and_parse_frame(void) { + // Construir + uint8_t buffer[520]; + uint8_t payload[] = {0xAA, 0xBB}; + + size_t frame_len = llp_build_frame( + buffer, sizeof(buffer), + LLP_DATA, 123, + payload, sizeof(payload) + ); + + // Parsear + llp_parser_t parser; + llp_parser_init(&parser); + + for (size_t i = 0; i < frame_len; i++) { + llp_parser_process_byte(&parser, buffer[i], 0); + } + + // Verificar + TEST_ASSERT_EQUAL_INT(LLP_DATA, parser.frame.type); + TEST_ASSERT_EQUAL_INT(123, parser.frame.id); +} +``` + +### Pattern 4: Boundary Test + +```c +void test_maximum_payload_size(void) { + uint8_t buffer[1024]; + uint8_t max_payload[LLP_MAX_PAYLOAD]; + + size_t frame_len = llp_build_frame( + buffer, sizeof(buffer), + LLP_DATA, 1, + max_payload, LLP_MAX_PAYLOAD // Máximo permitido + ); + + TEST_ASSERT_GREATER_THAN_INT(0, frame_len); +} +``` + +--- + +## Unity Assertions Reference + +Common assertions used in tests: + +```c +TEST_ASSERT(condition) // condition == true +TEST_ASSERT_EQUAL_INT(expected, actual) // Integers +TEST_ASSERT_EQUAL_HEX8(expected, actual) // Hex bytes +TEST_ASSERT_EQUAL_HEX16(expected, actual) // Hex words +TEST_ASSERT_GREATER_THAN_INT(threshold, actual) +TEST_ASSERT_NULL(pointer) +TEST_ASSERT_NOT_NULL(pointer) +TEST_ASSERT_TRUE(condition) +TEST_ASSERT_FALSE(condition) +TEST_ASSERT_NOT_EQUAL_INT(unexpected, actual) +TEST_FAIL_MESSAGE("Custom error message") +``` + +--- + +## Compilation for Different Platforms + +### Arduino UNO + +```bash +platformio run -e arduino_uno +``` + +Output: +``` +Linking .pio/build/arduino_uno/firmware.elf +Checking size .pio/build/arduino_uno/firmware.elf + text data bss dec hex filename + 1234 256 100 1590 63a .pio/build/arduino_uno/firmware.elf +``` + +### ESP32 + +```bash +platformio run -e esp32 +``` + +### Multiple Platforms + +```bash +platformio run # Compila todos los entornos +``` + +--- + +## Debugging Failed Tests + +### Step 1: Run with Verbose Output + +```bash +platformio test -e test -vvv +``` + +### Step 2: Add Debug Printing (Unity) + +```c +void test_debug_example(void) { + llp_parser_t parser; + llp_parser_init(&parser); + + TEST_PRINTF("Parser state: %d\n", parser.state); + TEST_PRINTF("Parser bytes_received: %d\n", parser.bytes_received); + + TEST_ASSERT_EQUAL_INT(0, parser.state); +} +``` + +### Step 3: Check Values + +```bash +platformio test -e test -v 2>&1 | grep "ERROR\|FAILED" +``` + +### Step 4: Inspect Code + +```c +// Verifica que llp_protocol.c implementa: +extern void llp_parser_init(llp_parser_t* parser); +extern int llp_parser_process_byte(llp_parser_t* parser, + uint8_t byte, + unsigned long current_ms); +extern 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); +``` + +--- + +## CI/CD with GitHub Actions + +Tests se ejecutan automáticamente en: +- Cada push a `main`, `develop`, o `feature/*` +- Cada pull request a `main` o `develop` + +Ver resultados en GitHub → Actions → Workflow runs + +--- + +## Next Steps + +1. **Implementar** `src/llp_protocol.c` según v3.0.0 +2. **Ejecutar tests** - `platformio test -e test -v` +3. **Iterar** - Arregla fallos, mejora cobertura +4. **Validar hardware** - `platformio run -e arduino_uno` +5. **Commit** - Cuando todo funcione + +--- + +## Useful Commands Cheatsheet + +```bash +# Testing +platformio test -e test -v # Run all tests verbosely +platformio test -e test -vvv # Extra verbose +platformio test -e test --filter test_crc # Specific test file + +# Building +platformio run -e arduino_uno # Compile for Arduino UNO +platformio run -e esp32 # Compile for ESP32 +platformio run # Compile all environments + +# Cleanup +platformio run -t clean # Clean build artifacts +pio lib update # Update libraries + +# Info +platformio boards # List available boards +platformio envs # List configured environments +``` + +--- + +For more details, see `README.md` and `STRUCTURE.md`. From dd4065c7116d517d4284552788265e2e1eda6afd Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Mon, 11 May 2026 22:25:05 -0300 Subject: [PATCH 4/9] =?UTF-8?q?Actualizado=20proyecto=20y=20librer=C3=ADa?= =?UTF-8?q?=20a=20version=203.0.0.=20Se=20cambia=20estructura=20a=20Platfo?= =?UTF-8?q?rmIO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/platformio.yml | 20 + README.md | 410 ++------------- STRUCTURE.md | 433 +++------------- TESTING.md | 470 +++-------------- examples/minimal_uart/minimal_uart.ino | 260 ++-------- .../request_response/request_responde.ino | 172 ------- .../request_response/request_response.ino | 115 +++++ include/llp-protocol.h | 334 ------------- include/llp_protocol.h | 473 ++++++++++++++++++ library.properties | 4 +- src/llp_protocol.c | 1 + test/cross_test_generate.c | 236 +++++++++ test/test_crc.c | 92 ++++ test/test_frame_builder.c | 162 ++++++ test/test_main.c | 97 ++++ test/test_parser_init.c | 55 ++ test/test_parser_process.c | 219 ++++++++ 17 files changed, 1680 insertions(+), 1873 deletions(-) create mode 100644 .github/workflows/platformio.yml delete mode 100644 examples/request_response/request_responde.ino create mode 100644 examples/request_response/request_response.ino delete mode 100644 include/llp-protocol.h create mode 100644 include/llp_protocol.h create mode 100644 src/llp_protocol.c create mode 100644 test/cross_test_generate.c create mode 100644 test/test_crc.c create mode 100644 test/test_frame_builder.c create mode 100644 test/test_main.c create mode 100644 test/test_parser_init.c create mode 100644 test/test_parser_process.c diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml new file mode 100644 index 0000000..d00f5e0 --- /dev/null +++ b/.github/workflows/platformio.yml @@ -0,0 +1,20 @@ +name: PlatformIO CI + +on: + push: + branches: [main, develop, feature/*] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install PlatformIO + run: pip install platformio + - name: Run tests + run: platformio test -e test -v diff --git a/README.md b/README.md index 61561bf..310082a 100644 --- a/README.md +++ b/README.md @@ -1,403 +1,89 @@ -# 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) +[![PlatformIO CI](https://github.com/EnzoLeonel/llp-protocol/actions/workflows/platformio.yml/badge.svg)](https://github.com/EnzoLeonel/llp-protocol/actions/workflows/platformio.yml) +[![Arduino Compatible](https://img.shields.io/badge/Arduino-Compatible-brightgreen)](https://www.arduino.cc/) +[![C Standard](https://img.shields.io/badge/C-99-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. +Protocolo de comunicación **liviano, robusto y extensible** para microcontroladores. +Ideal para UART, RF (433MHz, LoRa), RS485, CAN y otros medios con ruido. **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 +- Ultra-liviano: ~500B de código, sin dependencias +- Robusto: CRC16-CCITT, byte stuffing, sincronización anti-ruido, timeouts +- Extensible: layer chain con metadata (passthrough y transform layers) +- Agnóstico del medio: UART, RF, RS485, Bluetooth, CAN, etc. +- Header-only library: incluir `llp_protocol.h` y listo --- -## 📑 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) - ---- - -## 📐 Estructura del Frame +## Wire Format ``` -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) +[0xAA][0x55][LEN_L][LEN_H][PAYLOAD...][CRC_L][CRC_H] ``` -**Total:** 9 bytes overhead + payload +Byte stuffing: `0xAA` en LEN/PAYLOAD/CRC se escribe como `0xAA 0x00`. +Layer chain en PAYLOAD: `[LAYER_ID][META_LEN][METADATA...]...[0x00][RAW DATA]` --- -## 🚀 Instalación - -### Para Arduino IDE: - -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 - -2. **Opción B: Vía Git** - ```bash - cd ~/Documents/Arduino/libraries - git clone https://github.com/EnzoLeonel/llp-protocol.git - ``` - -3. **Opción C: Manual** - - Copia `include/llp_protocol.h` a tu proyecto +## Estructura del proyecto -### Para C puro / Embedded: - -```bash -git clone https://github.com/EnzoLeonel/llp-protocol.git -#include "llp_protocol.h" +``` +llp-protocol/ +├── include/llp_protocol.h # API pública (single-header) +├── src/llp_protocol.c # Verifica compilación standalone +├── test/ # Test suites con Unity (35 tests) +│ ├── test_parser_init.c +│ ├── test_frame_builder.c +│ ├── test_parser_process.c +│ └── test_crc.c +├── examples/ # Sketches Arduino +├── .github/workflows/ # CI con GitHub Actions +├── platformio.ini # Configuración multi-plataforma +└── library.properties # Arduino Library Manager ``` --- -## ⚡ Uso Rápido +## Uso rápido -### Inicializar Parser - -```cpp +```c #include "llp_protocol.h" llp_parser_t parser; -llp_parser_init(&parser); -``` -### Procesar Bytes Entrantes - -```cpp void setup() { - Serial.begin(9600); - llp_parser_init(&parser); + Serial.begin(115200); + 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); + while (Serial.available()) { + uint8_t byte = Serial.read(); + int result = llp_parser_process_byte(&parser, byte, millis()); + if (result == 1) { + uint8_t data[64]; + int len = llp_get_final_payload(&parser.frame, data, sizeof(data)); + if (len > 0) { + // Procesar datos recibidos + } + } } - } -} - -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(); -} -``` - -### 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); } ``` --- -## 🔌 API Completa - -### Inicialización - -```cpp -void llp_parser_init(llp_parser_t* parser); -``` -Inicializa el parser a estado inicial. Debe llamarse una vez en `setup()`. - ---- - -### Procesamiento de Bytes - -```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); -``` - -**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) - ---- - -### Estadísticas - -```cpp -void llp_get_stats(llp_parser_t* parser, - uint32_t* frames_ok, - uint32_t* frames_error, - uint32_t* timeouts); -``` - -Obtiene contadores de frames recibidos, errores y timeouts. - -```cpp -void llp_reset_stats(llp_parser_t* parser); -``` - -Reinicia los contadores a cero. - ---- - -## 🏷️ Tipos de Mensaje - -| 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 | - -**Custom Types:** Usa valores 0x16-0xFF para tipos específicos de tu aplicación. - ---- - -## 📚 Ejemplos - -### Ejemplo 1: UART Simple (Arduino) - -Ver [`examples/minimal_uart/minimal_uart.ino`](examples/minimal_uart/) - -Envía un PING cada 5 segundos y procesa respuestas. - -### Ejemplo 2: Request-Response - -Ver [`examples/request_response/request_response.ino`](examples/request_response/) +## Ejecutar tests -Implementa patrón request-response con timeout y reintentos. - -### Ejemplo 3: Multi-node RS485 (próximamente) - ---- - -## ⚙️ Configuración Avanzada - -### Ajustar Payload Máximo - -Para **Arduino UNO/Nano (2KB RAM)**: -```cpp -#define LLP_MAX_PAYLOAD 64 // En lugar de 512 -``` - -Para **ESP8266/STM32 (> 32KB RAM)**: -```cpp -#define LLP_MAX_PAYLOAD 1024 // Más payload -``` - -### Modificar Timeout - -```cpp -#define LLP_FRAME_TIMEOUT_MS 5000 // Para RF lento o latente -``` - -### Magic Bytes Personalizados - -```cpp -#define LLP_MAGIC_1 0xCC -#define LLP_MAGIC_2 0xDD -``` - ---- - -## 📊 Performance - -### Consumo de Memoria - -| Componente | Bytes | -|------------|-------| -| `llp_parser_t` | ~600-650 | -| `llp_frame_t` (con payload 512B) | ~520 | -| Código ejecutable | ~500 | -| **Total (mínimo)** | ~650 | - -Para Arduino UNO (2KB RAM): ~30% de memoria disponible. - -### Velocidad - -- **Parsing:** ~50 µs por byte (Arduino UNO, 16 MHz) -- **CRC16:** ~15 µs por byte -- **Frame builder:** ~100 µs (típico) - -### Throughput (UART 9600 baud) - -``` -8 bits/byte + 1 start + 1 stop = 10 bits/byte -9600 baud / 10 = 960 bytes/seg - -Con 20B overhead + 100B payload = 120B/frame -960 / 120 ≈ 8 frames/segundo máximo +```bash +platformio test -e test -v ``` --- -## 🛡️ Seguridad y Robustez - -| 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 | - ---- - -## ❓ Preguntas Frecuentes - -**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. - -**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()`. - -**P: ¿Cómo detectar comandos perdidos?** -R: Usa el campo `ID` para correlacionar. Si no recibes ACK en N milisegundos, retransmite. - -**P: ¿Soporta fragmentación?** -R: No automática. Para payloads > 512B, implementa en tu aplicación (e.g., ID + fragment counter en payload). - -**P: ¿Consume mucho flash en Arduino?** -R: ~500-700 bytes. Compatible con Arduino UNO/Nano. - ---- - -## 🤝 Contribuciones - -Las **contribuciones son bienvenidas**: - -- 🐛 Reporta bugs en [Issues](https://github.com/EnzoLeonel/llp-protocol/issues) -- 💡 Propón mejoras -- 📝 Agrega ejemplos (RF, RS485, etc) -- ✏️ Mejora documentación - -### Cómo contribuir: - -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 - ---- - -## 📜 Licencia - -MIT License - Ver [`LICENSE`](LICENSE) - -Uso, modificación y distribución libres sin restricciones. - ---- - -## ✍️ Autor - -Creado por **EnzoLeonel** con la colaboración de amigos revisores. - ---- - -## 📞 Contacto / Soporte - -- 📧 [Crear Issue](https://github.com/EnzoLeonel/llp-protocol/issues) -- 💬 [Discussions](https://github.com/EnzoLeonel/llp-protocol/discussions) - ---- +## Licencia -**Last updated:** 2026-03-30 -**Version:** 1.0.0 +MIT — Ver [LICENSE](LICENSE) diff --git a/STRUCTURE.md b/STRUCTURE.md index d943b11..87b5367 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -1,402 +1,87 @@ -# Project Structure - LLP Protocol v3.0.0 +# Project Structure — LLP Protocol v3.0.0 ## Directory Tree ``` llp-protocol/ │ -├── 📄 platformio.ini # Configuración central de PlatformIO -│ # Define entornos, compiladores, flags +├── platformio.ini # PlatformIO configuration │ -├── 📁 src/ # Código fuente principal -│ └── llp_protocol.c # Implementación del protocolo LLP +├── src/ +│ └── llp_protocol.c # Source (includes header-only lib) │ -├── 📁 include/ # Headers públicos -│ └── llp_protocol.h # API pública del protocolo +├── include/ +│ └── llp_protocol.h # Public API (single-header library) │ -├── 📁 test/ # Suite de tests con Unity -│ ├── test_parser_init.c # 5 tests - Inicialización -│ ├── test_frame_builder.c # 10 tests - Construcción de frames -│ ├── test_parser_process.c # 9 tests - Procesamiento byte-a-byte -│ └── test_crc.c # 9 tests - Validación de CRC16-CCITT +├── test/ +│ ├── 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 │ -├── 📁 lib/ # Librerías externas -│ └── (unity instalado automáticamente) +├── lib/ # External libraries (auto-installed) +│ └── (Unity via PIO lib-deps) │ -├── 📁 examples/ # Sketches y ejemplos +├── examples/ │ ├── minimal_uart/ -│ │ └── minimal_uart.ino # Ejemplo: UART simple +│ │ └── minimal_uart.ino # Minimal UART echo example │ └── request_response/ -│ └── request_response.ino # Ejemplo: Request-Response +│ └── request_response.ino # Request-response with retries │ -├── 📁 docs/ # Documentación -│ ├── (specs detalladas del protocolo) +├── docs/ +│ └── PROTOCOL.md # Protocol specification │ -├── 📁 .github/ +├── .github/ │ └── workflows/ -│ └── platformio.yml # CI/CD automatizado en GitHub Actions +│ └── platformio.yml # CI/CD via GitHub Actions │ -├── 📄 .gitignore # Archivos ignorados por Git -├── 📄 README.md # Documentación principal -├── 📄 TESTING.md # Guía de testing (este archivo) -├── 📄 STRUCTURE.md # Explicación de estructura -├── 📄 CHANGELOG.md # Historial de cambios -├── 📄 LICENSE # MIT License -└── 📄 library.properties # Metadata para Arduino Library Manager +├── .gitignore +├── CHANGELOG.md +├── LICENSE +├── README.md +├── STRUCTURE.md # This file +├── TESTING.md # Testing guide +└── library.properties # Arduino Library Manager metadata ``` ## File Purposes -### Core Files - -| Archivo | Propósito | Tamaño Típico | -|---------|-----------|---------------| -| `platformio.ini` | Configuración central | ~2.5 KB | -| `src/llp_protocol.c` | Implementación | ~3-5 KB | -| `include/llp_protocol.h` | API pública | ~1-2 KB | -| `lib/unity/` | Framework de testing | ~100 KB | - -### Test Files - -| Archivo | Descripción | Tests | Cobertura | -|---------|-------------|-------|-----------| -| `test/test_parser_init.c` | Inicialización del parser | 5 | Estado inicial, variables | -| `test/test_frame_builder.c` | Construcción de frames | 10 | Magic, tipo, ID, payload, CRC | -| `test/test_parser_process.c` | Procesamiento byte-a-byte | 9 | Parsing, state machine, recovery | -| `test/test_crc.c` | Validación CRC16-CCITT | 9 | Checksum, detección de errores | - -**Total: 33 tests** - -### GitHub Actions - -| Archivo | Propósito | -|---------|-----------| -| `.github/workflows/platformio.yml` | Ejecuta tests en cada push/PR | - -### Documentation - -| Archivo | Contenido | -|---------|----------| -| `README.md` | Documentación principal, instalación, API | -| `TESTING.md` | Guía completa de testing | -| `STRUCTURE.md` | Este archivo - explicación de estructura | -| `CHANGELOG.md` | Historial de versiones | -| `docs/` | Especificaciones detalladas del protocolo | - -## Configuration: platformio.ini - -### Section: [platformio] - -Define comportamiento global de PlatformIO: - -```ini -[platformio] -src_dir = src # Dónde está el código principal -lib_dir = lib # Dónde van las librerías externas -include_dir = include # Path de headers -test_dir = test # Dónde están los tests -default_envs = test # Entorno por defecto -``` - -### Environments - -Cada `[env:nombre]` define cómo compilar para una plataforma: - -```ini -[env:test] -platform = native # Compilar con gcc nativo (no Arduino) -test_framework = unity # Usar Unity para tests -build_flags = # Flags del compilador C - -Wall - -DLLP_MAX_PAYLOAD=512 -``` - -### Available Environments - -```bash -platformio test -e test # Tests (compilación nativa) -platformio run -e arduino_uno # Arduino UNO -platformio run -e arduino_nano # Arduino Nano -platformio run -e arduino_mega # Arduino Mega -platformio run -e esp8266 # ESP8266 -platformio run -e esp32 # ESP32 -platformio run -e stm32f103 # STM32 -platformio run -e attiny85 # ATtiny85 -``` - -## Build Process - -### Step 1: Compilation - -``` -Source Files (.c, .h) - ↓ -Preprocessor (macros, includes) - ↓ -C Compiler (gcc/arm-gcc) - ↓ -Object Files (.o) -``` - -### Step 2: Linking - -``` -Object Files (.o) - ↓ -Unity Framework Library - ↓ -Linker - ↓ -Executable -``` - -### Step 3: Execution (Tests) - -``` -Executable - ↓ -Run Tests - ↓ -Compare Results - ↓ -Pass/Fail Report -``` - -## Memory Layout (Arduino UNO) - -``` -Total RAM: 2 KB (2048 bytes) - -llp_parser_t struct: ~600 bytes -llp_frame_t struct: ~520 bytes -Local variables: ~100 bytes -Stack overhead: ~100 bytes -─────────────────────────── -Available for application: ~730 bytes (36% of RAM) -``` - -## Build Artifacts - -PlatformIO crea automáticamente: - -``` -.pio/ -├── build/ -│ ├── test/ # Compilación de tests -│ │ ├── firmware.elf -│ │ └── (archivos objeto) -│ ├── arduino_uno/ -│ │ ├── firmware.hex # Imagen para cargar en Arduino -│ │ └── (archivos objeto) -│ └── ... -├── build_cache/ -└── ... -``` - -## Adding New Features - -### Scenario: Añadir nueva función `llp_validate_frame()` - -#### 1. Declare en Header - -```c -// include/llp_protocol.h -bool llp_validate_frame(const llp_frame_t* frame); -``` - -#### 2. Implement en Source - -```c -// src/llp_protocol.c -bool llp_validate_frame(const llp_frame_t* frame) { - if (!frame) return false; - if (frame->payload_len > LLP_MAX_PAYLOAD) return false; - return true; -} -``` - -#### 3. Write Tests - -```c -// test/test_validation.c -void test_validate_frame_returns_true_for_valid_frame(void) { - llp_frame_t frame = { - .type = LLP_DATA, - .id = 1, - .payload_len = 10 - }; - TEST_ASSERT_TRUE(llp_validate_frame(&frame)); -} - -void test_validate_frame_returns_false_for_null(void) { - TEST_ASSERT_FALSE(llp_validate_frame(NULL)); -} -``` - -#### 4. Run Tests - -```bash -platformio test -e test -v -``` - -#### 5. Commit - -```bash -git add include/llp_protocol.h src/llp_protocol.c test/test_validation.c -git commit -m "Add frame validation function" -``` - -## Version Management - -### Current Version Structure - -``` -Version 3.0.0 -│ -├── Major (3) - Breaking changes -├── Minor (0) - New features (backwards compatible) -└── Patch (0) - Bug fixes -``` - -Update in: -1. `library.properties` - Arduino Library Manager -2. `README.md` - Documentation -3. `CHANGELOG.md` - Release notes -4. GitHub Tags - `git tag v3.0.0` - -## Git Workflow - -### Feature Branch - -```bash -git checkout -b feature/v3.0.0 -# ... hacer cambios ... -git add . -git commit -m "Add testing infrastructure" -git push origin feature/v3.0.0 -``` - -### Create PR - -- Push to GitHub -- Open Pull Request -- CI/CD runs automatically -- Review and merge - -### Release - -```bash -git checkout main -git merge feature/v3.0.0 -git tag v3.0.0 -git push origin main --tags -``` - -## Performance Considerations - -### Code Size - -| Component | Flash | RAM | -|-----------|-------|-----| -| llp_protocol.c | ~1-2 KB | ~600 B | -| Unity tests | ~0 B | ~1 KB | -| Arduino Framework | ~2 KB | ~400 B | - -### Execution Time - -- **Parsing 1 byte**: ~50 µs -- **Building frame**: ~100 µs -- **CRC calculation**: ~15 µs per byte - -## Troubleshooting - -### Issue: Tests don't compile +| 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_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 | +| `.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 | +| **Total** | **35** | | + +## Running Tests ```bash -# Verificar path de includes -platformio test -e test -vvv - -# Limpiar y reconstruir -platformio run -t clean -platformio test -e test +platformio test -e test -v # All tests verbose +platformio test -e test --filter test_crc # Specific suite ``` -### Issue: Can't find Unity framework +## Build Environments ```bash -# Reinstalar dependencias -pio lib update -platformio run --update-all -``` - -### Issue: Code compiles for test but not for Arduino - -Verificar: -1. Includes en `platformio.ini` -2. Archivos de configuración de Arduino no compilados en test -3. Usar `#ifdef` para código específico de plataforma - -```c -#ifdef ARDUINO - Serial.println("Arduino only"); -#endif -``` - -## Best Practices - -### 1. Organize Tests Logically - -Separate test files por funcionalidad: -- `test_parser_*.c` para parser -- `test_frame_*.c` para frame building -- `test_crc_*.c` para CRC - -### 2. Keep Tests Independent - -Cada test debe funcionar sin depender de otros. - -✅ Bueno: -```c -void test_feature_a(void) { - // Setup completo - llp_parser_t parser; - llp_parser_init(&parser); - // Test -} -``` - -### 3. Use Meaningful Names - -```c -// ✅ Descriptive -void test_parser_recovers_from_corrupted_magic_bytes(void) { } - -// ❌ Vague -void test_parser_recovery(void) { } +platformio run -e arduino_uno # Arduino UNO +platformio run -e arduino_nano # Arduino Nano +platformio run -e arduino_mega # Arduino Mega +platformio run -e esp8266 # ESP8266 +platformio run -e esp32 # ESP32 +platformio run -e stm32f103 # STM32 (Nucleo) +platformio run -e attiny85 # ATtiny85 ``` - -### 4. Document Complex Tests - -```c -// Test que verifica sincronización RF con ruido simulado -void test_parser_syncs_with_rf_noise(void) { - // Enviar bytes de ruido - llp_parser_process_byte(&parser, 0xFF, 0); - llp_parser_process_byte(&parser, 0xFF, 0); - - // Enviar frame válido - // Debe recuperarse y parsear correctamente -} -``` - -## Next Steps - -1. **Implementar `src/llp_protocol.c`** según v3.0.0 -2. **Ejecutar tests** - `platformio test -e test -v` -3. **Compilar ejemplos** - `platformio run -e arduino_uno` -4. **Validar hardware** - Cargar en Arduino/ESP32 -5. **Crear PR** - Abrir pull request a `main` - ---- - -**For more details, see `TESTING.md` and `README.md`** diff --git a/TESTING.md b/TESTING.md index 0943ff9..6016483 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,418 +1,84 @@ -# Testing Guide - LLP Protocol v3.0.0 +# Testing Guide — LLP Protocol v3.0.0 -## Quick Start - -### Prerequisites - -Instala PlatformIO: +## Prerequisites ```bash pip install platformio ``` -### Run All Tests +## Run All Tests ```bash -cd llp-protocol -git checkout feature/v3.0.0 platformio test -e test -v ``` Expected output: ``` -========= [PASSED] ========= -33 passed in 0.42s -``` - ---- - -## Test Structure - -### 4 Test Suites - -| Suite | File | Tests | Focus | -|-------|------|-------|-------| -| **Initialization** | `test/test_parser_init.c` | 5 | Parser startup, state reset | -| **Frame Building** | `test/test_frame_builder.c` | 10 | Frame construction, CRC | -| **Processing** | `test/test_parser_process.c` | 9 | Byte parsing, state machine | -| **CRC Validation** | `test/test_crc.c` | 9 | CRC16-CCITT, error detection | - -**Total: 33 tests** - ---- - -## Test Execution Examples - -### Run Tests Verbosely - -```bash -platformio test -e test -v -``` - -Output: -``` -Running test_parser_init_sets_state_to_waiting_magic ... [PASSED] -Running test_parser_init_initializes_all_fields ... [PASSED] -... -``` - -### Run Specific Test File +test_parser_init [PASSED] +test_frame_builder [PASSED] +test_parser_process [PASSED] +test_crc [PASSED] +``` + +Total: **35 tests** + +## Test Suites + +### test_parser_init.c (5 tests) +| Test | What it verifies | +|------|-----------------| +| `test_init_sets_state_wait_magic1` | State after init is WAIT_MAGIC1 | +| `test_init_clears_all_fields` | All struct fields properly zeroed | +| `test_init_clears_frame_payload_len` | Frame payload length is 0 | +| `test_can_be_reinitialized` | Calling init again resets everything | +| `test_init_resets_stats` | frames_ok/error/timeouts start at 0 | + +### test_frame_builder.c (11 tests) +| Test | What it verifies | +|------|-----------------| +| `test_build_final_payload_empty` | Empty data -> `[0x00]` | +| `test_build_final_payload_with_data` | Data wrapped correctly | +| `test_build_final_payload_small_buffer` | Returns 0 when buffer too small | +| `test_build_frame_magic_bytes` | Frame starts with 0xAA 0x55 | +| `test_build_frame_payload_length` | Length field correct LE | +| `test_build_frame_roundtrip` | Build -> parse -> extract matches | +| `test_build_frame_small_buffer` | Returns 0 for tiny output buffer | +| `test_build_frame_oversized_payload` | Returns 0 when payload > LLP_MAX_PAYLOAD | +| `test_build_frame_stuffing` | 0xAA in payload is stuffed/unstuffed | +| `test_build_frame_deterministic` | Same input -> same output | +| `test_build_frame_empty_payload_chain` | FinalNode-only frame roundtrips | + +### test_parser_process.c (11 tests) +| Test | What it verifies | +|------|-----------------| +| `test_process_incomplete_frame_returns_zero` | Partial frame returns 0 | +| `test_process_complete_frame_returns_one` | Complete frame returns 1 | +| `test_process_extracts_correct_data` | Received data matches sent data | +| `test_process_crc_error_returns_negative_one` | Corrupted CRC returns -1 | +| `test_process_timeout_returns_negative_one` | Timeout returns -1 | +| `test_process_recovers_from_noise` | Random bytes before frame | +| `test_process_multiple_frames` | 3 consecutive frames parsed OK | +| `test_process_increments_error_counter` | OK and error counters tracked | +| `test_process_stuffed_byte` | 0xAA in payload handles correctly | +| `test_process_sync_error_on_aa55_inside_frame` | 0xAA 0x55 mid-frame detected | +| `test_process_payload_len_error` | Oversized payload length rejected | + +### test_crc.c (8 tests) +| Test | What it verifies | +|------|-----------------| +| `test_crc16_known_values` | "123456789" -> 0x29B1 (CCITT standard) | +| `test_crc16_differs_for_different_payloads` | Different data -> different CRC | +| `test_crc16_detects_bit_flip` | Single bit flip changes CRC | +| `test_crc16_detects_burst_error` | Multi-bit errors detected | +| `test_crc16_deterministic` | Same data -> same CRC always | +| `test_crc16_update_chain` | Sequential updates match batch | +| `test_crc16_order_matters` | "AB" != "BA" | +| `test_crc16_empty` | Zero-length input -> 0xFFFF | + +## Debugging ```bash -platformio test -e test --filter test_frame_builder +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 ``` - -### Run Tests with Extra Verbosity - -```bash -platformio test -e test -vvv -``` - -Shows compiler warnings, full linking output, etc. - ---- - -## Test File Breakdown - -### test_parser_init.c - -**Purpose:** Validar que el parser se inicialice correctamente - -**Tests:** -1. `test_parser_init_sets_state_to_waiting_magic` - Estado inicial correcto -2. `test_parser_init_initializes_all_fields` - Todos los campos inicializados -3. `test_parser_init_clears_frame_buffer` - Buffer vacío -4. `test_parser_can_be_reinitialized` - Reset funciona -5. `test_parser_init_resets_stats` - Estadísticas a cero - -**Example assertion:** -```c -TEST_ASSERT_EQUAL_INT(0, parser.state); -``` - -### test_frame_builder.c - -**Purpose:** Validar construcción correcta de frames - -**Tests:** -1. `test_build_frame_with_empty_payload` - Frame sin payload -2. `test_build_frame_sets_correct_magic_bytes` - 0xAA, 0x55 -3. `test_build_frame_sets_type_field` - Campo tipo correcto -4. `test_build_frame_stores_id_little_endian` - ID en LE -5. `test_build_frame_sets_payload_length` - Longitud correcta -6. `test_build_frame_copies_payload_correctly` - Datos copiados -7. `test_build_frame_includes_crc16` - CRC presente -8. `test_build_frame_returns_zero_for_small_buffer` - Validación de buffer -9. `test_build_frame_returns_zero_for_oversized_payload` - Payload máximo -10. `test_build_frame_works_with_all_message_types` - Todos los tipos - -**Example:** -```c -uint8_t buffer[520]; -size_t frame_len = llp_build_frame( - buffer, sizeof(buffer), - LLP_PING, 123, - NULL, 0 -); -TEST_ASSERT_EQUAL_HEX8(0xAA, buffer[0]); -``` - -### test_parser_process.c - -**Purpose:** Validar procesamiento byte por byte - -**Tests:** -1. `test_process_byte_returns_zero_for_incomplete_frame` - Retorna 0 si incompleto -2. `test_process_byte_returns_one_for_complete_frame` - Retorna 1 si completo -3. `test_process_byte_extracts_correct_frame_data` - Datos extraídos correctamente -4. `test_process_byte_returns_negative_one_for_crc_error` - Retorna -1 si error -5. `test_process_byte_recovers_from_bad_magic_bytes` - Recuperación de ruido -6. `test_process_byte_handles_multiple_frames` - Múltiples frames -7. `test_process_byte_increments_frames_ok_counter` - Contador de frames OK -8. `test_process_byte_handles_maximum_payload` - Payload máximo -9. `test_process_byte_handles_all_message_types` - Todos los tipos - -**Example:** -```c -llp_parser_t parser; -llp_parser_init(&parser); - -// Procesar frame byte por byte -for (size_t i = 0; i < frame_len; i++) { - result = llp_parser_process_byte(&parser, buffer[i], 0); -} - -TEST_ASSERT_EQUAL_INT(1, result); // Frame completo -``` - -### test_crc.c - -**Purpose:** Validar CRC16-CCITT - -**Tests:** -1. `test_crc16_calculated_for_empty_frame` - CRC incluso sin payload -2. `test_crc16_differs_for_different_payloads` - CRC varía con payload -3. `test_crc16_differs_for_different_types` - CRC varía con tipo -4. `test_crc16_differs_for_different_ids` - CRC varía con ID -5. `test_crc16_detects_single_bit_flip_in_payload` - Detecta 1 bit cambiado -6. `test_crc16_detects_burst_error` - Detecta múltiples bits cambiados -7. `test_crc16_stored_in_correct_byte_positions` - Posición correcta -8. `test_crc16_valid_for_maximum_payload` - Válido con payload máximo -9. `test_crc16_is_deterministic` - Resultado consistente - ---- - -## Understanding Test Results - -### Success Output - -``` -Platform native [100%] ✔ -Test ✔ 33 passed in 0.42s -========= [PASSED] ========= -``` - -**Meaning:** Todos los 33 tests pasaron. - -### Failure Output - -``` -Running test_build_frame_sets_correct_magic_bytes ... [FAILED] - -Expected 0xAA but was 0xFF -``` - -**Meaning:** El primer byte del frame no es 0xAA. Revisa `llp_build_frame()`. - -### Compilation Error - -``` -src/llp_protocol.c:15:5: error: conflicting types for 'llp_parser_init' -``` - -**Meaning:** Hay un error en el código. Verifica los tipos en la declaración vs definición. - ---- - -## Common Test Patterns - -### Pattern 1: Basic Functionality Test - -```c -void test_feature_works_correctly(void) { - // Setup - llp_parser_t parser; - llp_parser_init(&parser); - - // Action - int result = llp_parser_process_byte(&parser, 0xAA, 0); - - // Assert - TEST_ASSERT_EQUAL_INT(0, result); // Incompleto -} -``` - -### Pattern 2: Error Handling Test - -```c -void test_feature_handles_error(void) { - uint8_t buffer[4]; // Muy pequeño - - size_t frame_len = llp_build_frame( - buffer, sizeof(buffer), - LLP_DATA, 1, - NULL, 0 - ); - - TEST_ASSERT_EQUAL_INT(0, frame_len); // Error -} -``` - -### Pattern 3: Integration Test - -```c -void test_build_and_parse_frame(void) { - // Construir - uint8_t buffer[520]; - uint8_t payload[] = {0xAA, 0xBB}; - - size_t frame_len = llp_build_frame( - buffer, sizeof(buffer), - LLP_DATA, 123, - payload, sizeof(payload) - ); - - // Parsear - llp_parser_t parser; - llp_parser_init(&parser); - - for (size_t i = 0; i < frame_len; i++) { - llp_parser_process_byte(&parser, buffer[i], 0); - } - - // Verificar - TEST_ASSERT_EQUAL_INT(LLP_DATA, parser.frame.type); - TEST_ASSERT_EQUAL_INT(123, parser.frame.id); -} -``` - -### Pattern 4: Boundary Test - -```c -void test_maximum_payload_size(void) { - uint8_t buffer[1024]; - uint8_t max_payload[LLP_MAX_PAYLOAD]; - - size_t frame_len = llp_build_frame( - buffer, sizeof(buffer), - LLP_DATA, 1, - max_payload, LLP_MAX_PAYLOAD // Máximo permitido - ); - - TEST_ASSERT_GREATER_THAN_INT(0, frame_len); -} -``` - ---- - -## Unity Assertions Reference - -Common assertions used in tests: - -```c -TEST_ASSERT(condition) // condition == true -TEST_ASSERT_EQUAL_INT(expected, actual) // Integers -TEST_ASSERT_EQUAL_HEX8(expected, actual) // Hex bytes -TEST_ASSERT_EQUAL_HEX16(expected, actual) // Hex words -TEST_ASSERT_GREATER_THAN_INT(threshold, actual) -TEST_ASSERT_NULL(pointer) -TEST_ASSERT_NOT_NULL(pointer) -TEST_ASSERT_TRUE(condition) -TEST_ASSERT_FALSE(condition) -TEST_ASSERT_NOT_EQUAL_INT(unexpected, actual) -TEST_FAIL_MESSAGE("Custom error message") -``` - ---- - -## Compilation for Different Platforms - -### Arduino UNO - -```bash -platformio run -e arduino_uno -``` - -Output: -``` -Linking .pio/build/arduino_uno/firmware.elf -Checking size .pio/build/arduino_uno/firmware.elf - text data bss dec hex filename - 1234 256 100 1590 63a .pio/build/arduino_uno/firmware.elf -``` - -### ESP32 - -```bash -platformio run -e esp32 -``` - -### Multiple Platforms - -```bash -platformio run # Compila todos los entornos -``` - ---- - -## Debugging Failed Tests - -### Step 1: Run with Verbose Output - -```bash -platformio test -e test -vvv -``` - -### Step 2: Add Debug Printing (Unity) - -```c -void test_debug_example(void) { - llp_parser_t parser; - llp_parser_init(&parser); - - TEST_PRINTF("Parser state: %d\n", parser.state); - TEST_PRINTF("Parser bytes_received: %d\n", parser.bytes_received); - - TEST_ASSERT_EQUAL_INT(0, parser.state); -} -``` - -### Step 3: Check Values - -```bash -platformio test -e test -v 2>&1 | grep "ERROR\|FAILED" -``` - -### Step 4: Inspect Code - -```c -// Verifica que llp_protocol.c implementa: -extern void llp_parser_init(llp_parser_t* parser); -extern int llp_parser_process_byte(llp_parser_t* parser, - uint8_t byte, - unsigned long current_ms); -extern 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); -``` - ---- - -## CI/CD with GitHub Actions - -Tests se ejecutan automáticamente en: -- Cada push a `main`, `develop`, o `feature/*` -- Cada pull request a `main` o `develop` - -Ver resultados en GitHub → Actions → Workflow runs - ---- - -## Next Steps - -1. **Implementar** `src/llp_protocol.c` según v3.0.0 -2. **Ejecutar tests** - `platformio test -e test -v` -3. **Iterar** - Arregla fallos, mejora cobertura -4. **Validar hardware** - `platformio run -e arduino_uno` -5. **Commit** - Cuando todo funcione - ---- - -## Useful Commands Cheatsheet - -```bash -# Testing -platformio test -e test -v # Run all tests verbosely -platformio test -e test -vvv # Extra verbose -platformio test -e test --filter test_crc # Specific test file - -# Building -platformio run -e arduino_uno # Compile for Arduino UNO -platformio run -e esp32 # Compile for ESP32 -platformio run # Compile all environments - -# Cleanup -platformio run -t clean # Clean build artifacts -pio lib update # Update libraries - -# Info -platformio boards # List available boards -platformio envs # List configured environments -``` - ---- - -For more details, see `README.md` and `STRUCTURE.md`. diff --git a/examples/minimal_uart/minimal_uart.ino b/examples/minimal_uart/minimal_uart.ino index 9a642bf..f96ace6 100644 --- a/examples/minimal_uart/minimal_uart.ino +++ b/examples/minimal_uart/minimal_uart.ino @@ -1,241 +1,47 @@ /** - * 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); + } + } } - } - 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..e21ae80 --- /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 < 2) 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..75e3eec --- /dev/null +++ b/include/llp_protocol.h @@ -0,0 +1,473 @@ +/** + * 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); + 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 0; +} + +// ============================================================================= +// 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..d32d771 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ -LLP Protocol -version=1.0.0 +name=LLP Protocol +version=3.0.0 author=EnzoLeonel maintainer=EnzoLeonel sentence=Lightweight Link Protocol for microcontrollers 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/cross_test_generate.c b/test/cross_test_generate.c new file mode 100644 index 0000000..fa3db1b --- /dev/null +++ b/test/cross_test_generate.c @@ -0,0 +1,236 @@ +/** + * cross_test_generate.c — Cross-language test vector generator for LLP v3.0.0 + * + * Builds frames from known test vectors and writes them as binary files. + * Also reads Java-generated frames and parses them with the C parser. + * + * Compile: gcc -std=c99 -I include -o /tmp/llp_cross_gen cross_test_generate.c + * Usage: /tmp/llp_cross_gen [output_dir] + */ + +#include +#include +#include +#include "llp_protocol.h" + +#ifndef CROSS_OUTPUT_DIR +#define CROSS_OUTPUT_DIR "/tmp/llp_cross" +#endif + +typedef struct { + const char* label; + const uint8_t* data; + uint16_t len; +} test_vector_t; + +static int write_binary(const char* path, const uint8_t* data, size_t len) { + FILE* f = fopen(path, "wb"); + if (!f) { perror("fopen"); return -1; } + size_t w = fwrite(data, 1, len, f); + fclose(f); + return (w == len) ? 0 : -1; +} + +static uint8_t* read_binary(const char* path, size_t* out_len) { + FILE* f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + if (sz <= 0) { fclose(f); return NULL; } + uint8_t* buf = (uint8_t*)malloc((size_t)sz); + if (!buf) { fclose(f); return NULL; } + *out_len = (size_t)fread(buf, 1, (size_t)sz, f); + fclose(f); + return buf; +} + +static void print_hex(const char* label, const uint8_t* data, size_t len) { + printf(" %s (%zu bytes): ", label, len); + for (size_t i = 0; i < len; i++) printf("%02X ", data[i]); + printf("\n"); +} + +static int self_verify(const uint8_t* frame, size_t frame_len, + const uint8_t* expected_data, uint16_t expected_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[i], 0); + + if (result != 1) { + printf(" FAIL: C parser returned %d (expected 1)\n", result); + return -1; + } + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + if (extracted_len < 0) { + printf(" FAIL: llp_get_final_payload returned %d\n", extracted_len); + return -2; + } + if ((uint16_t)extracted_len != expected_len) { + printf(" FAIL: extracted len %d != expected %u\n", + extracted_len, expected_len); + return -3; + } + if (memcmp(extracted, expected_data, expected_len) != 0) { + printf(" FAIL: extracted data differs\n"); + return -4; + } + return 0; +} + +static int cross_verify(const uint8_t* frame, size_t frame_len, + const test_vector_t* vec) { + 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[i], 0); + + if (result != 1) { + printf(" CROSS-VERIFY [%s]: parser returned %d (expected 1)\n", + vec->label, result); + return -1; + } + + uint8_t extracted[LLP_MAX_PAYLOAD]; + int extracted_len = llp_get_final_payload(&parser.frame, + extracted, sizeof(extracted)); + if (extracted_len < 0) { + printf(" CROSS-VERIFY [%s]: get_final_payload returned %d\n", + vec->label, extracted_len); + return -2; + } + if ((uint16_t)extracted_len != vec->len) { + printf(" CROSS-VERIFY [%s]: len %d != expected %u\n", + vec->label, extracted_len, vec->len); + printf(" Extracted: "); + for (int i = 0; i < extracted_len; i++) printf("%02X ", extracted[i]); + printf("\n"); + return -3; + } + if (memcmp(extracted, vec->data, vec->len) != 0) { + printf(" CROSS-VERIFY [%s]: data differs\n", vec->label); + printf(" Expected: "); + for (uint16_t i = 0; i < vec->len; i++) printf("%02X ", vec->data[i]); + printf("\n Got: "); + for (int i = 0; i < extracted_len; i++) printf("%02X ", extracted[i]); + printf("\n"); + return -4; + } + + printf(" CROSS-VERIFY [%s]: OK (%u bytes)\n", vec->label, vec->len); + return 0; +} + +int main(int argc, char** argv) { + const char* out_dir = (argc > 1) ? argv[1] : CROSS_OUTPUT_DIR; + + char mkdir_cmd[256]; + snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s", out_dir); + system(mkdir_cmd); + + /* Test vectors: raw application data */ + const uint8_t empty_data[] = {}; + const uint8_t single_data[] = {0x42}; + const uint8_t hello_data[] = {'H', 'e', 'l', 'l', 'o'}; + const uint8_t aa_data[] = {0xAA}; + const uint8_t aa55_data[] = {0xAA, 0x55}; + const uint8_t triple_aa_data[] = {0xAA, 0xAA, 0xAA}; + const uint8_t mixed_data[] = {0x01, 0xAA, 0x02, 0xAA, 0x03}; + const uint8_t seq_data[] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F}; + const uint8_t zeros_data[32]= {0}; + const uint8_t ff_data[16] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; + + test_vector_t vectors[] = { + {"empty", empty_data, 0}, + {"single", single_data, 1}, + {"hello", hello_data, 5}, + {"aa_byte", aa_data, 1}, + {"aa55", aa55_data, 2}, + {"triple_aa", triple_aa_data, 3}, + {"mixed_aa", mixed_data, 5}, + {"sequence", seq_data, 16}, + {"zeros", zeros_data, 32}, + {"all_ff", ff_data, 16}, + }; + size_t num_vectors = sizeof(vectors) / sizeof(vectors[0]); + int all_ok = 1; + + printf("========================================\n"); + printf("LLP v3.0.0 Cross-Language Test Generator\n"); + printf("========================================\n\n"); + + /* Phase 1: Generate C frames */ + printf("--- Phase 1: Generating C frames ---\n\n"); + for (size_t v = 0; v < num_vectors; v++) { + uint8_t layer_chain[LLP_MAX_PAYLOAD]; + size_t layer_len = llp_build_final_payload( + layer_chain, sizeof(layer_chain), + vectors[v].data, vectors[v].len); + + uint8_t frame[LLP_MAX_FRAME_SIZE(layer_len)]; + size_t frame_len = llp_build_frame(frame, sizeof(frame), + layer_chain, layer_len); + + printf("Vector %zu: %s\n", v + 1, vectors[v].label); + + if (self_verify(frame, frame_len, vectors[v].data, vectors[v].len) == 0) + printf(" SELF-VERIFY: OK\n"); + else { + printf(" SELF-VERIFY: FAILED\n"); + all_ok = 0; + } + + char path[128]; + snprintf(path, sizeof(path), "%s/c_frame_%s.bin", out_dir, vectors[v].label); + write_binary(path, frame, frame_len); + printf(" Wrote: %s (%zu bytes)\n", path, frame_len); + + snprintf(path, sizeof(path), "%s/c_layerchain_%s.bin", out_dir, vectors[v].label); + write_binary(path, layer_chain, layer_len); + printf(" Wrote: %s (%zu bytes)\n\n", path, layer_len); + } + + /* Phase 2: Verify Java frames with C parser */ + printf("--- Phase 2: Verifying Java frames with C parser ---\n\n"); + int found_any = 0; + for (size_t v = 0; v < num_vectors; v++) { + char path[128]; + snprintf(path, sizeof(path), "%s/java_frame_%s.bin", out_dir, vectors[v].label); + + size_t frame_len; + uint8_t* frame = read_binary(path, &frame_len); + if (!frame) { + printf(" [%s] No Java frame found (run Java test first)\n", vectors[v].label); + continue; + } + found_any = 1; + printf(" Java frame [%s]: %zu bytes\n", vectors[v].label, frame_len); + + if (cross_verify(frame, frame_len, &vectors[v]) != 0) + all_ok = 0; + + free(frame); + } + + if (!found_any) + printf(" (No Java frames to verify)\n"); + + printf("\n========================================\n"); + if (all_ok) { + printf("RESULT: ALL TESTS PASSED\n"); + return 0; + } else { + printf("RESULT: SOME TESTS FAILED\n"); + return 1; + } +} 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..b751e73 --- /dev/null +++ b/test/test_main.c @@ -0,0 +1,97 @@ +#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); + +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); + + return UNITY_END(); +} 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); +} From 688bbb2301eb5c42597097698cbfd7b300264291 Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Sun, 17 May 2026 19:29:11 -0300 Subject: [PATCH 5/9] Actualizada documentacion y tests a la version 3.0.0 con vectores llp-spec 3.1.0 --- .gitignore | 1 + CHANGELOG.md | 30 +- README.md | 306 ++++- STRUCTURE.md | 34 +- TESTING.md | 104 +- docs/PROTOCOL.md | 195 +-- examples/minimal_uart/minimal_uart.ino | 4 +- .../request_response/request_response.ino | 2 +- include/llp_protocol.h | 3 +- library.properties | 6 +- test/cross_test_generate.c | 236 ---- test/test_main.c | 176 ++- test/test_spec_common.h | 124 ++ test/test_spec_vectors.c | 1046 +++++++++++++++++ 14 files changed, 1767 insertions(+), 500 deletions(-) delete mode 100644 test/cross_test_generate.c create mode 100644 test/test_spec_common.h create mode 100644 test/test_spec_vectors.c diff --git a/.gitignore b/.gitignore index 56783b7..f051e98 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ __pycache__/ # Temporal *.tmp test_output.txt +BUGS.md 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 310082a..58cb215 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,325 @@ # LLP Protocol — Lightweight Link Protocol +**Header-only C library** for embedded communication. Ultra-lightweight (~500B), robust (CRC16-CCITT, byte stuffing, timeouts), and extensible (layer chain with metadata). + [![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) -[![Arduino Compatible](https://img.shields.io/badge/Arduino-Compatible-brightgreen)](https://www.arduino.cc/) [![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) + +--- -Protocolo de comunicación **liviano, robusto y extensible** para microcontroladores. -Ideal para UART, RF (433MHz, LoRa), RS485, CAN y otros medios con ruido. +## Table of Contents -**Características:** -- Ultra-liviano: ~500B de código, sin dependencias -- Robusto: CRC16-CCITT, byte stuffing, sincronización anti-ruido, timeouts -- Extensible: layer chain con metadata (passthrough y transform layers) -- Agnóstico del medio: UART, RF, RS485, Bluetooth, CAN, etc. -- Header-only library: incluir `llp_protocol.h` y listo +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) + +--- + +## Overview + +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. + +**Use cases:** UART, RF (433MHz, LoRa), RS485, CAN, Bluetooth, any byte-oriented medium. + +**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 + +### Conformance + +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. --- ## Wire Format ``` -[0xAA][0x55][LEN_L][LEN_H][PAYLOAD...][CRC_L][CRC_H] ++--------+--------+--------+--------+------------------+--------+--------+ +| MAGIC1 | MAGIC2 | LEN_L | LEN_H | PAYLOAD (stuffed)| CRC_L | CRC_H | +| 0xAA | 0x55 | [N] | [N] | layer chain | [N] | [N] | ++--------+--------+--------+--------+------------------+--------+--------+ ``` -Byte stuffing: `0xAA` en LEN/PAYLOAD/CRC se escribe como `0xAA 0x00`. -Layer chain en PAYLOAD: `[LAYER_ID][META_LEN][METADATA...]...[0x00][RAW DATA]` +| 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 | + +### Byte Stuffing + +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). + +### CRC Coverage + +CRC is computed over **unstuffed** bytes: `MAGIC1 + MAGIC2 + LEN_L + LEN_H + PAYLOAD`. + +### Worst-Case Frame Size + +``` +LLP_MAX_FRAME_SIZE(n) = 2 + 4 + (n * 2) + 4 = 8 + n * 2 +``` + +Where `n` is the layer chain length. With `LLP_MAX_PAYLOAD=128`, worst case is 264 bytes. --- -## Estructura del proyecto +## Layer Chain + +The payload contains an ordered sequence of layer headers followed by raw application data: ``` -llp-protocol/ -├── include/llp_protocol.h # API pública (single-header) -├── src/llp_protocol.c # Verifica compilación standalone -├── test/ # Test suites con Unity (35 tests) -│ ├── test_parser_init.c -│ ├── test_frame_builder.c -│ ├── test_parser_process.c -│ └── test_crc.c -├── examples/ # Sketches Arduino -├── .github/workflows/ # CI con GitHub Actions -├── platformio.ini # Configuración multi-plataforma -└── library.properties # Arduino Library Manager +[LAYER_ID][META_LEN][METADATA...]...[0x00][RAW DATA] ``` +| 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 | + +### Meta Length Encoding + +- **0–254**: 1 byte (direct value) +- **255+**: 3 bytes: `0xFF` + big-endian high/low + --- -## Uso rápido +## API Reference + +### Initialization + +```c +llp_parser_t parser; +llp_parser_init(&parser); +``` + +### Processing + +```c +int result = llp_parser_process_byte(&parser, byte, millis()); + +// Returns: +// 1 → Frame complete, use parser.frame.payload +// 0 → Incomplete (more bytes needed) +// -1 → Error (check parser.error_code) +``` + +### 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); + +// 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); + +// Parse: extract layer chain +int result = llp_parser_process_byte(&parser, byte, millis()); + +// 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) +``` + +### 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 +``` + +### Statistics + +```c +uint32_t frames_ok, frames_error, timeouts; +llp_get_stats(&parser, &frames_ok, &frames_error, &timeouts); +llp_reset_stats(&parser); +``` + +--- + +## Quick Start + +### 1. Include the library ```c #include "llp_protocol.h" +``` + +### 2. Initialize parser +```c llp_parser_t parser; +llp_parser_init(&parser); +``` -void setup() { - Serial.begin(115200); - llp_parser_init(&parser); -} +### 3. Process incoming bytes +```c void loop() { while (Serial.available()) { uint8_t byte = Serial.read(); int result = llp_parser_process_byte(&parser, byte, millis()); + if (result == 1) { - uint8_t data[64]; + // Frame received successfully + uint8_t data[LLP_MAX_PAYLOAD]; int len = llp_get_final_payload(&parser.frame, data, sizeof(data)); if (len > 0) { - // Procesar datos recibidos + // data contains raw application payload } + } else if (result == -1) { + // Error: check parser.error_code } } } ``` +### 4. Send a response (optional) + +```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); + + uint8_t frame[LLP_MAX_FRAME_SIZE(payload_len)]; + size_t frame_len = llp_build_frame(frame, sizeof(frame), + payload, (uint16_t)payload_len); + + Serial.write(frame, frame_len); +} +``` + +--- + +## Examples + +### Minimal UART Echo + +Located at `examples/minimal_uart/minimal_uart.ino`: +- Receives frames via Serial +- Echoes received data back + +### Request-Response with Retries + +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 + --- -## Ejecutar tests +## Testing + +### Run all tests ```bash platformio test -e test -v ``` +**Expected output:** `90 Tests 0 Failures 0 Ignored OK` + +### Test categories + +| 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) | + +### Spec vector categories + +- **Transport**: valid, crc, stuffing, resync, truncation, timeout +- **Layers**: passthrough, transform, malformed, traversal +- **Parser**: incremental, fragmented, recovery + +### Cross-language testing + +```bash +gcc -std=c99 -I include -o /tmp/llp_cross_gen tools/cross_test_generate.c +/tmp/llp_cross_gen /tmp/llp_cross +``` + +--- + +## Project Structure + +``` +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 +``` + +--- + +## Configuration + +Override defaults with build flags: + +```bash +-DLLP_MAX_PAYLOAD=256 # Default: 128 +-DLLP_FRAME_TIMEOUT_MS=3000 # Default: 2000 +``` + --- -## Licencia +## License -MIT — Ver [LICENSE](LICENSE) +MIT — See [LICENSE](LICENSE) \ No newline at end of file diff --git a/STRUCTURE.md b/STRUCTURE.md index 87b5367..7753ea8 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -14,10 +14,16 @@ llp-protocol/ │ └── 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_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) @@ -29,7 +35,7 @@ llp-protocol/ │ └── request_response.ino # Request-response with retries │ ├── docs/ -│ └── PROTOCOL.md # Protocol specification +│ └── PROTOCOL.md # Protocol specification v3.1.0 │ ├── .github/ │ └── workflows/ @@ -41,7 +47,7 @@ llp-protocol/ ├── README.md ├── STRUCTURE.md # This file ├── TESTING.md # Testing guide -└── library.properties # Arduino Library Manager metadata +�└── library.properties # Arduino Library Manager metadata ``` ## File Purposes @@ -51,10 +57,15 @@ llp-protocol/ | `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 @@ -65,23 +76,12 @@ llp-protocol/ | `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 | -| **Total** | **35** | | +| `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 -``` - -## Build Environments - -```bash -platformio run -e arduino_uno # Arduino UNO -platformio run -e arduino_nano # Arduino Nano -platformio run -e arduino_mega # Arduino Mega -platformio run -e esp8266 # ESP8266 -platformio run -e esp32 # ESP32 -platformio run -e stm32f103 # STM32 (Nucleo) -platformio run -e attiny85 # ATtiny85 -``` +``` \ No newline at end of file diff --git a/TESTING.md b/TESTING.md index 6016483..04dfe49 100644 --- a/TESTING.md +++ b/TESTING.md @@ -14,66 +14,56 @@ platformio test -e test -v Expected output: ``` -test_parser_init [PASSED] -test_frame_builder [PASSED] -test_parser_process [PASSED] -test_crc [PASSED] +90 Tests 0 Failures 0 Ignored OK ``` -Total: **35 tests** - ## Test Suites -### test_parser_init.c (5 tests) -| Test | What it verifies | -|------|-----------------| -| `test_init_sets_state_wait_magic1` | State after init is WAIT_MAGIC1 | -| `test_init_clears_all_fields` | All struct fields properly zeroed | -| `test_init_clears_frame_payload_len` | Frame payload length is 0 | -| `test_can_be_reinitialized` | Calling init again resets everything | -| `test_init_resets_stats` | frames_ok/error/timeouts start at 0 | - -### test_frame_builder.c (11 tests) -| Test | What it verifies | -|------|-----------------| -| `test_build_final_payload_empty` | Empty data -> `[0x00]` | -| `test_build_final_payload_with_data` | Data wrapped correctly | -| `test_build_final_payload_small_buffer` | Returns 0 when buffer too small | -| `test_build_frame_magic_bytes` | Frame starts with 0xAA 0x55 | -| `test_build_frame_payload_length` | Length field correct LE | -| `test_build_frame_roundtrip` | Build -> parse -> extract matches | -| `test_build_frame_small_buffer` | Returns 0 for tiny output buffer | -| `test_build_frame_oversized_payload` | Returns 0 when payload > LLP_MAX_PAYLOAD | -| `test_build_frame_stuffing` | 0xAA in payload is stuffed/unstuffed | -| `test_build_frame_deterministic` | Same input -> same output | -| `test_build_frame_empty_payload_chain` | FinalNode-only frame roundtrips | - -### test_parser_process.c (11 tests) -| Test | What it verifies | -|------|-----------------| -| `test_process_incomplete_frame_returns_zero` | Partial frame returns 0 | -| `test_process_complete_frame_returns_one` | Complete frame returns 1 | -| `test_process_extracts_correct_data` | Received data matches sent data | -| `test_process_crc_error_returns_negative_one` | Corrupted CRC returns -1 | -| `test_process_timeout_returns_negative_one` | Timeout returns -1 | -| `test_process_recovers_from_noise` | Random bytes before frame | -| `test_process_multiple_frames` | 3 consecutive frames parsed OK | -| `test_process_increments_error_counter` | OK and error counters tracked | -| `test_process_stuffed_byte` | 0xAA in payload handles correctly | -| `test_process_sync_error_on_aa55_inside_frame` | 0xAA 0x55 mid-frame detected | -| `test_process_payload_len_error` | Oversized payload length rejected | - -### test_crc.c (8 tests) -| Test | What it verifies | -|------|-----------------| -| `test_crc16_known_values` | "123456789" -> 0x29B1 (CCITT standard) | -| `test_crc16_differs_for_different_payloads` | Different data -> different CRC | -| `test_crc16_detects_bit_flip` | Single bit flip changes CRC | -| `test_crc16_detects_burst_error` | Multi-bit errors detected | -| `test_crc16_deterministic` | Same data -> same CRC always | -| `test_crc16_update_chain` | Sequential updates match batch | -| `test_crc16_order_matters` | "AB" != "BA" | -| `test_crc16_empty` | Zero-length input -> 0xFFFF | +### 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] +``` + +## Known Issues + +See [BUGS.md](BUGS.md) for known issues discovered through spec conformance testing. ## Debugging @@ -81,4 +71,4 @@ Total: **35 tests** 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 f96ace6..79d374f 100644 --- a/examples/minimal_uart/minimal_uart.ino +++ b/examples/minimal_uart/minimal_uart.ino @@ -28,8 +28,10 @@ void loop(void) { if (result == 1) { uint8_t data[LLP_MAX_PAYLOAD]; int len = llp_get_final_payload(&parser.frame, data, sizeof(data)); - if (len >= 0) { + if (len > 0) { send_data(data, (uint16_t)len); + } else { + send_data(NULL, 0); } } } diff --git a/examples/request_response/request_response.ino b/examples/request_response/request_response.ino index e21ae80..72cbbc1 100644 --- a/examples/request_response/request_response.ino +++ b/examples/request_response/request_response.ino @@ -53,7 +53,7 @@ void loop(void) { 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 < 2) return; + if (len <= 0) return; uint16_t rx_id = (uint16_t)data[0] | ((uint16_t)data[1] << 8); diff --git a/include/llp_protocol.h b/include/llp_protocol.h index 75e3eec..c537090 100644 --- a/include/llp_protocol.h +++ b/include/llp_protocol.h @@ -189,6 +189,7 @@ static inline int llp_parser_process_byte(llp_parser_t* p, uint8_t byte, 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; } @@ -393,7 +394,7 @@ static inline int llp_get_final_payload(const llp_frame_t* frame, pos += meta_len; } - return 0; + return -1; } // ============================================================================= diff --git a/library.properties b/library.properties index d32d771..b6ed8b6 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ name=LLP Protocol -version=3.0.0 -author=EnzoLeonel +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/test/cross_test_generate.c b/test/cross_test_generate.c deleted file mode 100644 index fa3db1b..0000000 --- a/test/cross_test_generate.c +++ /dev/null @@ -1,236 +0,0 @@ -/** - * cross_test_generate.c — Cross-language test vector generator for LLP v3.0.0 - * - * Builds frames from known test vectors and writes them as binary files. - * Also reads Java-generated frames and parses them with the C parser. - * - * Compile: gcc -std=c99 -I include -o /tmp/llp_cross_gen cross_test_generate.c - * Usage: /tmp/llp_cross_gen [output_dir] - */ - -#include -#include -#include -#include "llp_protocol.h" - -#ifndef CROSS_OUTPUT_DIR -#define CROSS_OUTPUT_DIR "/tmp/llp_cross" -#endif - -typedef struct { - const char* label; - const uint8_t* data; - uint16_t len; -} test_vector_t; - -static int write_binary(const char* path, const uint8_t* data, size_t len) { - FILE* f = fopen(path, "wb"); - if (!f) { perror("fopen"); return -1; } - size_t w = fwrite(data, 1, len, f); - fclose(f); - return (w == len) ? 0 : -1; -} - -static uint8_t* read_binary(const char* path, size_t* out_len) { - FILE* f = fopen(path, "rb"); - if (!f) return NULL; - fseek(f, 0, SEEK_END); - long sz = ftell(f); - fseek(f, 0, SEEK_SET); - if (sz <= 0) { fclose(f); return NULL; } - uint8_t* buf = (uint8_t*)malloc((size_t)sz); - if (!buf) { fclose(f); return NULL; } - *out_len = (size_t)fread(buf, 1, (size_t)sz, f); - fclose(f); - return buf; -} - -static void print_hex(const char* label, const uint8_t* data, size_t len) { - printf(" %s (%zu bytes): ", label, len); - for (size_t i = 0; i < len; i++) printf("%02X ", data[i]); - printf("\n"); -} - -static int self_verify(const uint8_t* frame, size_t frame_len, - const uint8_t* expected_data, uint16_t expected_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[i], 0); - - if (result != 1) { - printf(" FAIL: C parser returned %d (expected 1)\n", result); - return -1; - } - - uint8_t extracted[LLP_MAX_PAYLOAD]; - int extracted_len = llp_get_final_payload(&parser.frame, - extracted, sizeof(extracted)); - if (extracted_len < 0) { - printf(" FAIL: llp_get_final_payload returned %d\n", extracted_len); - return -2; - } - if ((uint16_t)extracted_len != expected_len) { - printf(" FAIL: extracted len %d != expected %u\n", - extracted_len, expected_len); - return -3; - } - if (memcmp(extracted, expected_data, expected_len) != 0) { - printf(" FAIL: extracted data differs\n"); - return -4; - } - return 0; -} - -static int cross_verify(const uint8_t* frame, size_t frame_len, - const test_vector_t* vec) { - 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[i], 0); - - if (result != 1) { - printf(" CROSS-VERIFY [%s]: parser returned %d (expected 1)\n", - vec->label, result); - return -1; - } - - uint8_t extracted[LLP_MAX_PAYLOAD]; - int extracted_len = llp_get_final_payload(&parser.frame, - extracted, sizeof(extracted)); - if (extracted_len < 0) { - printf(" CROSS-VERIFY [%s]: get_final_payload returned %d\n", - vec->label, extracted_len); - return -2; - } - if ((uint16_t)extracted_len != vec->len) { - printf(" CROSS-VERIFY [%s]: len %d != expected %u\n", - vec->label, extracted_len, vec->len); - printf(" Extracted: "); - for (int i = 0; i < extracted_len; i++) printf("%02X ", extracted[i]); - printf("\n"); - return -3; - } - if (memcmp(extracted, vec->data, vec->len) != 0) { - printf(" CROSS-VERIFY [%s]: data differs\n", vec->label); - printf(" Expected: "); - for (uint16_t i = 0; i < vec->len; i++) printf("%02X ", vec->data[i]); - printf("\n Got: "); - for (int i = 0; i < extracted_len; i++) printf("%02X ", extracted[i]); - printf("\n"); - return -4; - } - - printf(" CROSS-VERIFY [%s]: OK (%u bytes)\n", vec->label, vec->len); - return 0; -} - -int main(int argc, char** argv) { - const char* out_dir = (argc > 1) ? argv[1] : CROSS_OUTPUT_DIR; - - char mkdir_cmd[256]; - snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s", out_dir); - system(mkdir_cmd); - - /* Test vectors: raw application data */ - const uint8_t empty_data[] = {}; - const uint8_t single_data[] = {0x42}; - const uint8_t hello_data[] = {'H', 'e', 'l', 'l', 'o'}; - const uint8_t aa_data[] = {0xAA}; - const uint8_t aa55_data[] = {0xAA, 0x55}; - const uint8_t triple_aa_data[] = {0xAA, 0xAA, 0xAA}; - const uint8_t mixed_data[] = {0x01, 0xAA, 0x02, 0xAA, 0x03}; - const uint8_t seq_data[] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, - 0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F}; - const uint8_t zeros_data[32]= {0}; - const uint8_t ff_data[16] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; - - test_vector_t vectors[] = { - {"empty", empty_data, 0}, - {"single", single_data, 1}, - {"hello", hello_data, 5}, - {"aa_byte", aa_data, 1}, - {"aa55", aa55_data, 2}, - {"triple_aa", triple_aa_data, 3}, - {"mixed_aa", mixed_data, 5}, - {"sequence", seq_data, 16}, - {"zeros", zeros_data, 32}, - {"all_ff", ff_data, 16}, - }; - size_t num_vectors = sizeof(vectors) / sizeof(vectors[0]); - int all_ok = 1; - - printf("========================================\n"); - printf("LLP v3.0.0 Cross-Language Test Generator\n"); - printf("========================================\n\n"); - - /* Phase 1: Generate C frames */ - printf("--- Phase 1: Generating C frames ---\n\n"); - for (size_t v = 0; v < num_vectors; v++) { - uint8_t layer_chain[LLP_MAX_PAYLOAD]; - size_t layer_len = llp_build_final_payload( - layer_chain, sizeof(layer_chain), - vectors[v].data, vectors[v].len); - - uint8_t frame[LLP_MAX_FRAME_SIZE(layer_len)]; - size_t frame_len = llp_build_frame(frame, sizeof(frame), - layer_chain, layer_len); - - printf("Vector %zu: %s\n", v + 1, vectors[v].label); - - if (self_verify(frame, frame_len, vectors[v].data, vectors[v].len) == 0) - printf(" SELF-VERIFY: OK\n"); - else { - printf(" SELF-VERIFY: FAILED\n"); - all_ok = 0; - } - - char path[128]; - snprintf(path, sizeof(path), "%s/c_frame_%s.bin", out_dir, vectors[v].label); - write_binary(path, frame, frame_len); - printf(" Wrote: %s (%zu bytes)\n", path, frame_len); - - snprintf(path, sizeof(path), "%s/c_layerchain_%s.bin", out_dir, vectors[v].label); - write_binary(path, layer_chain, layer_len); - printf(" Wrote: %s (%zu bytes)\n\n", path, layer_len); - } - - /* Phase 2: Verify Java frames with C parser */ - printf("--- Phase 2: Verifying Java frames with C parser ---\n\n"); - int found_any = 0; - for (size_t v = 0; v < num_vectors; v++) { - char path[128]; - snprintf(path, sizeof(path), "%s/java_frame_%s.bin", out_dir, vectors[v].label); - - size_t frame_len; - uint8_t* frame = read_binary(path, &frame_len); - if (!frame) { - printf(" [%s] No Java frame found (run Java test first)\n", vectors[v].label); - continue; - } - found_any = 1; - printf(" Java frame [%s]: %zu bytes\n", vectors[v].label, frame_len); - - if (cross_verify(frame, frame_len, &vectors[v]) != 0) - all_ok = 0; - - free(frame); - } - - if (!found_any) - printf(" (No Java frames to verify)\n"); - - printf("\n========================================\n"); - if (all_ok) { - printf("RESULT: ALL TESTS PASSED\n"); - return 0; - } else { - printf("RESULT: SOME TESTS FAILED\n"); - return 1; - } -} diff --git a/test/test_main.c b/test/test_main.c index b751e73..578accc 100644 --- a/test/test_main.c +++ b/test/test_main.c @@ -46,6 +46,93 @@ 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(); @@ -93,5 +180,92 @@ int main(void) 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_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 From d51c9557ef675c7d8e4318b39df5ce2942cf6857 Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Sun, 17 May 2026 19:31:42 -0300 Subject: [PATCH 6/9] Agregado detalle de licencia en README --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 58cb215..1547d86 100644 --- a/README.md +++ b/README.md @@ -322,4 +322,10 @@ Override defaults with build flags: ## License -MIT — See [LICENSE](LICENSE) \ No newline at end of file +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. + +--- \ No newline at end of file From 1a95466e7a8588865ff3f51dbd82d17722b9cf2a Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Sun, 17 May 2026 19:51:42 -0300 Subject: [PATCH 7/9] Actualizados workflow para coverage y publicacion al realizar tag --- .github/workflows/arduino-publish.yml | 67 +++++++++++++++++++++++++++ .github/workflows/platformio.yml | 36 ++++++++++++-- .gitignore | 6 +++ TESTING.md | 22 +++++++++ platformio.ini | 3 ++ scripts/platformio_coverage.py | 4 ++ 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/arduino-publish.yml create mode 100644 scripts/platformio_coverage.py 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 index d00f5e0..4708b71 100644 --- a/.github/workflows/platformio.yml +++ b/.github/workflows/platformio.yml @@ -2,19 +2,47 @@ name: PlatformIO CI on: push: - branches: [main, develop, feature/*] + branches: [main, develop, dev, feature/*] pull_request: - branches: [main, develop] + branches: [main, develop, dev] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: '3.11' + - name: Install PlatformIO run: pip install platformio - - name: Run tests + + - 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 \ + '/usr/*' '*/libdeps/*' '*/Unity/*' '*/unity/*' \ + --output-file coverage/lcov.info \ + --ignore-errors inconsistent + + - 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 f051e98..1668624 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,9 @@ __pycache__/ *.tmp test_output.txt BUGS.md + +# Coverage +coverage/ +*.gcov +*.gcda +*.gcno diff --git a/TESTING.md b/TESTING.md index 04dfe49..5ed4a49 100644 --- a/TESTING.md +++ b/TESTING.md @@ -61,6 +61,28 @@ 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. diff --git a/platformio.ini b/platformio.ini index eed1ee1..2572b93 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,10 +16,13 @@ build_flags = -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 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"]) From a22e8c12ceb08eec45ce9ca698bac662ca7e5168 Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Sun, 17 May 2026 21:21:13 -0300 Subject: [PATCH 8/9] Corregido error en CD/CI de Github --- .github/workflows/platformio.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml index 4708b71..2d4b0a1 100644 --- a/.github/workflows/platformio.yml +++ b/.github/workflows/platformio.yml @@ -2,9 +2,9 @@ name: PlatformIO CI on: push: - branches: [main, develop, dev, feature/*] + branches: [main, dev, feature/*] pull_request: - branches: [main, develop, dev] + branches: [main, dev] jobs: test: @@ -35,9 +35,9 @@ jobs: --ignore-errors inconsistent # Exclude Unity framework and system headers lcov --remove coverage/lcov.info \ - '/usr/*' '*/libdeps/*' '*/Unity/*' '*/unity/*' \ + '*/libdeps/*' '*/Unity/*' '*/unity/*' \ --output-file coverage/lcov.info \ - --ignore-errors inconsistent + --ignore-errors inconsistent,unused - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 From 2735778d8e20e7b4426abdd8e169843b2764f30a Mon Sep 17 00:00:00 2001 From: Enzo Sanchez Date: Sun, 17 May 2026 21:37:42 -0300 Subject: [PATCH 9/9] Agregado codecov badge a README de repositorio --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1547d86..7d15292 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![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) ---