Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
78fbc3d
fix(build): repair the clang-tidy header filter and fix what it found
nehalkpatel Aug 29, 2026
49c9e59
chore: untrack workspace clutter and regenerate compile_commands link
nehalkpatel Aug 29, 2026
e6740b6
fix(toolchain): stop host-clang.cmake overriding user-chosen compilers
nehalkpatel Aug 29, 2026
e9adc93
chore: delete dead directories and leftover fragments
nehalkpatel Aug 29, 2026
0bd022f
docs: state the Python version the project actually requires
nehalkpatel Aug 29, 2026
4bf45c2
refactor(build): park unbuilt hardware scaffolding behind clear guards
nehalkpatel Aug 29, 2026
e124d6d
refactor(build): move to target-based usage requirements
nehalkpatel Aug 29, 2026
e17a96b
refactor(mcu/host): one Transact helper for the emulator round trip
nehalkpatel Aug 29, 2026
368a720
refactor(mcu): trim Uart and I2C to the surface the host can honor
nehalkpatel Aug 29, 2026
bbb294e
refactor(apps): one RunApp helper; honest errors at the top layer
nehalkpatel Aug 29, 2026
ee8d0fa
refactor(mcu/host): dispatch by receiver claim, not predicate
nehalkpatel Aug 29, 2026
390ac61
refactor(transport): extract endpoint arbitration; present-tense comm…
nehalkpatel Aug 29, 2026
492f40d
refactor(tests): shared infra for host-peripheral and transport tests
nehalkpatel Aug 29, 2026
89b838c
refactor(emulator): one Peripheral class; wire vocabulary as StrEnums
nehalkpatel Aug 29, 2026
f9a832c
refactor(pytests): usefixtures, shared helpers, and honest tooling co…
nehalkpatel Aug 29, 2026
b08242f
build(ci): one compose service, one LLVM pin, no root-then-chown
nehalkpatel Aug 29, 2026
0f1f962
docs: canonical wire-protocol doc; forward-looking project plan
nehalkpatel Aug 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .devcontainer/docker-compose.devcontainer.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Same image tag as docker-compose.yml on purpose: the devcontainer build (with
# dev tools) then serves `docker compose run` too, instead of two tags built
# from one Dockerfile that shadow and rebuild over each other.
services:
embedded-cpp-dev:
image: embedded-cpp-docker:devcontainer
user: "1000:1000"
build:
args:
INSTALL_DEV_TOOLS: "true"

21 changes: 14 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ jobs:
- name: Type-check Python
working-directory: py/host-emulator
run: |
if ! uv run --frozen mypy src; then
echo "::error::Python type errors. Reproduce with: uv run mypy src"
if ! uv run --frozen mypy; then
echo "::error::Python type errors. Reproduce with: uv run mypy"
exit 1
fi

Expand All @@ -79,14 +79,21 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

# Run as the runner's own uid/gid so everything written into the mounted
# checkout is owned by the runner: no root-then-chown repair needed. The
# CI compose overlay mounts the checkout where a non-1000 uid can reach
# it and gives uv a writable HOME (see docker-compose.ci.yml).
- name: Run host-debug workflow
run: docker compose run --rm --user root host-debug
run: >
docker compose -f docker-compose.yml -f docker-compose.ci.yml run --rm
--user "$(id -u):$(id -g)" embedded-cpp-dev
cmake --workflow --preset host-debug

- name: Generate coverage reports
run: |
docker compose run --rm --user root embedded-cpp-dev \
cmake --build build/host --config Debug --target ccov-all
sudo chown -R runner:docker build/host/ccov
run: >
docker compose -f docker-compose.yml -f docker-compose.ci.yml run --rm
--user "$(id -u):$(id -g)" embedded-cpp-dev
cmake --build build/host --config Debug --target ccov-all

- name: Upload test results
if: always()
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ CMakeUserPresets.json
.vscode/
.cache/
.claude/settings.local.json
.DS_Store

# Symlink into the configured build tree, recreated at configure time by the
# root CMakeLists.txt.
/compile_commands.json

# Python
__pycache__/
Expand All @@ -22,4 +27,5 @@ __pycache__/
.ruff_cache/

# Reference material (not source)
*.pdf
/ProfessionalCMake_21st_Edition.pdf
/ProfessionalCMake_21st_Edition.txt
22 changes: 12 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ cmake --build build/host --target format-check
# single commit with `git commit --no-verify`.

# Python type-check (not covered by format.sh - types are not formatting)
cd py/host-emulator && uv run mypy src
cd py/host-emulator && uv run mypy

# Cross-compile for ARM - not yet functional. Presets and toolchain files exist,
# but src/libs/mcu/CMakeLists.txt does add_subdirectory(${EMBEDDED_CPP_MCU}) and
# only the `host` implementation exists, so configure fails on the missing
# arm_cm4/ directory. Host build and emulation come first; hardware follows.
cmake --workflow --preset=stm32f3_discovery-release
# Cross-compile for ARM - not yet functional. Toolchain files and configure
# presets exist, but only the `host` MCU/board implementations do; configuring
# an ARM preset stops with a message saying the backend is not implemented.
# Host build and emulation come first; hardware follows.
cmake --preset=stm32f3_discovery

# Docker alternative
docker compose run --rm host-debug
docker compose run --rm embedded-cpp-dev cmake --workflow --preset host-debug
```

## Architecture
Expand Down Expand Up @@ -93,15 +93,16 @@ class MyApp {

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/host_emulator/`
3. Add message types to `host_emulator_messages.hpp` and their JSON tables to `emulator_message_json_encoder.hpp`
4. Update the Python emulator in `py/host-emulator/src/host_emulator/`. The wire protocol is documented in `py/host-emulator/README.md`; the C++ and Python vocabularies mirror each other and must change together
5. Write unit tests (C++) and integration tests (Python)
6. Implement hardware versions in board-specific directories

## Testing

- **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. CTest builds a uv venv under `build/host/host_emulator_venv` and runs them as the `host_emulator_test` target
- **Python integration tests**: `py/host-emulator/tests/`, use pytest with fixtures that manage emulator/app lifecycle. They run as the `host_emulator_test` CTest target; a CTest setup fixture syncs a uv venv under `build/host/host_emulator_venv` first (a no-op once synced)
- **System tests**: none yet — end-to-end coverage lives in the Python integration tests. Add a dedicated harness only when a test doesn't fit the emulator harness
- **clang-tidy**: Runs automatically during build, no separate step needed
- **Python tooling**: uv + ruff + strict mypy, all configured in `py/host-emulator/pyproject.toml`

Expand All @@ -111,4 +112,5 @@ class MyApp {
- `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
- `py/host-emulator/README.md` - The ZeroMQ/JSON wire protocol (canonical doc)
- `CMakePresets.json` - Build configurations for host and ARM targets
172 changes: 99 additions & 73 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,124 @@ cmake_minimum_required(VERSION 3.27)

project(embedded-cpp-bsp VERSION 0.0.1 LANGUAGES CXX C ASM)

# ---------------------------------------------------------------------------
# Project-wide setup
# ---------------------------------------------------------------------------

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Warnings only. Optimization and debug-info flags are per-configuration and must
# not be set here: these options are appended after the per-config flags, and the
# last -O on the command line wins, so a global -Os silently overrode Release's
# -O3 and made Debug builds optimized. The ARM toolchain sets its own
# Defines the BUILD_TESTING option (ON by default) and calls enable_testing().
# Testing is enabled here, unconditionally and early, so no add_test() call in
# a subdirectory can be silently discarded.
include(CTest)

# Platform selection. The presets set these; a plain `cmake -B build` gets the
# host defaults, so the project configures without any preset at all.
set(EMBEDDED_CPP_MCU "host" CACHE STRING
"MCU backend to build (selects src/libs/mcu/<value>)")
set_property(CACHE EMBEDDED_CPP_MCU PROPERTY STRINGS host arm_cm4 arm_cm7)
set(EMBEDDED_CPP_BOARD "host" CACHE STRING
"Board implementation to build (selects src/libs/board/<value>)")
set_property(CACHE EMBEDDED_CPP_BOARD PROPERTY STRINGS host stm32f3_discovery)

# One bin/ directory per build tree (with per-config subdirectories under the
# multi-config generator). A build-layout decision, so it lives here rather
# than in the presets.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")

# Keep a compile_commands.json symlink at the repo root so clangd and IDEs find
# the database of whichever build tree was configured last. The link is
# regenerated on every configure and is gitignored.
file(CREATE_LINK "${PROJECT_BINARY_DIR}/compile_commands.json"
"${PROJECT_SOURCE_DIR}/compile_commands.json" SYMBOLIC)

# Common usage requirements, carried by INTERFACE targets so each consumer
# states its dependency explicitly instead of inheriting directory state.
#
# project_warnings: warnings only. Optimization and debug-info flags are
# per-configuration and must not be set here: these options are appended after
# the per-config flags, and the last -O on the command line wins, so a global
# -Os would silently override Release's -O3. The ARM toolchain sets its own
# CMAKE_*_FLAGS_{DEBUG,RELEASE}_INIT (cmake/toolchain/armgcc.cmake); the host
# build uses CMake's defaults.
set(COMMON_COMPILE_OPTIONS
add_library(project_warnings INTERFACE)
target_compile_options(project_warnings INTERFACE
-Wall
-Wextra
-Werror
-Wpedantic
$<$<CXX_COMPILER_ID:Clang>:-Wno-c++98-compat;-Wno-exit-time-destructors;-Wno-global-constructors;-Wno-weak-vtables>
$<$<CXX_COMPILER_ID:GNU>:-Wno-unknown-pragmas>
)

if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set(COMMON_COMPILE_OPTIONS ${COMMON_COMPILE_OPTIONS}
-Wno-c++98-compat
-Wno-exit-time-destructors
-Wno-global-constructors
-Wno-weak-vtables
-fno-rtti
-stdlib=libc++
)
# Needed to make clang-tidy and ensure all the built libs use
# the same C++ stdlib implementation
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
endif()
# project_options: everything a target of this project compiles with beyond
# warnings — the src/ include root (headers are included by their project-
# relative path, e.g. "libs/mcu/pin.hpp") and the no-RTTI policy.
add_library(project_options INTERFACE)
target_include_directories(project_options INTERFACE
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>)
target_compile_options(project_options INTERFACE
$<$<COMPILE_LANGUAGE:CXX>:-fno-rtti>)
target_link_libraries(project_options INTERFACE project_warnings)

if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
set(COMMON_COMPILE_OPTIONS ${COMMON_COMPILE_OPTIONS}
-Wno-unknown-pragmas
-fno-rtti
)
endif()
# ---------------------------------------------------------------------------
# Dependencies
# ---------------------------------------------------------------------------

include(FetchContent)

# GIT_TAG pins the commit hash (with the tag name alongside for the reader):
# tags can move, hashes cannot.

# Reusable CMake modules: clang-tidy integration (tools) and coverage
FetchContent_Declare(
etl
GIT_REPOSITORY https://github.com/ETLCPP/etl
GIT_TAG 20.38.1
CmakeScripts
GIT_REPOSITORY https://github.com/StableCoder/cmake-scripts.git
GIT_TAG 5b6c6efaeaab749001b1a1323f46e0ba7cf1c01f # 25.08
)

FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest
GIT_TAG v1.14.0
GIT_TAG f8d7d77c06936315286eb55f8de22cd23c188571 # v1.14.0
)

# cppzmq is a header-only C++ binding; the libzmq it binds is expected to be
# installed on the system (libzmq3-dev).
FetchContent_Declare(
stm32cubef7
GIT_REPOSITORY https://github.com/STMicroelectronics/STM32CubeF7
GIT_TAG v1.17.1
cppzmq
GIT_REPOSITORY https://github.com/zeromq/cppzmq.git
GIT_TAG c94c20743ed7d4aa37835a5c46567ab0790d4acc # v4.10.0
)

FetchContent_Declare(
CmakeScripts
GIT_REPOSITORY https://github.com/StableCoder/cmake-scripts.git
GIT_TAG 25.08
json
GIT_REPOSITORY https://github.com/nlohmann/json
GIT_TAG 0ca0fe433eb70cea0d5761079c0c5b47b736565b # v3.11.2
)

FetchContent_GetProperties(CmakeScripts)

if(NOT cmakescripts_POPULATED)
FetchContent_MakeAvailable(CmakeScripts)
set(CMAKE_MODULE_PATH ${cmakescripts_SOURCE_DIR} ${CMAKE_MODULE_PATH})
endif()
FetchContent_MakeAvailable(CmakeScripts)
list(PREPEND CMAKE_MODULE_PATH "${cmakescripts_SOURCE_DIR}")

# Assume libzmq is installed on the system
# ZeroMQ, JSON, and googletest serve the host-emulation platform; a hardware
# build has no use for them.
if(EMBEDDED_CPP_MCU STREQUAL "host")
set(CPPZMQ_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(cppzmq json)

# CPPZMQ header-only library; uses ZeroMQ
option(CPPZMQ_BUILD_TESTS OFF)
FetchContent_Declare(cppzmq GIT_REPOSITORY https://github.com/zeromq/cppzmq.git GIT_TAG v4.10.0)

# FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.11.2/json.tar.xz)
FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json GIT_TAG v3.11.2)

FetchContent_MakeAvailable(etl)
if(BUILD_TESTING)
# For Windows: prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
endif()
endif()

add_subdirectory(external)
# ---------------------------------------------------------------------------
# Tooling: clang-tidy, coverage, formatting
# ---------------------------------------------------------------------------

include(tools)
include(code-coverage)
Expand All @@ -94,34 +128,26 @@ include(code-coverage)
if(CODE_COVERAGE)
# Create 'ccov-all' target and set global exclusions
add_code_coverage_all_targets(
EXCLUDE "test_*.cpp" "*/test/*" "*/_deps/*" "*/googletest/*" "*/gtest/*" "*/external/*"
LLVM_EXCLUDE ".*/test_.*\\.cpp" ".*/test/.*" ".*/_deps/.*" ".*/googletest/.*" ".*/gtest/.*" ".*/external/.*"
LCOV_EXCLUDE "*/test_*.cpp" "*/test/*" "*/_deps/*" "*/googletest/*" "*/gtest/*" "*/external/*"
EXCLUDE "test_*.cpp" "*/test/*" "*/_deps/*" "*/googletest/*" "*/gtest/*"
LLVM_EXCLUDE ".*/test_.*\\.cpp" ".*/test/.*" ".*/_deps/.*" ".*/googletest/.*" ".*/gtest/.*"
LCOV_EXCLUDE "*/test_*.cpp" "*/test/*" "*/_deps/*" "*/googletest/*" "*/gtest/*"
)
endif()

if(CMAKE_PRESET STREQUAL "host")
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(googletest)
FetchContent_MakeAvailable(cppzmq)
FetchContent_MakeAvailable(json)
include(CTest)
enable_testing()
endif()

if(CMAKE_PRESET STREQUAL "arm-cm7")
FetchContent_MakeAvailable(stm32cubef7)
endif()

# `format` / `format-check` targets, and the pre-commit hook. Included for every
# preset: formatting is not host-specific, and a cross-compiling developer
# should get the same guard rails.
# configuration: formatting is not host-specific, and a cross-compiling
# developer should get the same guard rails.
include("${PROJECT_SOURCE_DIR}/cmake/format.cmake")

clang_tidy("-header-filter=${CMAKE_CURRENT_SOURCE_DIR}/src/.*}")
# ---------------------------------------------------------------------------
# Targets
# ---------------------------------------------------------------------------

clang_tidy("-header-filter=${PROJECT_SOURCE_DIR}/src/.*")
add_subdirectory(src)
add_subdirectory(test)
reset_clang_tidy()
add_subdirectory(py) # No clang-tidy for python

# The Python emulator and its integration tests exist to test the host build.
if(EMBEDDED_CPP_MCU STREQUAL "host")
add_subdirectory(py/host-emulator) # No clang-tidy for python
endif()
Loading
Loading