Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ jobs:
- name: Test
run: ctest --test-dir build-core --output-on-failure

# ebus_mqtt depends on nothing: each of its sources and headers must compile with
# its own include directory as the only one, so a Homie or ArduinoJson include fails.
- name: ebus_mqtt alone
run: |
set -e
flags="-std=c++17 -Wall -Wextra -Werror -I mqtt/include"
for f in mqtt/src/*.cpp; do
echo "$f"
${{ matrix.cxx }} $flags -c "$f" -o /dev/null
done
for h in mqtt/include/ebus/mqtt/*.h; do
echo "$h"
echo "#include <${h#mqtt/include/}>" | ${{ matrix.cxx }} $flags -x c++ -c - -o /dev/null
done

# The POSIX port (ports/posix): the core on Linux over Paho MQTT C, its demo programs,
# and end-to-end tests against a mosquitto each test starts on a free localhost port.
posix-port:
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to `cpp-sdk` are recorded here. Format follows [Keep a Chang

## [Unreleased]

### Added

- `ebus_mqtt`, a CMake target with no dependency (`mqtt/`, [doc/mqtt.md](doc/mqtt.md)): `MqttTransport`, the transport interface, whose `queue_publish()` takes a QoS and a completion callback (`mqtt_publish_done_fn` and a `void*` context); `PublishHold`, the disconnected-link rules, moved from the POSIX port; and `mqtt_after_connect()`, which runs the reconnect order (flush, re-subscribe, notify). `ebus_core` links it. PlatformIO still builds one library.
- Unity suites `test_mqtt_publish_hold` and `test_mqtt_reconnect`, which link `ebus_mqtt` alone, and a CI step that compiles `ebus_mqtt` with only its own include directory.

### Changed

- `HomieTransport` derives from `MqttTransport`. Its `queue_publish(..., Property* source)` is no longer pure: it forwards to the generic overload with a callback that calls `Property::queued_publish_done()`. A port that overrides it, as esp32-sdk's does, builds unchanged; a new port overrides the generic overload instead.
- `PublishHold` sizes are `EBUS_MQTT_HOLD_ENTRIES`, `EBUS_MQTT_HOLD_TOPIC_MAX` and `EBUS_MQTT_HOLD_PAYLOAD_MAX` (were `EBUS_POSIX_HOLD_ENTRIES` and `EBUS_POSIX_HOLD_PAYLOAD_MAX`). It no longer logs; `hold()` returns `EVICTED_OLDEST` or `DROPPED_TOO_LARGE` and the port logs. Its header is `<ebus/mqtt/publish_hold.h>`.
- The POSIX port's `PahoTransport` overrides the generic `queue_publish()` and runs its connect through `mqtt_after_connect()`.

## [0.1.0] - 2026-10-08

### Added
Expand Down
41 changes: 25 additions & 16 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Builds the core without PlatformIO, for Zephyr, embedded Linux, or a host check.
# Usage from a parent project: add_subdirectory(path/to/cpp-sdk) and link ebus_core.
# Usage from a parent project: add_subdirectory(path/to/cpp-sdk) and link ebus_core, the
# umbrella, or one component target (ebus_mqtt).
cmake_minimum_required(VERSION 3.18)
project(ebus_core LANGUAGES CXX)

add_subdirectory(mqtt)

add_library(ebus_core STATIC
src/homie/controller.cpp
src/homie/controller_inbox.cpp
Expand All @@ -22,6 +25,7 @@ add_library(ebus_core STATIC
)
target_include_directories(ebus_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(ebus_core PUBLIC cxx_std_17)
target_link_libraries(ebus_core PUBLIC ebus_mqtt)

# ArduinoJson, the core's one dependency (header-only). A parent project that already has
# an ArduinoJson target keeps it. Otherwise the release PlatformIO uses (library.json,
Expand Down Expand Up @@ -56,26 +60,31 @@ endif()

if(_ebus_core_top_level AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(ebus_core PRIVATE -Wall -Wextra)
target_compile_options(ebus_mqtt PRIVATE -Wall -Wextra)
endif()

# Compiles every public header in a translation unit of its own, so a header that needs
# something it does not include (or includes a platform header) fails here.
# something it does not include (or includes a platform header) fails here. A component's
# headers are compiled against that component's target alone.
option(EBUS_CORE_CHECK_HEADERS "Compile each public header on its own" ${_ebus_core_top_level})
if(EBUS_CORE_CHECK_HEADERS)
file(GLOB_RECURSE _ebus_core_headers RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/include
CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
set(_ebus_core_header_tus "")
foreach(_header IN LISTS _ebus_core_headers)
string(MAKE_C_IDENTIFIER ${_header} _tu_name)
set(_tu ${CMAKE_CURRENT_BINARY_DIR}/header_check/${_tu_name}.cpp)
file(CONFIGURE OUTPUT ${_tu} CONTENT "#include <${_header}>\n")
list(APPEND _ebus_core_header_tus ${_tu})
endforeach()
add_library(ebus_core_header_check OBJECT ${_ebus_core_header_tus})
target_link_libraries(ebus_core_header_check PRIVATE ebus_core)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(ebus_core_header_check PRIVATE -Wall -Wextra)
endif()
function(ebus_header_check target include_dir)
file(GLOB_RECURSE _headers RELATIVE ${include_dir} CONFIGURE_DEPENDS ${include_dir}/*.h)
set(_tus "")
foreach(_header IN LISTS _headers)
string(MAKE_C_IDENTIFIER ${_header} _tu_name)
set(_tu ${CMAKE_CURRENT_BINARY_DIR}/header_check/${target}/${_tu_name}.cpp)
file(CONFIGURE OUTPUT ${_tu} CONTENT "#include <${_header}>\n")
list(APPEND _tus ${_tu})
endforeach()
add_library(${target}_header_check OBJECT ${_tus})
target_link_libraries(${target}_header_check PRIVATE ${target})
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${target}_header_check PRIVATE -Wall -Wextra)
endif()
endfunction()
ebus_header_check(ebus_mqtt ${CMAKE_CURRENT_SOURCE_DIR}/mqtt/include)
ebus_header_check(ebus_core ${CMAKE_CURRENT_SOURCE_DIR}/include)
endif()

# The Unity suites in test/, one CTest test each. Off when the core is a subproject.
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ Pull requests are welcome.
- For substantive changes (new public API, changes to `HomieTransport` or the other port interfaces, new ports, changes that alter Homie/eBus wire behavior), open a Discussion or Issue first so we can align on scope before you invest the effort.
- **Spec conformance is the north star.** The [Homie 5 convention](https://homieiot.github.io/) and the [Electrification Bus specification](https://github.com/electrification-bus/specification) are the source of truth for on-the-wire behavior: topic structure, datatypes, `$state` semantics, the nested/proxied device model, the eBus device- and capability-type vocabularies. When the code and the spec disagree, the spec wins; fix the code rather than codifying the deviation.
- **Update `.ebus-spec.json` on every spec sync.** When you reconcile the core with a newer specification commit, bump `synced_commit`, `synced_date`, and `framework`, and edit `supports` to match the framework features the code now implements (see the specification's `conventions/spec-provenance.md`).
- **House style for the core (`include/`, `src/`).** It runs on microcontrollers, so:
- **House style for the core (`include/`, `src/`, `mqtt/`).** It runs on microcontrollers, so:
- No dynamic allocation in steady state. Use fixed-size buffers and static or caller-owned storage; what allocates today is listed in [doc/core.md](doc/core.md) ("Rules for code in the core"), and new code adds nothing to that list.
- No STL, no Arduino `String`, no lambdas. Use `snprintf` over string concatenation, and the C function-pointer + `void*` context idiom for callbacks. Members are underscore-prefixed.
- No Arduino, ESP-IDF, FreeRTOS or OS header. The core includes only standard headers, ArduinoJson and other core headers; anything platform-specific goes through the port interfaces. CI builds the core with only its own include directory and fails on anything else.
- No Arduino, ESP-IDF, FreeRTOS or OS header. The core includes only standard headers, ArduinoJson and other core headers, and `ebus_mqtt` (`mqtt/`) only standard headers and its own; anything platform-specific goes through the port interfaces. CI builds each with only its own include directory and fails on anything else.
- C++17, GCC or Clang, warnings as errors in CI.
- **Ports (`ports/`) may use their platform's facilities**, but keep the core's contract as documented in [doc/core.md](doc/core.md), including the rules for publishes while the broker link is down.
- **Run the tests.** `cmake -S . -B build && cmake --build build -j && ctest --test-dir build --output-on-failure` runs the core's Unity suites; the POSIX port's tests (`ctest --test-dir ports/posix/build`) need `mosquitto`. See the [README](README.md). To add a core suite, create `test/test_<name>/test_<name>.cpp` with its own `main()`; `test/CMakeLists.txt` picks it up. The fakes in `test/support/` stand in for a port. CI runs both test sets with GCC and Clang on every push.
- **Run the tests.** `cmake -S . -B build && cmake --build build -j && ctest --test-dir build --output-on-failure` runs the core's Unity suites; the POSIX port's tests (`ctest --test-dir ports/posix/build`) need `mosquitto`. See the [README](README.md). To add a core suite, create `test/test_<name>/test_<name>.cpp` with its own `main()`; `test/CMakeLists.txt` picks it up. A suite named `test_mqtt_<name>` links `ebus_mqtt` alone. The fakes in `test/support/` stand in for a port. CI runs both test sets with GCC and Clang on every push.
- **Test wire changes against a real broker.** A unit test is necessary but not sufficient for anything that changes what goes on the wire; the POSIX port's end-to-end tests and its demo programs run the core against mosquitto.
- **Respect upstream licenses: reference, don't copy.** This SDK is MIT. Do **not** paste source from copyleft projects into it. PRs that copy GPL/copyleft code will be asked to reimplement.
- **Keep comments to a minimum.** Write self-explanatory code and reserve comments for non-obvious *why* (a spec corner, a platform quirk). Don't add comments that just restate the code.
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,24 +52,31 @@ cmake --build build -j
ctest --test-dir build --output-on-failure
```

As the top-level project this builds the `ebus_core` library, compiles each public header on its own, and runs the Unity suites in [`test/`](test/). ArduinoJson 7.4.3 and Unity 2.6.1 are fetched and checked against their SHA-256. A parent CMake project uses `add_subdirectory(<path>/cpp-sdk)` and links `ebus_core`; the tests are then off.
As the top-level project this builds the libraries below, compiles each public header on its own, and runs the Unity suites in [`test/`](test/). ArduinoJson 7.4.3 and Unity 2.6.1 are fetched and checked against their SHA-256. A parent CMake project uses `add_subdirectory(<path>/cpp-sdk)` and links `ebus_core`; the tests are then off.

| Target | Contents | Depends on | Docs |
|---|---|---|---|
| `ebus_mqtt` | The MQTT transport interface (`MqttTransport`), the publishes held while the link is down (`PublishHold`), the reconnect order (`mqtt_after_connect()`) | nothing | [`doc/mqtt.md`](doc/mqtt.md) |
| `ebus_core` | Everything: the Homie model, `/set` dispatch, the controller, validation, broker discovery, and `ebus_mqtt` | `ebus_mqtt`, ArduinoJson | [`doc/core.md`](doc/core.md) |

`ebus_mqtt` builds with only its own include directory (`mqtt/include`), so a project can take it alone with `add_subdirectory(<path>/cpp-sdk/mqtt)`.

## Writing a port

A port binds four things, all documented in [`doc/core.md`](doc/core.md) ("What a port implements"):

| Piece | Header | What the port does |
|---|---|---|
| MQTT transport | `homie/homie_transport.h` | Subclasses `HomieTransport`: `publish()`, `subscribe()` and `queue_publish()` |
| MQTT transport | `homie/homie_transport.h` | Subclasses `HomieTransport`, which is `ebus_mqtt`'s `MqttTransport` plus a Homie adapter: `publish()`, `subscribe()` and `queue_publish()` with a completion callback ([`doc/mqtt.md`](doc/mqtt.md)) |
| Clock | `homie/homie_clock.h` | `homie_clock_bind(now_ms, sleep_ms)` |
| Console | `homie/homie_log.h` | `homie_log_bind(vprintf_sink)`; optional, stdout otherwise |
| Settable table | `homie/homie_settable.h` | Supplies storage to `settable_table_bind()`, routes each claimed `/set` message to `settable_dispatch()` on its own task, and subscribes every settable topic after each connect |

The POSIX port is the reference: [`ports/posix/src/posix_port.cpp`](ports/posix/src/posix_port.cpp) binds the clock, console and settable table, and [`ports/posix/src/paho_transport.cpp`](ports/posix/src/paho_transport.cpp) is the transport. A port should also follow the rules for publishes made while the broker link is down, which the POSIX port takes from [ebus-mqtt-client](https://github.com/electrification-bus/ebus-mqtt-client).
The POSIX port is the reference: [`ports/posix/src/posix_port.cpp`](ports/posix/src/posix_port.cpp) binds the clock, console and settable table, and [`ports/posix/src/paho_transport.cpp`](ports/posix/src/paho_transport.cpp) is the transport. A port should also follow the rules for publishes made while the broker link is down and the reconnect order, both from [ebus-mqtt-client](https://github.com/electrification-bus/ebus-mqtt-client); `ebus_mqtt` provides them as `PublishHold` and `mqtt_after_connect()`, which the POSIX port uses.

## Using the core from PlatformIO

[`library.json`](library.json) makes this repository a PlatformIO library named `ebus_core` that compiles `include/` and `src/` and depends on ArduinoJson 7.4.3. Add it with a git URL:
[`library.json`](library.json) makes this repository one PlatformIO library named `ebus_core` that compiles `src/` and `mqtt/src/`, puts `include/` and `mqtt/include/` on the include path, and depends on ArduinoJson 7.4.3. Add it with a git URL:

```ini
lib_deps =
Expand Down
Loading
Loading