From d2baec930066d08a88c2e0ac868e7347da5045c7 Mon Sep 17 00:00:00 2001 From: Nehal Patel Date: Mon, 19 Jan 2026 14:46:47 -0800 Subject: [PATCH 1/2] Condense CLAUDE.md and README.md for clarity Reduce CLAUDE.md from 820 to 92 lines, focusing on essential guidance for AI assistants: build commands, architecture pattern, constraints, and code patterns. Remove information discoverable from source files. Reduce README.md from 277 to 117 lines, keeping quick start options, architecture overview, and essential commands while removing redundant sections. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 830 ++++-------------------------------------------------- README.md | 268 ++++-------------- 2 files changed, 104 insertions(+), 994 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ca1ed64..cd3aea6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,140 +1,54 @@ -# CLAUDE.md - AI Assistant Guide for Embedded C++ BSP +# CLAUDE.md -This document provides comprehensive guidance for AI assistants working with this embedded C++ Board Support Package (BSP) repository. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Overview +## Build Commands -**Purpose**: This is a demonstrative/educational embedded systems project exploring modern C++ (C++23) practices and software engineering principles in embedded contexts. - -**Key Goals**: -- Apply modern C++ features to embedded systems -- Build a host-side simulation and testing environment -- Demonstrate correct-by-construction design patterns -- Create clean hardware abstraction layers +```bash +# Full workflow: configure + build + test (recommended) +cmake --workflow --preset=host-debug +cmake --workflow --preset=host-release -**Not Production**: This is a learning/demonstration project, not a production framework. +# Manual steps +cmake --preset=host # Configure +cmake --build --preset=host --config Debug # Build +ctest --preset=host -C Debug # Test all -## Technology Stack +# Run single C++ test +ctest --preset=host -C Debug -R test_zmq_transport -- **Language**: C++23 with strict compiler flags -- **Build System**: CMake 3.27+ with Ninja Multi-Config -- **Compilers**: Clang/LLVM (preferred for host), ARM GCC (for embedded targets) -- **Testing**: Google Test (C++), pytest (Python integration tests) -- **IPC**: ZeroMQ with JSON serialization -- **Embedded Targets**: STM32F3, STM32F7 Nucleo, nRF52832 -- **MCU Architectures**: ARM Cortex-M4, ARM Cortex-M7 +# Run single Python integration test +cd py/host-emulator && pytest tests/test_blinky.py -v -## Repository Structure +# Cross-compile for ARM +cmake --workflow --preset=stm32f3_discovery-release -``` -embedded-cpp/ -├── .devcontainer/ # VS Code DevContainer configuration -│ ├── devcontainer.json # DevContainer settings and extensions -│ └── docker-compose.devcontainer.yml # DevContainer-specific compose overrides -│ -├── .github/ # GitHub configuration -│ └── workflows/ # GitHub Actions CI/CD -│ └── ci.yml # Main CI workflow (build + test on main/PRs) -│ -├── cmake/ # CMake configuration and toolchains -│ └── toolchain/ # Cross-compilation toolchains -│ ├── host-clang.cmake # Host builds (testing/dev) -│ ├── armgcc.cmake # ARM GCC base configuration -│ ├── armgcc-cm4.cmake # Cortex-M4 builds -│ └── armgcc-cm7.cmake # Cortex-M7 builds -│ -├── src/ # Main source code -│ ├── apps/ # Application layer -│ │ ├── blinky/ # Example LED blink application -│ │ └── uart_echo/ # Example UART echo with RxHandler -│ │ -│ └── libs/ # Library abstractions -│ ├── common/ # Common utilities (error handling) -│ │ └── error.hpp # std::expected-based error handling -│ │ -│ ├── mcu/ # MCU abstraction layer (HAL) -│ │ ├── pin.hpp # Pin interface (Input/Output/Bidirectional) -│ │ ├── uart.hpp # UART interface with RxHandler -│ │ ├── i2c.hpp # I2C controller interface -│ │ ├── delay.hpp # Delay/timing interface -│ │ │ -│ │ └── host/ # Host emulator implementations -│ │ ├── host_pin.hpp/cpp # ZMQ-based pin emulation -│ │ ├── host_uart.hpp/cpp # ZMQ-based UART emulation -│ │ ├── zmq_transport.hpp/cpp # ZeroMQ IPC transport -│ │ ├── dispatcher.hpp # Message routing -│ │ └── *_messages.hpp # Message definitions -│ │ -│ └── board/ # Board abstraction layer (BSP) -│ ├── board.hpp # Board interface -│ ├── host/ # Host board implementation -│ ├── stm32f3_discovery/ # STM32F3 Discovery -│ ├── stm32f767zi_nucleo/ # STM32F7 Nucleo -│ └── nrf52832_dk/ # Nordic nRF52 DK -│ -├── py/ # Python components -│ └── host-emulator/ # Python-based hardware emulator -│ ├── src/ # Emulator implementation -│ │ └── emulator.py # Virtual device simulator -│ └── tests/ # Integration tests -│ ├── test_blinky.py -│ └── test_uart_echo.py -│ -├── test/ # System-level tests -├── external/ # External dependencies (placeholder) -│ -├── CMakeLists.txt # Root build configuration -├── CMakePresets.json # Build presets (host, arm-cm4, arm-cm7, etc.) -├── Dockerfile # Docker development environment -├── docker-compose.yml # Docker Compose configuration -├── README.md # Project overview -└── CLAUDE.md # This file +# Docker alternative +docker compose run --rm host-debug ``` ## Architecture -### Layered Design - -The codebase follows a strict layered architecture with dependency inversion: +**Layered design with dependency inversion** - upper layers depend on abstract interfaces, platform selected at CMake time: ``` -┌─────────────────────────────────────┐ -│ Application Layer (apps/) │ ← User applications (blinky, uart_echo) -└─────────────────────────────────────┘ - ↓ depends on -┌─────────────────────────────────────┐ -│ Board Abstraction (libs/board/) │ ← Board interface (LEDs, buttons, UART, I2C) -└─────────────────────────────────────┘ - ↓ depends on -┌─────────────────────────────────────┐ -│ MCU Abstraction (libs/mcu/) │ ← Hardware interfaces (Pin, UART, I2C, Delay) -└─────────────────────────────────────┘ - ↓ depends on -┌─────────────────────────────────────┐ -│ Platform Implementations │ ← Host/STM32/nRF52 specific code -└─────────────────────────────────────┘ +Application (apps/) → Board (libs/board/) → MCU (libs/mcu/) → Platform Implementations ``` -**Key Principle**: Upper layers depend on abstract interfaces, not concrete implementations. Platform selection happens at CMake configuration time via presets. +**Host emulation**: C++ apps communicate with Python hardware emulator via ZeroMQ/JSON IPC. This enables desktop development and integration testing without hardware. -### Core Abstractions +## Key Constraints -#### 1. Error Handling (`libs/common/error.hpp`) +- **No exceptions** - RTTI disabled; use `std::expected` for all fallible operations +- **No raw pointers** - use references or smart pointers +- **No `new`/`delete`** - use RAII and smart pointers +- **clang-tidy enforced** - build fails on violations; naming conventions strictly enforced: + - `PascalCase`: Classes, structs, functions, methods, enum values (prefixed with `k`) + - `snake_case`: Namespaces, variables, private members (with trailing `_`) -```cpp -enum class Error { - kOk, - kUnknown, - kInvalidArgument, - kInvalidState, - kInvalidOperation, - // ... more error codes -}; -``` - -**Pattern**: All operations that can fail return `std::expected` instead of throwing exceptions (no exceptions in embedded). +## Code Patterns -**Usage**: +Error handling: ```cpp auto result = SomeOperation(); if (!result) { @@ -143,679 +57,35 @@ if (!result) { // Use result.value() ``` -#### 2. Pin Abstraction (`libs/mcu/pin.hpp`) - -```cpp -enum class PinDirection { kInput, kOutput }; -enum class PinState { kLow, kHigh, kHighZ }; - -struct InputPin { - virtual auto Get() -> PinState = 0; - virtual auto SetInterruptHandler(InterruptCallback) -> void = 0; -}; - -struct OutputPin { - virtual auto SetHigh() -> void = 0; - virtual auto SetLow() -> void = 0; -}; - -struct BidirectionalPin : InputPin, OutputPin { - virtual auto Configure(PinDirection) -> std::expected = 0; -}; -``` - -#### 3. UART Abstraction (`libs/mcu/uart.hpp`) - -```cpp -struct Uart { - virtual auto Init(const UartConfig&) -> std::expected = 0; - virtual auto Send(const std::vector&) -> std::expected = 0; - virtual auto Receive(std::vector&, size_t) -> std::expected = 0; - - // Event-driven reception for unsolicited data (similar to Pin interrupts) - virtual auto SetRxHandler(std::function) - -> std::expected = 0; -}; -``` - -**Pattern**: UART supports both blocking operations (Send/Receive) and event-driven reception via RxHandler callback. - -**RxHandler Usage**: -```cpp -// Register callback for unsolicited incoming data -board.Uart1().SetRxHandler([](const uint8_t* data, size_t size) { - // Process received data asynchronously - std::vector echo_data(data, data + size); - board.Uart1().Send(echo_data); // Echo back -}); -``` - -**Important**: Applications must explicitly initialize UART when needed - it is NOT initialized by `Board::Init()`. This prevents unnecessary emulator connections in tests that don't use UART. - -#### 4. Board Interface (`libs/board/board.hpp`) - -```cpp -struct Board { - virtual auto Init() -> std::expected = 0; - virtual auto UserLed1() -> mcu::OutputPin& = 0; - virtual auto UserLed2() -> mcu::OutputPin& = 0; - virtual auto UserButton1() -> mcu::InputPin& = 0; - virtual auto I2C1() -> mcu::I2CController& = 0; - virtual auto Uart1() -> mcu::Uart& = 0; -}; -``` - -### Host Emulation Architecture - -The "host" platform is special - it enables desktop development and testing: - -``` -┌──────────────────┐ ZMQ/JSON ┌──────────────────┐ -│ C++ Application │ ←──────────────────────→ │ Python Emulator │ -│ (host build) │ IPC over sockets │ (virtual HW) │ -└──────────────────┘ └──────────────────┘ -``` - -**Components**: -- **Transport Layer** (`zmq_transport.hpp`): ZeroMQ PAIR socket communication -- **Message Protocol** (`host_emulator_messages.hpp`): Request/response messages -- **Dispatcher** (`dispatcher.hpp`): Routes messages to receivers using predicates -- **JSON Encoding** (`emulator_message_json_encoder.hpp`): Serialization -- **Python Emulator** (`py/host-emulator/src/emulator.py`): Virtual hardware simulator - -**Flow**: -1. C++ code calls `pin.SetHigh()` -2. HostPin serializes request to JSON -3. ZMQ transport sends to Python emulator -4. Emulator updates virtual pin state -5. Response sent back to C++ -6. Integration tests verify behavior - -## Development Environment - -### Docker and DevContainers - -The project provides a complete Docker-based development environment with VS Code DevContainer support. - -**Dockerfile** (`Dockerfile`): -- Base image: mcr.microsoft.com/devcontainers/base:ubuntu24.04 -- Pre-installed tools: - - **Build**: cmake, ninja-build, build-essential - - **Clang/LLVM**: clang-18, clang-format, clang-tidy, lld, lldb, libc++-dev - - **Python**: python3, pip, venv - - **ARM Toolchain**: gcc-arm-none-eabi, gdb, gdb-multiarch - - **Libraries**: libzmq3-dev - - **Optional dev tools** (when `INSTALL_DEV_TOOLS=true`): bat, fzf, htop, nano, ripgrep, tree - -**Docker Compose** (`docker-compose.yml`): -- **embedded-cpp-dev**: Main development service - - Mounts workspace at `/home/vscode/workspace` - - Named volume `build-cache` for faster incremental builds - - Image tag: `embedded-cpp-docker:latest` -- **host-debug**: Runs host-debug workflow preset -- **host-release**: Runs host-release workflow preset - -**DevContainer** (`.devcontainer/`): -- **devcontainer.json**: VS Code configuration - - Pre-configured extensions: C++ tools, CMake, Cortex-Debug, Python, Ruff, GitLens, Error Lens - - CMake settings: source dir, build dir, Ninja generator - - Remote user: `vscode` - - Workspace folder: `/home/vscode/workspace` -- **docker-compose.devcontainer.yml**: DevContainer-specific overrides - - Sets `INSTALL_DEV_TOOLS=true` for additional CLI tools - - Uses separate image tag: `embedded-cpp-docker:devcontainer` - -**Usage**: -```bash -# Open in VS Code DevContainer -code . -# Then: Ctrl+Shift+P → "Dev Containers: Reopen in Container" - -# Or use docker-compose directly -docker compose up embedded-cpp-dev -docker compose run --rm host-debug -docker compose run --rm host-release -``` - -### Continuous Integration - -**GitHub Actions** (`.github/workflows/ci.yml`): -- **Triggers**: Push to main, pull requests to main, manual workflow dispatch -- **Job**: `host-build-test` - - Runs on: ubuntu-latest - - Steps: - 1. Checkout repository - 2. Build Docker image via docker-compose - 3. Run host-debug workflow (configure + build + test) - 4. Upload test results (XML/logs from `build/host/Testing/`) -- All tests must pass before merging to main - -## Build System - -### CMake Presets - -Build configurations are defined in `CMakePresets.json`: - -| Preset | MCU | Board | Toolchain | Use Case | Status | -|--------|-----|-------|-----------|----------|--------| -| `host` | host | host | Clang | Development, testing, debugging | ✅ Fully working | -| `arm-cm4` | arm_cm4 | - | ARM GCC | Base Cortex-M4 builds | ⚙️ Base preset (hidden) | -| `arm-cm7` | arm_cm7 | - | ARM GCC | Base Cortex-M7 builds | ⚙️ Base preset (hidden) | -| `stm32f3_discovery` | arm_cm4 | stm32f3_discovery | ARM GCC | STM32F3 Discovery board | 🚧 Partial (files present, no C++ impl) | - -### Build Commands - -```bash -# Using CMake Workflow Presets (Recommended) -cmake --workflow --preset=host-debug # Configure + build + test (Debug) -cmake --workflow --preset=host-release # Configure + build + test (Release) -cmake --workflow --preset=stm32f3_discovery-release # Configure + build (no tests on hardware) - -# Manual CMake Commands -# Configure for host (development/testing) -cmake --preset=host - -# Build host configuration -cmake --build --preset=host --config Debug - -# Run tests -ctest --preset=host -C Debug - -# Configure for STM32F3 Discovery -cmake --preset=stm32f3_discovery - -# Build for hardware -cmake --build --preset=stm32f3_discovery --config Release - -# Using Docker Compose -docker compose run --rm host-debug # Run host-debug workflow in container -docker compose run --rm host-release # Run host-release workflow in container -``` - -### External Dependencies - -Managed via CMake `FetchContent`: - -| Library | Version | Purpose | When Fetched | -|---------|---------|---------|--------------| -| etl | 20.38.1 | Embedded Template Library (STL alternative) | Always | -| googletest | v1.14.0 | C++ unit testing | Host builds only | -| cppzmq | v4.10.0 | C++ bindings for ZeroMQ | Host builds only | -| nlohmann/json | v3.11.2 | JSON serialization | Host builds only | -| STM32CubeF7 | v1.17.1 | STM32F7 HAL library | arm_cm7 builds only | -| cmake-scripts | main | CMake build utilities | Always | - -**Note**: Dependencies are fetched automatically during CMake configuration based on the selected preset. - -## Code Style and Conventions - -### Formatting (`.clang-format`) - -- **Base Style**: Google style guide -- **Pointer Alignment**: Left (`int* ptr`, not `int *ptr`) -- **Newline at EOF**: Enforced -- **Indentation**: 2 spaces - -### Linting (`.clang-tidy`) - -**Enabled Check Categories**: -- `bugprone-*`: Bug-prone code patterns -- `google-*`: Google style guide -- `misc-*`: Miscellaneous checks -- `modernize-*`: Modern C++ features -- `performance-*`: Performance issues -- `portability-*`: Portability concerns -- `readability-*`: Code readability - -**Warnings as Errors**: All enabled checks produce errors, not warnings. - -**Build Integration**: clang-tidy runs automatically during compilation for all source files (except tests and external dependencies). Build will fail if any violations are detected. No separate step required - just build normally: -```bash -cmake --build --preset=host --config Debug -``` - -### Naming Conventions - -Enforced by `clang-tidy`: - -| Element | Convention | Example | -|---------|-----------|---------| -| Namespaces | `snake_case` | `common`, `mcu`, `board` | -| Classes/Structs | `PascalCase` | `InputPin`, `HostBoard` | -| Functions/Methods | `PascalCase` | `SetHigh()`, `Configure()` | -| Variables | `snake_case` | `direction`, `pin_state` | -| Private Members | `snake_case_` | `transport_`, `state_` | -| Constants | `kPascalCase` | `kOk`, `kInvalidState` | -| Macros | `UPPER_CASE` | `GPIO_PIN_SET` | -| Enum Values | `kPascalCase` | `kInput`, `kOutput` | - -### Include Order - -```cpp -// 1. System/STL headers -#include -#include -#include - -// 2. Third-party libraries -#include "etl/span.h" -#include "zmq.hpp" - -// 3. Project headers (relative to src/) -#include "libs/common/error.hpp" -#include "libs/mcu/pin.hpp" -``` - -### Code Organization - -- **Header-only when possible**: Enables optimization and reduces compilation units -- **Interface separation**: Abstract interfaces (`.hpp`) separate from implementations (`.cpp`) -- **No circular dependencies**: Strict layering prevents cycles -- **Minimal includes**: Forward declarations preferred over includes when possible - -## Testing Infrastructure - -### Unit Tests (C++) - -**Framework**: Google Test - -**Location**: Colocated with implementation (`src/libs/mcu/host/test_*.cpp`) - -**Example**: -```cpp -#include -#include "libs/mcu/host/zmq_transport.hpp" - -TEST(ZmqTransportTest, SendReceive) { - // Test implementation -} -``` - -**Running**: -```bash -ctest --preset=host -C Debug -R test_zmq_transport -``` - -### Integration Tests (Python) - -**Framework**: pytest - -**Location**: `py/host-emulator/tests/` - -**Key Files**: -- `conftest.py`: Fixtures for emulator and blinky executable -- `test_blinky.py`: End-to-end behavior tests - -**Fixtures**: -- `emulator`: Starts Python emulator subprocess -- `blinky`: Starts C++ blinky application subprocess -- Both fixtures manage lifecycle (startup, teardown, cleanup) - -**Running**: -```bash -# From py/host-emulator/ -pytest tests/ --blinky=/path/to/blinky - -# Via CMake/CTest -ctest --preset=host -C Debug -R pytest -``` - -**Test Strategy**: -1. Start Python emulator (listens on ZMQ socket) -2. Start C++ application (connects to emulator) -3. Emulator intercepts pin operations and verifies behavior -4. Test asserts expected state changes (LED blinks, button presses) - -## Development Workflows - -### Adding a New Feature - -1. **Identify Layer**: Determine if feature belongs in MCU, Board, or App layer -2. **Define Interface**: Add abstract interface in appropriate header -3. **Implement for Host**: Create host implementation first for testing -4. **Write Tests**: Add unit tests (C++) and/or integration tests (Python) -5. **Run Linting**: Ensure `clang-tidy` passes -6. **Format Code**: Run `clang-format` on modified files -7. **Build and Test**: Verify all tests pass -8. **Implement Hardware**: Add hardware-specific implementations as needed - -### Adding a New Board - -1. **Create Board Directory**: `src/libs/board//` -2. **Implement Board Interface**: Subclass `Board` from `board.hpp` -3. **Add CMakeLists.txt**: Define board library and dependencies -4. **Create Preset**: Add configuration in `CMakePresets.json` -5. **Update Root CMake**: Ensure preset conditionally includes board directory -6. **Document**: Add board-specific notes to this file - -### Debugging Host Applications - -**Advantages of Host Build**: -- Use familiar debuggers (lldb, gdb) -- Faster iteration (no flashing) -- Integration with system tools -- Python emulator inspection -- Works in DevContainer with VS Code debugging - -**Workflow (DevContainer)**: -```bash -# In VS Code DevContainer terminal -cmake --workflow --preset=host-debug - -# Use VS Code debugger (F5) with launch.json configuration - -# Or run under debugger manually -lldb build/host/bin/blinky -``` - -**Workflow (Local/Docker)**: -```bash -# Build with debug symbols -cmake --build --preset=host --config Debug - -# Run under debugger -lldb build/host/bin/blinky - -# Or run with Python emulator manually -cd py/host-emulator -python -m src.emulator & # Start emulator -../../build/host/bin/blinky # Run application -``` - -### Adding Tests - -**Unit Test**: -```cpp -// In appropriate test file (e.g., test_new_feature.cpp) -#include - -TEST(NewFeatureTest, BasicBehavior) { - // Setup - // Execute - // Verify -} -``` - -**Integration Test**: -```python -# In py/host-emulator/tests/test_new_feature.py -def test_new_feature(emulator, blinky): - # Given - # When - # Then -``` - -**Run Tests**: -```bash -ctest --preset=host -C Debug --output-on-failure -``` - -## Common Patterns - -### Error Propagation - +Dependency injection via abstract interfaces: ```cpp -auto DoSomething() -> std::expected { - auto step1 = Step1(); - if (!step1) { - return std::unexpected(step1.error()); - } - - auto step2 = Step2(step1.value()); - if (!step2) { - return std::unexpected(step2.error()); - } - - return Result{step2.value()}; -} -``` - -### Pin Interrupt Handling - -```cpp -auto SetupButton() -> void { - button_.SetInterruptHandler([this](auto transition) { - if (transition == mcu::PinStateTransition::kRisingEdge) { - led_.SetHigh(); - } - }); -} -``` - -### Dependency Injection - -```cpp -class Blinky { +class MyApp { public: - Blinky(mcu::OutputPin& led, mcu::InputPin& button) - : led_(led), button_(button) {} - + MyApp(mcu::OutputPin& led) : led_(led) {} private: mcu::OutputPin& led_; - mcu::InputPin& button_; }; ``` -## Compiler Flags +## Adding New Features -### Common Flags (All Builds) -- `-std=c++23`: C++23 standard -- `-Wall -Wextra -Werror -Wpedantic`: All warnings as errors -- `-Os`: Optimize for size -- `-g`: Debug symbols -- `-fno-rtti`: No runtime type information - -### Clang-Specific -- `-stdlib=libc++`: Use LLVM's standard library -- `-Wno-c++98-compat`: Ignore C++98 compatibility warnings -- `-Wno-exit-time-destructors`, `-Wno-global-constructors`: Embedded-appropriate warnings suppressed - -### ARM-Specific -- `-mthumb`: Thumb instruction set -- `-mcpu=cortex-m4` or `-mcpu=cortex-m7`: CPU architecture -- `-mfloat-abi=hard -mfpu=fpv5-d16`: FPU support (CM7) - -## Key Files Reference - -### Configuration -- `CMakeLists.txt` - Root build configuration -- `CMakePresets.json` - Build presets and toolchain selection -- `src/.clang-format` - Code formatting rules -- `src/.clang-tidy` - Static analysis configuration - -### Core Abstractions -- `src/libs/common/error.hpp` - Error handling -- `src/libs/mcu/pin.hpp` - Pin abstraction -- `src/libs/mcu/uart.hpp` - UART abstraction with RxHandler -- `src/libs/mcu/i2c.hpp` - I2C abstraction -- `src/libs/board/board.hpp` - Board interface - -### Host Implementation -- `src/libs/mcu/host/host_pin.hpp` - Host pin with ZMQ -- `src/libs/mcu/host/host_uart.hpp` - Host UART with ZMQ -- `src/libs/mcu/host/zmq_transport.hpp` - ZeroMQ transport -- `src/libs/mcu/host/dispatcher.hpp` - Message routing -- `src/libs/board/host/host_board.hpp` - Host board - -### Example Applications -- `src/apps/blinky/blinky.hpp` - LED blink app with button interrupt -- `src/apps/uart_echo/uart_echo.hpp` - UART echo app demonstrating RxHandler - -### Testing -- `py/host-emulator/src/emulator.py` - Hardware emulator -- `py/host-emulator/tests/test_blinky.py` - Blinky integration tests -- `py/host-emulator/tests/test_uart_echo.py` - UART echo integration tests -- `py/host-emulator/tests/conftest.py` - Pytest fixtures - -## Implementation Status - -| Component | Status | Notes | -|-----------|--------|-------| -| Host emulator | ✅ Fully working | ZMQ-based with Python emulator | -| Blinky app | ✅ Fully working | LED blink + button interrupt | -| UART echo app | ✅ Fully working | UART RxHandler demonstration | -| C++ unit tests | ✅ Fully working | Transport, messages, dispatcher, UART | -| Python integration tests | ✅ Fully working | Blinky + UART echo behavior tests | -| Docker environment | ✅ Fully working | Complete dev environment | -| DevContainer | ✅ Fully working | VS Code integration | -| CI/CD | ✅ Fully working | GitHub Actions pipeline | -| STM32F3 Discovery | 🚧 Partial | Hardware files present, no C++ board impl | -| STM32F7 Nucleo | 🚧 Partial | Hardware files present, no C++ board impl | -| nRF52832 DK | ⚠️ Placeholder | Minimal setup only | - -## Best Practices for AI Assistants - -### DO -- ✅ Use `std::expected` for error handling -- ✅ Follow naming conventions strictly (clang-tidy enforces) -- ✅ Write unit tests for new MCU/board implementations -- ✅ Test on host platform first before hardware -- ✅ Use header-only implementations when possible -- ✅ Depend on abstract interfaces, not concrete types -- ✅ Use `auto` for return types to enable refactoring -- ✅ Add integration tests for new application features -- ✅ Document non-obvious design decisions in comments -- ✅ Run clang-format and clang-tidy before committing -- ✅ Use DevContainer for consistent development environment -- ✅ Ensure CI passes before merging to main - -### DON'T -- ❌ Use exceptions (RTTI disabled, embedded context) -- ❌ Use raw pointers (use references or smart pointers) -- ❌ Create circular dependencies between layers -- ❌ Add dependencies without justification -- ❌ Skip tests (unit tests required for new features) -- ❌ Ignore clang-tidy warnings (they're errors) -- ❌ Use magic numbers (define named constants) -- ❌ Mix platform-specific code with abstractions -- ❌ Use `new`/`delete` directly (RAII, smart pointers) -- ❌ Add global mutable state - -### When Making Changes - -1. **Understand the Layer**: Determine if change affects MCU, Board, or App layer -2. **Check Existing Patterns**: Look for similar implementations -3. **Host First**: Implement and test on host platform -4. **Run Full Build**: Test both host and at least one ARM preset -5. **Verify Tests**: Ensure all tests pass (`ctest --preset=host` or `cmake --workflow --preset=host-debug`) -6. **Check Linting**: Run clang-tidy on modified files -7. **Format**: Apply clang-format before committing -8. **CI Check**: Verify GitHub Actions CI passes on your branch - -### Common Tasks - -**Add a new pin type**: -1. Define interface in `libs/mcu/pin.hpp` -2. Implement for host in `libs/mcu/host/host_pin.hpp` +1. Define interface in `libs/mcu/*.hpp` (for peripherals) or `libs/board/board.hpp` +2. Implement host version in `libs/mcu/host/` with ZMQ messaging 3. Add message types to `host_emulator_messages.hpp` -4. Update Python emulator in `py/host-emulator/src/emulator.py` -5. Write tests - -**Add a new peripheral (e.g., SPI)**: -1. Define interface in `libs/mcu/spi.hpp` -2. Implement host version in `libs/mcu/host/host_spi.hpp` -3. Add to board interface in `libs/board/board.hpp` -4. Implement in hardware boards -5. Add example app usage - -**Debug integration test failure**: -1. Run emulator manually: `python -m py.host-emulator.src.emulator` -2. Run blinky manually: `build/host/bin/blinky` -3. Check ZMQ messages in emulator output -4. Add print statements to emulator callbacks -5. Verify JSON message format matches expectations - -## Software Engineering Principles - -This project demonstrates several key principles: - -1. **Strong Types**: Enums and type aliases prevent errors -2. **Type Safety**: Explicit casting, no implicit conversions -3. **Compile-Time Checks**: `constexpr`, templates, concepts -4. **Correct by Construction**: APIs designed to prevent misuse -5. **Separate Calculation from Doing**: Pure functions separate from side effects -6. **Dependency Inversion**: High-level code depends on abstractions -7. **Single Responsibility**: Each class/function has one clear purpose -8. **Interface Segregation**: Small, focused interfaces -9. **Don't Repeat Yourself**: Common functionality abstracted - -## Troubleshooting - -### Build Issues - -**Problem**: CMake can't find toolchain -``` -Solution: Set CMAKE_TOOLCHAIN_PATH environment variable -export CMAKE_TOOLCHAIN_PATH=/path/to/toolchain -``` - -**Problem**: clang-tidy errors on correct code -``` -Solution: Check .clang-tidy file, verify naming conventions -``` - -**Problem**: Linker errors with std::expected -``` -Solution: Ensure C++23 support, check compiler version -``` - -### Test Issues - -**Problem**: Emulator not starting -``` -Solution: Check Python dependencies, run: pip install -r py/host-emulator/requirements.txt -``` - -**Problem**: Blinky executable not found in pytest -``` -Solution: Pass --blinky flag: pytest --blinky=build/host/bin/blinky -``` - -**Problem**: ZMQ socket bind error -``` -Solution: Kill existing processes on port, or wait for socket cleanup -``` - -### Runtime Issues - -**Problem**: Pin operations timing out -``` -Solution: Ensure emulator is running and responsive, check ZMQ connection -``` - -**Problem**: Interrupt handler not called -``` -Solution: Verify emulator sends response messages, check dispatcher routing -``` - -## Resources - -### Documentation -- Project README: `README.md` -- Test README: `test/README.md` -- Emulator README: `py/host-emulator/README.md` - -### External References -- C++23 std::expected: https://en.cppreference.com/w/cpp/utility/expected -- ZeroMQ Guide: https://zguide.zeromq.org/ -- CMake Presets: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html -- Embedded Template Library: https://www.etlcpp.com/ - -### Related Videos -- Correct by Construction: https://youtu.be/nLSm3Haxz0I -- Separate Calculating from Doing: https://youtu.be/b4p_tcLYDV0 +4. Update Python emulator in `py/host-emulator/src/` +5. Write unit tests (C++) and integration tests (Python) +6. Implement hardware versions in board-specific directories -## Version History +## Testing -- **0.0.1** (Current): Initial BSP implementation with host emulation -- Multi-board support (STM32F3, STM32F7, nRF52) -- Python-based integration testing -- ZMQ transport layer +- **C++ unit tests**: Colocated with code (`src/libs/mcu/host/test_*.cpp`), use Google Test +- **Python integration tests**: `py/host-emulator/tests/`, use pytest with fixtures that manage emulator/app lifecycle +- **clang-tidy**: Runs automatically during build, no separate step needed ---- +## Important Files -**Last Updated**: 2025-11-22 -**CMake Version**: 3.27+ -**C++ Standard**: C++23 -**Docker Base**: Ubuntu 24.04 (DevContainer) -**Primary Maintainer**: Project repository owner +- `src/libs/common/error.hpp` - Error enum and `std::expected` usage +- `src/libs/mcu/pin.hpp` - Pin abstraction (InputPin, OutputPin, BidirectionalPin) +- `src/libs/mcu/uart.hpp` - UART with RxHandler callback pattern +- `src/libs/board/board.hpp` - Board interface aggregating all peripherals +- `CMakePresets.json` - Build configurations for host and ARM targets diff --git a/README.md b/README.md index 36b8ecf..92642d7 100644 --- a/README.md +++ b/README.md @@ -5,272 +5,112 @@ A modern C++23 embedded systems project demonstrating best practices for hardwar ## Overview This project explores: -1. **Modern C++ in Embedded Systems** - Applying C++23 features and software engineering principles to microcontroller development -2. **Host-Side Simulation** - Desktop development and testing environment with Python-based hardware emulation -3. **Correct-by-Construction Design** - Type-safe abstractions and compile-time verification +- **Modern C++ in Embedded Systems** - C++23 features and software engineering principles for microcontrollers +- **Host-Side Simulation** - Desktop development with Python-based hardware emulation via ZeroMQ +- **Correct-by-Construction Design** - Type-safe abstractions and compile-time verification **Status**: Educational/demonstrative project (not production-ready) -## Key Features - -- ✅ **Multi-Platform Support**: Host (x86_64), ARM Cortex-M4, ARM Cortex-M7 -- ✅ **Hardware Abstraction Layers**: Clean separation between MCU, Board, and Application layers -- ✅ **Host Emulation**: ZeroMQ-based IPC with Python hardware simulator -- ✅ **Comprehensive Testing**: C++ unit tests (Google Test) + Python integration tests (pytest) -- ✅ **DevContainer Support**: Full VS Code DevContainer configuration -- ✅ **CI/CD Pipeline**: GitHub Actions for automated builds and tests -- 🚧 **Multi-Board**: STM32F3 Discovery, STM32F7 Nucleo, nRF52832 DK (partial implementation) - ## Quick Start -### Option 1: VS Code DevContainer (Recommended) +### VS Code DevContainer (Recommended) 1. Install [VS Code](https://code.visualstudio.com/) and the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 2. Open this repository in VS Code -3. Press `Ctrl+Shift+P` and select "Dev Containers: Reopen in Container" -4. Wait for the container to build (first time only) -5. Open a terminal and run: - ```bash - cmake --workflow --preset=host-debug - ``` - -That's it! The DevContainer includes all tools pre-installed. +3. Press `Ctrl+Shift+P` → "Dev Containers: Reopen in Container" +4. Run: `cmake --workflow --preset=host-debug` -### Option 2: Docker Compose +### Docker Compose ```bash -# Clone the repository -git clone -cd embedded-cpp - -# Run host-debug workflow (build + test) docker compose run --rm host-debug - -# Or run host-release workflow -docker compose run --rm host-release ``` -### Option 3: Local Build +### Local Build -**Requirements**: -- CMake 3.27+ -- Ninja -- Clang 18+ (host builds) or ARM GCC (embedded builds) -- Python 3.10+ (for integration tests) -- ZeroMQ (libzmq3-dev) +**Requirements**: CMake 3.27+, Ninja, Clang 18+, Python 3.10+, ZeroMQ (libzmq3-dev) ```bash -# Configure for host platform -cmake --preset=host - -# Build (Debug) -cmake --build --preset=host --config Debug - -# Run tests -ctest --preset=host -C Debug --output-on-failure - -# Or use workflow preset (configure + build + test) -cmake --workflow --preset=host-debug +cmake --workflow --preset=host-debug # Configure + build + test ``` -## Project Structure +## Architecture ``` -embedded-cpp/ -├── src/ -│ ├── apps/ -│ │ ├── blinky/ # Example LED blink application -│ │ └── uart_echo/ # Example UART echo with RxHandler -│ ├── libs/ -│ │ ├── common/ # Error handling (std::expected) -│ │ ├── mcu/ # MCU abstraction (Pin, UART, I2C, Delay) -│ │ │ └── host/ # Host emulation with ZeroMQ -│ │ └── board/ # Board abstraction (LEDs, buttons, UART, peripherals) -│ │ ├── host/ # Host board implementation -│ │ ├── stm32f3_discovery/ -│ │ ├── stm32f767zi_nucleo/ -│ │ └── nrf52832_dk/ -│ -├── py/host-emulator/ # Python hardware emulator -│ ├── src/emulator.py # Virtual device simulator -│ └── tests/ # Integration tests (pytest) -│ -├── cmake/toolchain/ # Cross-compilation toolchains -├── .devcontainer/ # VS Code DevContainer config -├── .github/workflows/ # GitHub Actions CI/CD -└── CLAUDE.md # Comprehensive project documentation +Application (apps/) → Board (libs/board/) → MCU (libs/mcu/) → Platform Implementations ``` -## Technology Stack +- **apps/**: Example applications (blinky, uart_echo) +- **libs/mcu/**: Hardware abstractions (Pin, UART, I2C, Delay) with host emulation +- **libs/board/**: Board-specific implementations (host, STM32F3, STM32F7, nRF52) +- **py/host-emulator/**: Python hardware simulator for desktop testing -| Category | Technology | -|----------|------------| -| **Language** | C++23 | -| **Build System** | CMake 3.27+ with Ninja Multi-Config | -| **Compilers** | Clang 18 (host), ARM GCC (embedded) | -| **Testing** | Google Test (C++), pytest (Python) | -| **IPC** | ZeroMQ with JSON serialization | -| **Embedded Targets** | STM32F3, STM32F7, nRF52832 | -| **Architectures** | ARM Cortex-M4, ARM Cortex-M7 | -| **Development** | Docker, VS Code DevContainers | -| **CI/CD** | GitHub Actions | - -## Building for Different Platforms +## Build Commands ```bash -# Host platform (development/testing) +# Host (development/testing) cmake --workflow --preset=host-debug cmake --workflow --preset=host-release -# STM32F3 Discovery (ARM Cortex-M4) +# ARM targets cmake --workflow --preset=stm32f3_discovery-release - -# ARM Cortex-M7 (base preset) -cmake --workflow --preset=arm-cm7-release ``` ## Running Tests -### C++ Unit Tests - ```bash -# Run all tests +# All tests ctest --preset=host -C Debug --output-on-failure -# Run specific test +# Single C++ test ctest --preset=host -C Debug -R test_zmq_transport -``` - -### Python Integration Tests -```bash -cd py/host-emulator - -# Install dependencies -pip install -r requirements.txt - -# Run tests (requires built executables) -pytest tests/ --blinky=../../build/host/bin/blinky --uart-echo=../../build/host/bin/uart_echo +# Python integration tests +cd py/host-emulator && pytest tests/ -v ``` -## Example Applications - -### Blinky - -The `blinky` application demonstrates: -- LED blinking at 500ms intervals -- Button interrupt handling (rising edge detection) -- Dependency injection with board abstraction -- Error handling with `std::expected` +## Example: Running Blinky -**Run on host emulator**: ```bash -# Terminal 1: Start Python emulator -cd py/host-emulator -python -m src.emulator +# Terminal 1: Start emulator +cd py/host-emulator && python -m src.emulator -# Terminal 2: Run blinky -cd build/host/bin -./blinky +# Terminal 2: Run application +./build/host/bin/Debug/blinky ``` -The emulator will print pin state changes as the application runs. - -### UART Echo - -The `uart_echo` application demonstrates: -- UART initialization and configuration -- Event-driven reception with RxHandler callback (similar to Pin interrupts) -- Asynchronous data echoing -- LED toggling on data received -- Greeting message on startup - -**Run on host emulator**: -```bash -# Terminal 1: Run uart_echo -cd build/host/bin -./uart_echo - -# Terminal 2: Send data via Python -python ->>> from src.emulator import DeviceEmulator ->>> emu = DeviceEmulator() ->>> emu.start() ->>> emu.uart1().send_data([72, 101, 108, 108, 111]) # "Hello" ->>> bytes(emu.uart1().rx_buffer) # See echoed data ->>> emu.uart1().rx_buffer.clear() -``` - -## Software Engineering Principles - -This project demonstrates: - -1. **Strong Types** - Enums and type aliases prevent errors (PinState, PinDirection, Error) -2. **Type Safety** - Explicit casting, no implicit conversions -3. **Compile-Time Checks** - `constexpr`, templates, concepts -4. **Correct by Construction** - APIs designed to prevent misuse -5. **Separate Calculation from Doing** - Pure functions separate from side effects -6. **Dependency Inversion** - High-level code depends on abstractions -7. **Single Responsibility** - Each class/function has one clear purpose -8. **Interface Segregation** - Small, focused interfaces (InputPin, OutputPin, BidirectionalPin) -9. **Error Handling** - `std::expected` instead of exceptions (RTTI disabled) +## Technology Stack -See [Correct-by-Construction](https://youtu.be/nLSm3Haxz0I) and [Separate Calculating from Doing](https://youtu.be/b4p_tcLYDV0) for more details. +| Category | Technology | +|----------|------------| +| Language | C++23 | +| Build | CMake 3.27+ / Ninja | +| Compilers | Clang 18 (host), ARM GCC (embedded) | +| Testing | Google Test, pytest | +| IPC | ZeroMQ + JSON | +| Targets | STM32F3, STM32F7, nRF52832 | ## Code Quality -- **Formatting**: `.clang-format` (Google style, pointer-left alignment) -- **Linting**: `.clang-tidy` (comprehensive checks with naming convention enforcement) -- **Warnings**: All warnings are errors (`-Werror`) -- **Standard**: C++23 with strict compliance -- **RTTI**: Disabled (no exceptions, embedded-friendly) - -## Development Workflow - -1. **Work in DevContainer** (or use Docker) -2. **Implement on Host First** - Faster iteration, easier debugging -3. **Write Tests** - Unit tests (C++) and integration tests (Python) -4. **Run Linting** - `clang-tidy` enforces conventions -5. **Format Code** - `clang-format` on modified files -6. **Verify CI** - Ensure GitHub Actions passes -7. **Port to Hardware** - Test on actual embedded boards - -## Documentation - -- **[CLAUDE.md](CLAUDE.md)** - Comprehensive project documentation (architecture, conventions, workflows) -- **[test/README.md](test/README.md)** - Testing infrastructure details -- **[py/host-emulator/README.md](py/host-emulator/README.md)** - Python emulator documentation - -## Contributing - -This is an educational project. If you'd like to contribute or experiment: - -1. Use the DevContainer for a consistent environment -2. Follow the coding conventions (enforced by clang-tidy) -3. Write tests for new features -4. Ensure CI passes before submitting PRs -5. See [CLAUDE.md](CLAUDE.md) for detailed guidelines +- **No exceptions** - Uses `std::expected` (RTTI disabled) +- **clang-tidy** - Enforced during build with strict naming conventions +- **clang-format** - Google style with left pointer alignment +- **-Werror** - All warnings are errors ## Implementation Status | Component | Status | |-----------|--------| -| Host emulation | ✅ Fully working | -| Blinky app | ✅ Fully working | -| UART echo app | ✅ Fully working | -| C++ unit tests | ✅ Fully working | -| Python integration tests | ✅ Fully working | -| Docker/DevContainer | ✅ Fully working | -| CI/CD | ✅ Fully working | -| STM32F3 Discovery | 🚧 Partial (hardware files present) | -| STM32F7 Nucleo | 🚧 Partial (hardware files present) | -| nRF52832 DK | ⚠️ Placeholder only | - -## License - -See LICENSE file for details. - -## Acknowledgments - -- Built with [CMake](https://cmake.org/), [Embedded Template Library](https://www.etlcpp.com/), [ZeroMQ](https://zeromq.org/) -- Inspired by modern C++ embedded practices and correct-by-construction design +| Host emulation | ✅ Working | +| Example apps | ✅ Working | +| C++ unit tests | ✅ Working | +| Python integration tests | ✅ Working | +| Docker/DevContainer | ✅ Working | +| CI/CD | ✅ Working | +| STM32F3/F7 | 🚧 Partial | +| nRF52832 | ⚠️ Placeholder | + +## Resources + +- [Correct-by-Construction](https://youtu.be/nLSm3Haxz0I) +- [Separate Calculating from Doing](https://youtu.be/b4p_tcLYDV0) From bfe909c0cb701c016cdd5083c1b81568f3aa4836 Mon Sep 17 00:00:00 2001 From: Nehal Patel Date: Mon, 19 Jan 2026 15:08:32 -0800 Subject: [PATCH 2/2] Fix critical safety issues and modernize C++ codebase Critical fixes: - Wrap JSON parsing in try-catch to handle exceptions safely (RTTI disabled) - Change I2C ReceiveData to use caller-provided buffer instead of returning span to internal storage (prevents dangling references) - Fix ZmqTransport race condition with proper condition variable sync instead of sleep_for hack Modernization: - Add [[nodiscard]] to all error-returning interface methods - Replace manual operator== with defaulted spaceship operator<=> - Use std::println instead of iostream in logger - Use std::as_bytes instead of reinterpret_cast in uart_echo Co-Authored-By: Claude Opus 4.5 --- src/apps/i2c_demo/i2c_demo.cpp | 15 ++- src/apps/uart_echo/uart_echo.cpp | 6 +- src/libs/board/board.hpp | 12 +-- src/libs/common/logger.cpp | 10 +- .../host/emulator_message_json_encoder.hpp | 12 ++- src/libs/mcu/host/host_emulator_messages.hpp | 36 ++----- src/libs/mcu/host/host_i2c.cpp | 50 +++++----- src/libs/mcu/host/host_i2c.hpp | 19 ++-- src/libs/mcu/host/host_pin.cpp | 32 ++++--- src/libs/mcu/host/host_uart.cpp | 25 +++-- src/libs/mcu/host/test_host_i2c.cpp | 95 +++++++++++-------- src/libs/mcu/host/test_host_uart.cpp | 6 +- src/libs/mcu/host/test_messages.cpp | 16 +++- src/libs/mcu/host/zmq_transport.cpp | 17 +++- src/libs/mcu/host/zmq_transport.hpp | 5 +- src/libs/mcu/i2c.hpp | 32 ++++--- src/libs/mcu/pin.hpp | 16 ++-- src/libs/mcu/uart.hpp | 19 ++-- 18 files changed, 234 insertions(+), 189 deletions(-) diff --git a/src/apps/i2c_demo/i2c_demo.cpp b/src/apps/i2c_demo/i2c_demo.cpp index 99d7bcb..a14a2f3 100644 --- a/src/apps/i2c_demo/i2c_demo.cpp +++ b/src/apps/i2c_demo/i2c_demo.cpp @@ -39,6 +39,9 @@ auto I2CDemo::Run() -> std::expected { const std::array test_pattern{std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xEF}}; + // Buffer for receiving data (caller-provided, no heap allocation) + std::array receive_buffer{}; + // Main loop - write pattern, read it back, verify while (true) { // Write test pattern to I2C device @@ -53,9 +56,8 @@ auto I2CDemo::Run() -> std::expected { // Small delay between write and read mcu::Delay(50ms); - // Read data back from I2C device - auto read_result{ - board_.I2C1().ReceiveData(kDeviceAddress, test_pattern.size())}; + // Read data back from I2C device into our buffer + auto read_result{board_.I2C1().ReceiveData(kDeviceAddress, receive_buffer)}; if (!read_result) { // Turn off LED1 on read error std::ignore = board_.UserLed1().SetLow(); @@ -64,8 +66,11 @@ auto I2CDemo::Run() -> std::expected { } // Verify received data matches test pattern - const auto received_span{read_result.value()}; - const bool data_matches{std::ranges::equal(received_span, test_pattern)}; + const size_t bytes_received{read_result.value()}; + const bool data_matches{ + bytes_received == test_pattern.size() && + std::ranges::equal(std::span{receive_buffer.data(), bytes_received}, + test_pattern)}; // Toggle LED1 based on verification result if (data_matches) { diff --git a/src/apps/uart_echo/uart_echo.cpp b/src/apps/uart_echo/uart_echo.cpp index 08ce845..49173f1 100644 --- a/src/apps/uart_echo/uart_echo.cpp +++ b/src/apps/uart_echo/uart_echo.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "apps/app.hpp" #include "libs/board/board.hpp" @@ -47,10 +48,7 @@ auto UartEcho::Init() -> std::expected { auto UartEcho::Run() -> std::expected { // Send initial greeting message const std::string greeting{"UART Echo ready! Send data to echo it back.\n"}; - const auto* greeting_bytes{ - reinterpret_cast(greeting.data())}; - auto send_result{board_.Uart1().Send( - std::span{greeting_bytes, greeting.size()})}; + auto send_result{board_.Uart1().Send(std::as_bytes(std::span{greeting}))}; if (!send_result) { return std::unexpected(send_result.error()); } diff --git a/src/libs/board/board.hpp b/src/libs/board/board.hpp index b3f25c4..71eac0d 100644 --- a/src/libs/board/board.hpp +++ b/src/libs/board/board.hpp @@ -13,11 +13,11 @@ namespace board { struct Board { virtual ~Board() = default; - virtual auto Init() -> std::expected = 0; - virtual auto UserLed1() -> mcu::OutputPin& = 0; - virtual auto UserLed2() -> mcu::OutputPin& = 0; - virtual auto UserButton1() -> mcu::InputPin& = 0; - virtual auto I2C1() -> mcu::I2CController& = 0; - virtual auto Uart1() -> mcu::Uart& = 0; + [[nodiscard]] virtual auto Init() -> std::expected = 0; + [[nodiscard]] virtual auto UserLed1() -> mcu::OutputPin& = 0; + [[nodiscard]] virtual auto UserLed2() -> mcu::OutputPin& = 0; + [[nodiscard]] virtual auto UserButton1() -> mcu::InputPin& = 0; + [[nodiscard]] virtual auto I2C1() -> mcu::I2CController& = 0; + [[nodiscard]] virtual auto Uart1() -> mcu::Uart& = 0; }; } // namespace board diff --git a/src/libs/common/logger.cpp b/src/libs/common/logger.cpp index 3127eac..bd1a233 100644 --- a/src/libs/common/logger.cpp +++ b/src/libs/common/logger.cpp @@ -1,23 +1,23 @@ #include "logger.hpp" -#include +#include namespace common { auto ConsoleLogger::Debug(std::string_view msg) -> void { - std::cout << "[DEBUG] " << msg << '\n'; + std::println("[DEBUG] {}", msg); } auto ConsoleLogger::Info(std::string_view msg) -> void { - std::cout << "[INFO] " << msg << '\n'; + std::println("[INFO] {}", msg); } auto ConsoleLogger::Warning(std::string_view msg) -> void { - std::cerr << "[WARN] " << msg << '\n'; + std::println(stderr, "[WARN] {}", msg); } auto ConsoleLogger::Error(std::string_view msg) -> void { - std::cerr << "[ERROR] " << msg << '\n'; + std::println(stderr, "[ERROR] {}", msg); } } // namespace common diff --git a/src/libs/mcu/host/emulator_message_json_encoder.hpp b/src/libs/mcu/host/emulator_message_json_encoder.hpp index 3f1645b..80bd1a6 100644 --- a/src/libs/mcu/host/emulator_message_json_encoder.hpp +++ b/src/libs/mcu/host/emulator_message_json_encoder.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -96,8 +97,13 @@ inline auto Encode(const T& obj) -> std::string { }; template -inline auto Decode(const std::string_view& str) -> T { - return nlohmann::json::parse(str).get(); -}; +inline auto Decode(const std::string_view& str) + -> std::expected { + try { + return nlohmann::json::parse(str).template get(); + } catch (const nlohmann::json::exception&) { + return std::unexpected(common::Error::kInvalidArgument); + } +} } // namespace mcu diff --git a/src/libs/mcu/host/host_emulator_messages.hpp b/src/libs/mcu/host/host_emulator_messages.hpp index bb66177..854f4a8 100644 --- a/src/libs/mcu/host/host_emulator_messages.hpp +++ b/src/libs/mcu/host/host_emulator_messages.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -21,10 +22,7 @@ struct PinEmulatorRequest { std::string name; OperationType operation; PinState state; - auto operator==(const PinEmulatorRequest& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - operation == other.operation && state == other.state; - } + auto operator<=>(const PinEmulatorRequest&) const = default; }; struct PinEmulatorResponse { @@ -33,10 +31,7 @@ struct PinEmulatorResponse { std::string name; PinState state; common::Error status; - auto operator==(const PinEmulatorResponse& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - state == other.state && status == other.status; - } + auto operator<=>(const PinEmulatorResponse&) const = default; }; struct UartEmulatorRequest { @@ -47,11 +42,7 @@ struct UartEmulatorRequest { std::vector data; // For Send operation size_t size{0}; // For Receive operation (buffer size) uint32_t timeout_ms{0}; // For Receive operation - auto operator==(const UartEmulatorRequest& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - operation == other.operation && data == other.data && - size == other.size && timeout_ms == other.timeout_ms; - } + auto operator<=>(const UartEmulatorRequest&) const = default; }; struct UartEmulatorResponse { @@ -61,11 +52,7 @@ struct UartEmulatorResponse { std::vector data; // Received data size_t bytes_transferred{0}; common::Error status; - auto operator==(const UartEmulatorResponse& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - data == other.data && bytes_transferred == other.bytes_transferred && - status == other.status; - } + auto operator<=>(const UartEmulatorResponse&) const = default; }; struct I2CEmulatorRequest { @@ -76,11 +63,7 @@ struct I2CEmulatorRequest { uint16_t address{0}; std::vector data; // For Send operation size_t size{0}; // For Receive operation (buffer size) - auto operator==(const I2CEmulatorRequest& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - operation == other.operation && address == other.address && - data == other.data && size == other.size; - } + auto operator<=>(const I2CEmulatorRequest&) const = default; }; struct I2CEmulatorResponse { @@ -91,12 +74,7 @@ struct I2CEmulatorResponse { std::vector data; // Received data size_t bytes_transferred{0}; common::Error status; - auto operator==(const I2CEmulatorResponse& other) const -> bool { - return type == other.type && object == other.object && name == other.name && - address == other.address && data == other.data && - bytes_transferred == other.bytes_transferred && - status == other.status; - } + auto operator<=>(const I2CEmulatorResponse&) const = default; }; } // namespace mcu diff --git a/src/libs/mcu/host/host_i2c.cpp b/src/libs/mcu/host/host_i2c.cpp index e080733..a08f1d4 100644 --- a/src/libs/mcu/host/host_i2c.cpp +++ b/src/libs/mcu/host/host_i2c.cpp @@ -37,16 +37,20 @@ auto HostI2CController::SendData(uint16_t address, return std::unexpected(receive_result.error()); } - const auto response = Decode(receive_result.value()); - if (response.status != common::Error::kOk) { - return std::unexpected(response.status); + auto response = Decode(receive_result.value()); + if (!response) { + return std::unexpected(response.error()); + } + if (response->status != common::Error::kOk) { + return std::unexpected(response->status); } return {}; } -auto HostI2CController::ReceiveData(uint16_t address, size_t size) - -> std::expected, common::Error> { +auto HostI2CController::ReceiveData(uint16_t address, + std::span buffer) + -> std::expected { const I2CEmulatorRequest request{ .type = MessageType::kRequest, .object = ObjectType::kI2C, @@ -54,7 +58,7 @@ auto HostI2CController::ReceiveData(uint16_t address, size_t size) .operation = OperationType::kReceive, .address = address, .data = {}, - .size = size, + .size = buffer.size(), }; auto send_result = transport_.Send(Encode(request)); @@ -67,17 +71,19 @@ auto HostI2CController::ReceiveData(uint16_t address, size_t size) return std::unexpected(receive_result.error()); } - const auto response = Decode(receive_result.value()); - if (response.status != common::Error::kOk) { - return std::unexpected(response.status); + auto response = Decode(receive_result.value()); + if (!response) { + return std::unexpected(response.error()); + } + if (response->status != common::Error::kOk) { + return std::unexpected(response->status); } - // Store received data in buffer for this address - auto& buffer = data_buffers_[address]; - const size_t bytes_to_copy{std::min(response.data.size(), buffer.size())}; - std::copy_n(response.data.begin(), bytes_to_copy, buffer.begin()); + // Copy received data into caller-provided buffer + const size_t bytes_to_copy{std::min(response->data.size(), buffer.size())}; + std::copy_n(response->data.begin(), bytes_to_copy, buffer.begin()); - return std::span{buffer.data(), bytes_to_copy}; + return bytes_to_copy; } auto HostI2CController::SendDataInterrupt( @@ -89,10 +95,10 @@ auto HostI2CController::SendDataInterrupt( } auto HostI2CController::ReceiveDataInterrupt( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected { - callback(ReceiveData(address, size)); + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected { + callback(ReceiveData(address, buffer)); return {}; } @@ -105,10 +111,10 @@ auto HostI2CController::SendDataDma( } auto HostI2CController::ReceiveDataDma( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected { - callback(ReceiveData(address, size)); + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected { + callback(ReceiveData(address, buffer)); return {}; } diff --git a/src/libs/mcu/host/host_i2c.hpp b/src/libs/mcu/host/host_i2c.hpp index abdc2fa..03c61c6 100644 --- a/src/libs/mcu/host/host_i2c.hpp +++ b/src/libs/mcu/host/host_i2c.hpp @@ -1,11 +1,9 @@ #pragma once -#include #include #include #include #include -#include #include "libs/mcu/host/receiver.hpp" #include "libs/mcu/host/transport.hpp" @@ -26,31 +24,30 @@ class HostI2CController final : public I2CController, public Receiver { auto SendData(uint16_t address, std::span data) -> std::expected override; - auto ReceiveData(uint16_t address, size_t size) - -> std::expected, common::Error> override; + auto ReceiveData(uint16_t address, std::span buffer) + -> std::expected override; auto SendDataInterrupt( uint16_t address, std::span data, std::function)> callback) -> std::expected override; auto ReceiveDataInterrupt( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected override; + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected override; auto SendDataDma(uint16_t address, std::span data, std::function)> callback) -> std::expected override; auto ReceiveDataDma( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected override; + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected override; auto Receive(const std::string_view& message) -> std::expected override; private: const std::string name_; Transport& transport_; - std::unordered_map> data_buffers_; }; } // namespace mcu diff --git a/src/libs/mcu/host/host_pin.cpp b/src/libs/mcu/host/host_pin.cpp index 8f596d4..dc6f51a 100644 --- a/src/libs/mcu/host/host_pin.cpp +++ b/src/libs/mcu/host/host_pin.cpp @@ -64,7 +64,7 @@ auto HostPin::SendState(PinState state) -> std::expected { return transport_.Send(Encode(req)) .and_then([this]() { return transport_.Receive(); }) - .transform([](const std::string& rx_bytes) { + .and_then([](const std::string& rx_bytes) { return Decode(rx_bytes); }) .and_then([this, state](const PinEmulatorResponse& resp) @@ -100,14 +100,18 @@ auto HostPin::GetState() -> std::expected { return transport_.Send(Encode(req)) .and_then([this]() { return transport_.Receive(); }) - .transform([this](const std::string& rx_bytes) { - const auto resp = Decode(rx_bytes); + .and_then([this](const std::string& rx_bytes) + -> std::expected { + auto resp = Decode(rx_bytes); + if (!resp) { + return std::unexpected(resp.error()); + } // If the MCU is polling the input, then it should NOT be configured // for interrupts. Therefore, we should not invoke the handler. // const PinState prev_state{state_}; - state_ = resp.state; + state_ = resp->state; // CheckAndInvokeHandler(prev_state, resp.state); - return resp.state; + return resp->state; }); } @@ -115,11 +119,14 @@ auto HostPin::GetState() -> std::expected { // requests. HostPin will only send responses. auto HostPin::Receive(const std::string_view& message) -> std::expected { - const auto json_pin = json::parse(message); - if (json_pin["name"] != name_) { + auto req = Decode(message); + if (!req) { + return std::unexpected(common::Error::kInvalidArgument); + } + if (req->name != name_) { return std::unexpected(common::Error::kInvalidArgument); } - if (json_pin["type"] == MessageType::kResponse) { + if (req->type == MessageType::kResponse) { return std::unexpected(common::Error::kInvalidOperation); } PinEmulatorResponse resp = { @@ -129,14 +136,13 @@ auto HostPin::Receive(const std::string_view& message) .state = state_, .status = common::Error::kInvalidOperation, }; - const auto req = Decode(message); - if (req.operation == OperationType::kGet) { + if (req->operation == OperationType::kGet) { resp.status = common::Error::kOk; return Encode(resp); } // Set from the external world is only allowed if the pin is an input // with respect to the MCU - if (req.operation == OperationType::kSet) { + if (req->operation == OperationType::kSet) { if (direction_ == PinDirection::kOutput) { resp.status = common::Error::kInvalidOperation; return Encode(resp); @@ -144,8 +150,8 @@ auto HostPin::Receive(const std::string_view& message) // The external entity pushed a pin update to the MCU. // Therefore check for interrupt. const PinState prev_state{state_}; - state_ = req.state; - CheckAndInvokeHandler(prev_state, req.state); + state_ = req->state; + CheckAndInvokeHandler(prev_state, req->state); resp.state = state_; resp.status = common::Error::kOk; return Encode(resp); diff --git a/src/libs/mcu/host/host_uart.cpp b/src/libs/mcu/host/host_uart.cpp index 09af567..5dbd31a 100644 --- a/src/libs/mcu/host/host_uart.cpp +++ b/src/libs/mcu/host/host_uart.cpp @@ -51,9 +51,12 @@ auto HostUart::Send(std::span data) .and_then([this]() { return transport_.Receive(); }) .and_then([](const std::string& response_str) -> std::expected { - const auto response = Decode(response_str); - if (response.status != common::Error::kOk) { - return std::unexpected(response.status); + auto response = Decode(response_str); + if (!response) { + return std::unexpected(response.error()); + } + if (response->status != common::Error::kOk) { + return std::unexpected(response->status); } return {}; }); @@ -81,7 +84,7 @@ auto HostUart::Receive(std::span buffer, uint32_t timeout_ms) return transport_.Send(Encode(request)) .and_then([this]() { return transport_.Receive(); }) - .transform([](const std::string& response_str) { + .and_then([](const std::string& response_str) { return Decode(response_str); }) .and_then([buffer](const UartEmulatorResponse& response) @@ -203,12 +206,12 @@ auto HostUart::SetRxHandler(std::function auto HostUart::Receive(const std::string_view& message) -> std::expected { - // First, check if this is a request (unsolicited data) or response - const auto json_msg = nlohmann::json::parse(message); + // First, try to decode as a request (unsolicited data) + auto request_result = Decode(message); // Handle unsolicited incoming data from emulator (Request type) - if (json_msg["type"] == MessageType::kRequest) { - const auto request = Decode(message); + if (request_result && request_result->type == MessageType::kRequest) { + const auto& request = *request_result; // Verify this message is for us if (request.name != name_) { @@ -239,7 +242,11 @@ auto HostUart::Receive(const std::string_view& message) } // Handle async operation responses - const auto response = Decode(message); + auto response_result = Decode(message); + if (!response_result) { + return std::unexpected(common::Error::kInvalidArgument); + } + const auto& response = *response_result; // Verify this message is for us if (response.name != name_) { diff --git a/src/libs/mcu/host/test_host_i2c.cpp b/src/libs/mcu/host/test_host_i2c.cpp index 0e4fee3..1ce3f35 100644 --- a/src/libs/mcu/host/test_host_i2c.cpp +++ b/src/libs/mcu/host/test_host_i2c.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -98,8 +99,12 @@ class HostI2CTest : public ::testing::Test { const std::string_view message_str{ static_cast(message.data()), message.size()}; - const auto request = + auto request_result = mcu::Decode(std::string{message_str}); + if (!request_result) { + continue; // Skip malformed messages + } + const auto& request = *request_result; mcu::I2CEmulatorResponse response{ .type = mcu::MessageType::kResponse, .object = mcu::ObjectType::kI2C, @@ -165,16 +170,19 @@ TEST_F(HostI2CTest, SendReceiveData) { auto send_result = i2c_->SendData(device_address, send_data); ASSERT_TRUE(send_result); - // Receive data back from same device - auto recv_result = i2c_->ReceiveData(device_address, send_data.size()); + // Receive data back from same device (caller-provided buffer) + std::array recv_buffer{}; + auto recv_result = i2c_->ReceiveData(device_address, recv_buffer); ASSERT_TRUE(recv_result); - const auto received_span = recv_result.value(); - EXPECT_EQ(received_span.size(), send_data.size()); + const size_t bytes_received = recv_result.value(); + EXPECT_EQ(bytes_received, send_data.size()); // Compare received data with sent data - EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), - send_data.begin(), send_data.end())); + EXPECT_TRUE(std::equal( + recv_buffer.begin(), + recv_buffer.begin() + static_cast(bytes_received), + send_data.begin(), send_data.end())); } TEST_F(HostI2CTest, MultipleAddresses) { @@ -194,19 +202,19 @@ TEST_F(HostI2CTest, MultipleAddresses) { ASSERT_TRUE(send2_result); // Receive from first address - auto recv1_result = i2c_->ReceiveData(address1, data1.size()); + std::array recv1_buffer{}; + auto recv1_result = i2c_->ReceiveData(address1, recv1_buffer); ASSERT_TRUE(recv1_result); - const auto received1_span = recv1_result.value(); - EXPECT_EQ(received1_span.size(), data1.size()); - EXPECT_TRUE(std::equal(received1_span.begin(), received1_span.end(), + EXPECT_EQ(recv1_result.value(), data1.size()); + EXPECT_TRUE(std::equal(recv1_buffer.begin(), recv1_buffer.end(), data1.begin(), data1.end())); // Receive from second address - auto recv2_result = i2c_->ReceiveData(address2, data2.size()); + std::array recv2_buffer{}; + auto recv2_result = i2c_->ReceiveData(address2, recv2_buffer); ASSERT_TRUE(recv2_result); - const auto received2_span = recv2_result.value(); - EXPECT_EQ(received2_span.size(), data2.size()); - EXPECT_TRUE(std::equal(received2_span.begin(), received2_span.end(), + EXPECT_EQ(recv2_result.value(), data2.size()); + EXPECT_TRUE(std::equal(recv2_buffer.begin(), recv2_buffer.end(), data2.begin(), data2.end())); } @@ -214,12 +222,12 @@ TEST_F(HostI2CTest, ReceiveWithoutSend) { const uint16_t device_address{0x60}; // Try to receive from device that has no data - auto result = i2c_->ReceiveData(device_address, 10); + std::array recv_buffer{}; + auto result = i2c_->ReceiveData(device_address, recv_buffer); ASSERT_TRUE(result); - // Should return empty span - const auto received_span = result.value(); - EXPECT_EQ(received_span.size(), 0); + // Should return 0 bytes received + EXPECT_EQ(result.value(), 0); } TEST_F(HostI2CTest, ReceivePartialData) { @@ -232,15 +240,16 @@ TEST_F(HostI2CTest, ReceivePartialData) { auto send_result = i2c_->SendData(device_address, send_data); ASSERT_TRUE(send_result); - // Request only 5 bytes - auto recv_result = i2c_->ReceiveData(device_address, 5); + // Request only 5 bytes (buffer size limits the receive) + std::array recv_buffer{}; + auto recv_result = i2c_->ReceiveData(device_address, recv_buffer); ASSERT_TRUE(recv_result); - const auto received_span = recv_result.value(); - EXPECT_EQ(received_span.size(), 5); + const size_t bytes_received = recv_result.value(); + EXPECT_EQ(bytes_received, 5); // Should receive first 5 bytes - EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + EXPECT_TRUE(std::equal(recv_buffer.begin(), recv_buffer.end(), send_data.begin(), send_data.begin() + 5)); } @@ -275,13 +284,14 @@ TEST_F(HostI2CTest, ReceiveDataInterrupt) { ASSERT_TRUE(send_result); bool callback_called{false}; - std::expected, common::Error> callback_result{ + std::expected callback_result{ std::unexpected(common::Error::kUnknown)}; + std::array recv_buffer{}; auto result = i2c_->ReceiveDataInterrupt( - device_address, send_data.size(), - [&callback_called, &callback_result]( - std::expected, common::Error> result) { + device_address, recv_buffer, + [&callback_called, + &callback_result](std::expected result) { callback_called = true; callback_result = result; }); @@ -290,9 +300,9 @@ TEST_F(HostI2CTest, ReceiveDataInterrupt) { EXPECT_TRUE(callback_called); ASSERT_TRUE(callback_result); - const auto received_span = callback_result.value(); - EXPECT_EQ(received_span.size(), send_data.size()); - EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + const size_t bytes_received = callback_result.value(); + EXPECT_EQ(bytes_received, send_data.size()); + EXPECT_TRUE(std::equal(recv_buffer.begin(), recv_buffer.end(), send_data.begin(), send_data.end())); } @@ -328,23 +338,24 @@ TEST_F(HostI2CTest, ReceiveDataDma) { ASSERT_TRUE(send_result); bool callback_called{false}; - std::expected, common::Error> callback_result{ + std::expected callback_result{ std::unexpected(common::Error::kUnknown)}; + std::array recv_buffer{}; - auto result = i2c_->ReceiveDataDma( - device_address, send_data.size(), - [&callback_called, &callback_result]( - std::expected, common::Error> result) { - callback_called = true; - callback_result = result; - }); + auto result = + i2c_->ReceiveDataDma(device_address, recv_buffer, + [&callback_called, &callback_result]( + std::expected result) { + callback_called = true; + callback_result = result; + }); EXPECT_TRUE(result); EXPECT_TRUE(callback_called); ASSERT_TRUE(callback_result); - const auto received_span = callback_result.value(); - EXPECT_EQ(received_span.size(), send_data.size()); - EXPECT_TRUE(std::equal(received_span.begin(), received_span.end(), + const size_t bytes_received = callback_result.value(); + EXPECT_EQ(bytes_received, send_data.size()); + EXPECT_TRUE(std::equal(recv_buffer.begin(), recv_buffer.end(), send_data.begin(), send_data.end())); } diff --git a/src/libs/mcu/host/test_host_uart.cpp b/src/libs/mcu/host/test_host_uart.cpp index 412d0de..8de589e 100644 --- a/src/libs/mcu/host/test_host_uart.cpp +++ b/src/libs/mcu/host/test_host_uart.cpp @@ -97,8 +97,12 @@ class HostUartTest : public ::testing::Test { const std::string_view message_str{ static_cast(message.data()), message.size()}; - const auto request = + auto request_result = mcu::Decode(std::string{message_str}); + if (!request_result) { + continue; // Skip malformed messages + } + const auto& request = *request_result; mcu::UartEmulatorResponse response{ .type = mcu::MessageType::kResponse, .object = mcu::ObjectType::kUart, diff --git a/src/libs/mcu/host/test_messages.cpp b/src/libs/mcu/host/test_messages.cpp index 53954c9..e27b522 100644 --- a/src/libs/mcu/host/test_messages.cpp +++ b/src/libs/mcu/host/test_messages.cpp @@ -25,7 +25,9 @@ TEST(EmulatorMessageJsonEncoderTest, DecodePinEmulatorRequest) { .name = "PA0", .operation = OperationType::kSet, .state = PinState::kHigh}; - EXPECT_EQ(Decode(json), expected_request); + auto result = Decode(json); + ASSERT_TRUE(result); + EXPECT_EQ(*result, expected_request); } TEST(EmulatorMessageJsonEncoderTest, EncodeDecodePinEmulatorRequest) { @@ -35,8 +37,16 @@ TEST(EmulatorMessageJsonEncoderTest, EncodeDecodePinEmulatorRequest) { .operation = OperationType::kSet, .state = PinState::kHigh}; const auto json{Encode(request)}; - const auto decoded_request{Decode(json)}; - EXPECT_EQ(decoded_request, request); + auto decoded_request{Decode(json)}; + ASSERT_TRUE(decoded_request); + EXPECT_EQ(*decoded_request, request); +} + +TEST(EmulatorMessageJsonEncoderTest, DecodeInvalidJson) { + const std::string invalid_json{"not valid json"}; + auto result = Decode(invalid_json); + EXPECT_FALSE(result); + EXPECT_EQ(result.error(), common::Error::kInvalidArgument); } } // namespace diff --git a/src/libs/mcu/host/zmq_transport.cpp b/src/libs/mcu/host/zmq_transport.cpp index 4701d52..5fc7bb3 100644 --- a/src/libs/mcu/host/zmq_transport.cpp +++ b/src/libs/mcu/host/zmq_transport.cpp @@ -53,11 +53,11 @@ ZmqTransport::ZmqTransport(const std::string& to_emulator, // NOLINT server_thread_ = std::thread{&ZmqTransport::ServerThread, this, from_emulator}; - // Small sleep to let server thread bind (ZMQ binding is fast, ~1-5ms typical) - // This is a pragmatic approach - alternatives would require condition - // variables or synchronization primitives which add complexity for minimal - // benefit - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + // Wait for server thread to complete bind before connecting + { + std::unique_lock lock(bind_mutex_); + bind_cv_.wait(lock, [this]() { return server_bound_.load(); }); + } // Now CONNECT to emulator (emulator should already be bound) LogDebug("Connecting to emulator"); @@ -194,6 +194,13 @@ void ZmqTransport::ServerThread(const std::string& endpoint) { socket.bind(endpoint); + // Signal that bind is complete + { + const std::lock_guard lock(bind_mutex_); + server_bound_ = true; + } + bind_cv_.notify_one(); + LogDebug("ServerThread bound and listening"); while (running_) { diff --git a/src/libs/mcu/host/zmq_transport.hpp b/src/libs/mcu/host/zmq_transport.hpp index c0612a0..f52a3e4 100644 --- a/src/libs/mcu/host/zmq_transport.hpp +++ b/src/libs/mcu/host/zmq_transport.hpp @@ -104,8 +104,9 @@ class ZmqTransport : public Transport { zmq::context_t from_emulator_context_{1}; std::atomic running_{true}; - std::condition_variable shutdown_cv_; - std::mutex shutdown_mutex_; + std::atomic server_bound_{false}; + std::condition_variable bind_cv_; + std::mutex bind_mutex_; Dispatcher& dispatcher_; std::thread server_thread_; diff --git a/src/libs/mcu/i2c.hpp b/src/libs/mcu/i2c.hpp index 54dc1bb..018307d 100644 --- a/src/libs/mcu/i2c.hpp +++ b/src/libs/mcu/i2c.hpp @@ -14,29 +14,35 @@ class I2CController { public: virtual ~I2CController() = default; - virtual auto SendData(uint16_t address, std::span data) + [[nodiscard]] virtual auto SendData(uint16_t address, + std::span data) -> std::expected = 0; - virtual auto ReceiveData(uint16_t address, size_t size) - -> std::expected, common::Error> = 0; + /// @brief Receive data from I2C device into caller-provided buffer + /// @param address I2C device address + /// @param buffer Caller-provided buffer to store received data + /// @return Number of bytes actually received, or error + [[nodiscard]] virtual auto ReceiveData(uint16_t address, + std::span buffer) + -> std::expected = 0; - virtual auto SendDataInterrupt( + [[nodiscard]] virtual auto SendDataInterrupt( uint16_t address, std::span data, std::function)> callback) -> std::expected = 0; - virtual auto ReceiveDataInterrupt( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected = 0; + [[nodiscard]] virtual auto ReceiveDataInterrupt( + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected = 0; - virtual auto SendDataDma( + [[nodiscard]] virtual auto SendDataDma( uint16_t address, std::span data, std::function)> callback) -> std::expected = 0; - virtual auto ReceiveDataDma( - uint16_t address, size_t size, - std::function, common::Error>)> - callback) -> std::expected = 0; + [[nodiscard]] virtual auto ReceiveDataDma( + uint16_t address, std::span buffer, + std::function)> callback) + -> std::expected = 0; }; } // namespace mcu diff --git a/src/libs/mcu/pin.hpp b/src/libs/mcu/pin.hpp index 9e2a70e..466a089 100644 --- a/src/libs/mcu/pin.hpp +++ b/src/libs/mcu/pin.hpp @@ -14,9 +14,10 @@ enum class PinTransition { kRising = 1, kFalling, kBoth }; class InputPin { public: virtual ~InputPin() = default; - virtual auto Get() -> std::expected = 0; - virtual auto SetInterruptHandler(std::function handler, - PinTransition transition) + [[nodiscard]] virtual auto Get() + -> std::expected = 0; + [[nodiscard]] virtual auto SetInterruptHandler(std::function handler, + PinTransition transition) -> std::expected = 0; }; @@ -24,16 +25,17 @@ class OutputPin : public virtual InputPin { public: virtual ~OutputPin() = default; - virtual auto SetHigh() -> std::expected = 0; - virtual auto SetLow() -> std::expected = 0; - virtual auto Toggle() -> std::expected = 0; + [[nodiscard]] virtual auto SetHigh() + -> std::expected = 0; + [[nodiscard]] virtual auto SetLow() -> std::expected = 0; + [[nodiscard]] virtual auto Toggle() -> std::expected = 0; }; class BidirectionalPin : public virtual InputPin, public virtual OutputPin { public: virtual ~BidirectionalPin() = default; - virtual auto Configure(PinDirection direction) + [[nodiscard]] virtual auto Configure(PinDirection direction) -> std::expected = 0; }; } // namespace mcu diff --git a/src/libs/mcu/uart.hpp b/src/libs/mcu/uart.hpp index d6b258b..46b72a9 100644 --- a/src/libs/mcu/uart.hpp +++ b/src/libs/mcu/uart.hpp @@ -47,20 +47,21 @@ class Uart { /// @brief Initialize UART with configuration /// @param config UART configuration parameters /// @return Success or error code - virtual auto Init(const UartConfig& config) + [[nodiscard]] virtual auto Init(const UartConfig& config) -> std::expected = 0; /// @brief Send data (blocking) /// @param data Span of bytes to send /// @return Success or error code - virtual auto Send(std::span data) + [[nodiscard]] virtual auto Send(std::span data) -> std::expected = 0; /// @brief Receive data (blocking with timeout) /// @param buffer Buffer to store received data /// @param timeout_ms Timeout in milliseconds (0 = wait forever) /// @return Number of bytes received or error - virtual auto Receive(std::span buffer, uint32_t timeout_ms = 0) + [[nodiscard]] virtual auto Receive(std::span buffer, + uint32_t timeout_ms = 0) -> std::expected = 0; /// @brief Send data asynchronously @@ -68,7 +69,7 @@ class Uart { /// @param data Span of bytes to send /// @param callback Called when transfer completes /// @return Success or error code - virtual auto SendAsync( + [[nodiscard]] virtual auto SendAsync( std::span data, std::function)> callback) -> std::expected = 0; @@ -78,29 +79,29 @@ class Uart { /// @param buffer Buffer to store received data /// @param callback Called when data is received (with number of bytes) /// @return Success or error code - virtual auto ReceiveAsync( + [[nodiscard]] virtual auto ReceiveAsync( std::span buffer, std::function)> callback) -> std::expected = 0; /// @brief Check if UART is busy transmitting /// @return True if busy, false otherwise - virtual auto IsBusy() const -> bool = 0; + [[nodiscard]] virtual auto IsBusy() const -> bool = 0; /// @brief Get number of bytes available to read /// @return Number of bytes in receive buffer - virtual auto Available() const -> size_t = 0; + [[nodiscard]] virtual auto Available() const -> size_t = 0; /// @brief Flush transmit buffer (wait for all data to be sent) /// @return Success or error code - virtual auto Flush() -> std::expected = 0; + [[nodiscard]] virtual auto Flush() -> std::expected = 0; /// @brief Set handler for unsolicited incoming data /// Similar to pin interrupts, this allows the UART to notify the /// application when data arrives asynchronously (e.g., from external source) /// @param handler Callback invoked when data arrives (data pointer and size) /// @return Success or error code - virtual auto SetRxHandler( + [[nodiscard]] virtual auto SetRxHandler( std::function handler) -> std::expected = 0; };