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) 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; };