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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to cpp-sdk

Thanks for your interest in contributing! `cpp-sdk` is a portable C++ core for eBus devices and controllers using the [Homie 5 MQTT convention](https://homieiot.github.io/), with a POSIX reference port. Platform ports supply the MQTT client, console and clock; [esp32-sdk](https://github.com/electrification-bus/esp32-sdk) is the ESP32 port.
Thanks for your interest in contributing! `cpp-sdk` is a portable C++ core for eBus devices and controllers using the [Homie 5 MQTT convention](https://homieiot.github.io/), with a POSIX reference port. Platform ports supply the MQTT client, console and clock; [esp32-firmware](https://github.com/electrification-bus/esp32-firmware) is the ESP32 port.

## How to contribute

Expand Down Expand Up @@ -57,4 +57,4 @@ Be respectful and constructive. We appreciate everyone who takes the time to fil

## Maintenance posture

`cpp-sdk` is an active alpha project. Updates and maintenance, including responses to issues filed on GitHub, will take place on an "as time and resources permit" basis. It is maintained alongside the rest of the eBus ecosystem, the [Electrification Bus specification](https://github.com/electrification-bus/specification), [`esp32-sdk`](https://github.com/electrification-bus/esp32-sdk), [`python-sdk`](https://github.com/electrification-bus/python-sdk), and [`ebus-mqtt-client`](https://github.com/electrification-bus/ebus-mqtt-client); see the specification repo's README 搂Governance for the project's long-term governance context.
`cpp-sdk` is an active alpha project. Updates and maintenance, including responses to issues filed on GitHub, will take place on an "as time and resources permit" basis. It is maintained alongside the rest of the eBus ecosystem, the [Electrification Bus specification](https://github.com/electrification-bus/specification), [`esp32-firmware`](https://github.com/electrification-bus/esp32-firmware), [`python-sdk`](https://github.com/electrification-bus/python-sdk), and [`ebus-mqtt-client`](https://github.com/electrification-bus/ebus-mqtt-client); see the specification repo's README 搂Governance for the project's long-term governance context.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ A portable C++17 core for devices and controllers on the [Electrification Bus](h

| Target | Where | MQTT client |
|---|---|---|
| ESP32 (Arduino framework, PlatformIO) | [esp32-sdk](https://github.com/electrification-bus/esp32-sdk) | arduino-mqtt |
| ESP32 (Arduino framework, PlatformIO) | [esp32-firmware](https://github.com/electrification-bus/esp32-firmware) | arduino-mqtt |
| POSIX (Linux, macOS) | [`ports/posix/`](ports/posix/) in this repository | Eclipse Paho MQTT C |

## Status

Version 0.7.0, alpha. Known limits:

- The POSIX demo build has no TLS; it connects to plain-TCP brokers only.
- The driver contract (`NodeEntity`, `NodeProperty`) is still in esp32-sdk ([why](doc/core.md#still-in-esp32-sdk)).
- The driver contract (`NodeEntity`, `NodeProperty`) is still in esp32-firmware ([why](doc/core.md#still-in-esp32-firmware)).
- The code-first API has rough edges found while writing the POSIX demo: registering a settable property needs a live connection, and the controller has no change callback.
- esp32-sdk is not public yet; links to it will resolve once it is.
- esp32-firmware is not public yet; links to it will resolve once it is.

## Quick start: the POSIX demo

Expand Down Expand Up @@ -92,15 +92,15 @@ build_unflags =
-std=gnu++11
```

The core needs C++17; `build_unflags` drops the gnu++11 default of arduino-esp32. It is compiled without the project's `build_src_flags`, so a macro it reads (`USE_EBUS_TOPIC`, `MAX_DATA_LEN`, `CONTROLLER_INBOX_BYTES`) goes in `build_flags`. esp32-sdk carries the same code in `lib/ebus_core/` until it moves to this dependency.
The core needs C++17; `build_unflags` drops the gnu++11 default of arduino-esp32. It is compiled without the project's `build_src_flags`, so a macro it reads (`USE_EBUS_TOPIC`, `MAX_DATA_LEN`, `CONTROLLER_INBOX_BYTES`) goes in `build_flags`. esp32-firmware carries the same code in `lib/ebus_core/` until it moves to this dependency.

## License

[MIT License](LICENSE) - Copyright (c) 2026 Clark Communications Corporation. Fetched dependencies and their licenses: [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).

## Authors

Split out of [esp32-sdk](https://github.com/electrification-bus/esp32-sdk), with its history. Developed by:
Split out of [esp32-firmware](https://github.com/electrification-bus/esp32-firmware), with its history. Developed by:

- **Lead developer:** Doug Mendon莽a ([@nesl-admin](https://github.com/nesl-admin)), New Energy Solutions Lab, [doug@newenergysolutionslab.com](mailto:doug@newenergysolutionslab.com). Primary author of the firmware this code was split from, and a significant contributor to the eBus protocol and ecosystem more broadly.
- **Project owner:** Donald Clark Jackson ([@dcj](https://github.com/dcj)), Clark Communications Corporation, [dcj@clark-communications.com](mailto:dcj@clark-communications.com). Project direction and documentation.
12 changes: 6 additions & 6 deletions doc/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The headers below are the `ebus_homie` target (sources in `homie/`): the Homie m
| `ebus/homie/homie_clock.h` | `homie_now_ms()` / `homie_sleep_ms()` and the hooks a port binds |
| `ebus/homie/homie_json.h`, `ebus/homie/jsonUtils.h` | `$description` (de)serialization helpers (ArduinoJson) |
| `ebus/homie/homie.h` | Homie version, topic domain and prefix (`USE_EBUS_TOPIC` at build time, `homie_set_topic_domain()` at run time), datatype and attribute strings |
| `ebus/homie/homie_limits.h` | Longest ids and topics; every buffer is sized from these. esp32-sdk's `./ebus-esp32 generate` reads the four base values from `include/homie/homie_limits.h`, which repeats them, to check `device.yml` |
| `ebus/homie/homie_limits.h` | Longest ids and topics; every buffer is sized from these. esp32-firmware's `./ebus-esp32 generate` reads the four base values from `include/homie/homie_limits.h`, which repeats them, to check `device.yml` |
| `ebus/homie/homie_enums.h` | `PropertyDatatype` and `Unit` enums and their Homie strings |
| `ebus/homie/homie_descriptor.h` | `PropertyDesc`, the declarative property descriptor |
| `ebus/homie/homie_id.h` | Id sanitizing, MAC-based ids, id templates, child ids |
Expand All @@ -25,7 +25,7 @@ The headers below are the `ebus_homie` target (sources in `homie/`): the Homie m

## What a port implements

| Piece | Contract | ESP32 port ([esp32-sdk](https://github.com/electrification-bus/esp32-sdk)) |
| Piece | Contract | ESP32 port ([esp32-firmware](https://github.com/electrification-bus/esp32-firmware)) |
|---|---|---|
| MQTT transport | A `HomieTransport` subclass. `publish()` and `subscribe()` run on the task that owns the client and never inside its receive callback. `queue_publish()` may be called from any task; the port overrides either the generic overload, which must call `done(ctx, payload, length, sent)` exactly once per call when `done` is set, or the Property overload, which must call `source->queued_publish_done(payload, length, sent)` exactly once per call when `source` is set, in both cases including when it fails ([`mqtt.md`](mqtt.md)). `last_error()` is for logs only. | `MqttClientTransport` in `include/platform/mqtt_client.h`, over arduino-mqtt and the FreeRTOS publish queue; the instance is `mqtt_transport` |
| Clock | `homie_clock_bind(now_ms, sleep_ms)`. Required: unbound, the tick reads 0 and the sleep returns at once. | `millis()` / `delay()`, bound in `src/platform/homie_port.cpp` |
Expand Down Expand Up @@ -83,17 +83,17 @@ cmake --build build
ctest --test-dir build --output-on-failure
```

A parent project uses `add_subdirectory(<path>/cpp-sdk)` and links the `ebus_core` target, an INTERFACE target that links `ebus_mqtt`, `ebus_discovery` and `ebus_homie` and carries `include/`, the pre-split header paths. `ebus_homie` carries its include directory, C++17, ArduinoJson and `ebus_mqtt`; a project that wants only the Homie model can `add_subdirectory(<path>/cpp-sdk/homie)`, which adds `ebus_mqtt` too. A parent that already defines an `ArduinoJson` target keeps it; otherwise `FetchContent` downloads ArduinoJson 7.4.3, the release esp32-sdk's `platformio.ini` uses, checked against its SHA-256, so both builds serialize `$description` with the same code. `find_package()` was not used because few systems package ArduinoJson and none would pin that release. To build offline, pass `-DFETCHCONTENT_SOURCE_DIR_ARDUINOJSON=<checkout>`. `-DEBUS_CORE_EBUS_TOPIC=ON` defines `USE_EBUS_TOPIC`, which moves the topic root from `homie/5` to `ebus/5`; the ESP32 firmware sets it in `platformio.ini`. Built as the top-level project, the targets `ebus_core_header_check`, `ebus_mqtt_header_check`, `ebus_discovery_header_check` and `ebus_homie_header_check` also compile each public header in a translation unit of its own (a component's header against that component alone), and the Unity suites in `test/` are built and registered with CTest (`-DEBUS_CORE_TESTS=OFF` skips them).
A parent project uses `add_subdirectory(<path>/cpp-sdk)` and links the `ebus_core` target, an INTERFACE target that links `ebus_mqtt`, `ebus_discovery` and `ebus_homie` and carries `include/`, the pre-split header paths. `ebus_homie` carries its include directory, C++17, ArduinoJson and `ebus_mqtt`; a project that wants only the Homie model can `add_subdirectory(<path>/cpp-sdk/homie)`, which adds `ebus_mqtt` too. A parent that already defines an `ArduinoJson` target keeps it; otherwise `FetchContent` downloads ArduinoJson 7.4.3, the release esp32-firmware's `platformio.ini` uses, checked against its SHA-256, so both builds serialize `$description` with the same code. `find_package()` was not used because few systems package ArduinoJson and none would pin that release. To build offline, pass `-DFETCHCONTENT_SOURCE_DIR_ARDUINOJSON=<checkout>`. `-DEBUS_CORE_EBUS_TOPIC=ON` defines `USE_EBUS_TOPIC`, which moves the topic root from `homie/5` to `ebus/5`; the ESP32 firmware sets it in `platformio.ini`. Built as the top-level project, the targets `ebus_core_header_check`, `ebus_mqtt_header_check`, `ebus_discovery_header_check` and `ebus_homie_header_check` also compile each public header in a translation unit of its own (a component's header against that component alone), and the Unity suites in `test/` are built and registered with CTest (`-DEBUS_CORE_TESTS=OFF` skips them).

CI (`core-host-build` in `.github/workflows/ci.yml`) runs that CMake build and the tests with GCC and Clang, warnings as errors. Its step "ebus_homie alone" compiles each source and header in `homie/` with `homie/include`, `mqtt/include` and ArduinoJson as the only include directories. The step "ebus_link alone" does the same for `link/` with `link/include` added. The Homie suites in `test/` link `ebus_homie` without `ebus_core`.

## Include paths

The Homie headers are `<ebus/homie/...>`. The paths they had under the project's `include/` (`<homie/Device.h>`, `<util/jsonUtils.h>`, `<platform/broker_discovery.h>`) are forwarding headers to `ebus_homie` and `ebus_discovery`, so no `#include` in esp32-sdk changed when they moved. esp32-sdk's `./ebus-esp32 generate` parses `include/homie/homie_limits.h` for `HOMIE_DEVICE_ID_MAX`, `HOMIE_NODE_ID_MAX`, `HOMIE_PROPERTY_ID_MAX` and `HOMIE_TOPIC_MAX`, so that forwarder repeats those four `#define`s; a value that differs from `ebus/homie/homie_limits.h` is a macro redefinition warning, which fails CI.
The Homie headers are `<ebus/homie/...>`. The paths they had under the project's `include/` (`<homie/Device.h>`, `<util/jsonUtils.h>`, `<platform/broker_discovery.h>`) are forwarding headers to `ebus_homie` and `ebus_discovery`, so no `#include` in esp32-firmware changed when they moved. esp32-firmware's `./ebus-esp32 generate` parses `include/homie/homie_limits.h` for `HOMIE_DEVICE_ID_MAX`, `HOMIE_NODE_ID_MAX`, `HOMIE_PROPERTY_ID_MAX` and `HOMIE_TOPIC_MAX`, so that forwarder repeats those four `#define`s; a value that differs from `ebus/homie/homie_limits.h` is a macro redefinition warning, which fails CI.

## Still in esp32-sdk
## Still in esp32-firmware

`NodeEntity` and `NodeProperty` (esp32-sdk's `include/node/`, `src/node/`), the driver contract, stay in the firmware for two reasons. `include/node/NodeEntity.h` includes `platform/log_stream.h`, which is what routes every `lib/` driver's `Serial.printf()` through the UART + TCP tee; moving it needs a replacement for that include that keeps the tee. And both resolve properties against `theDevice` and `g_active_setup_device` (`include/homie/homie_globals.h`), globals the application defines in `src/platform/config.cpp`.
`NodeEntity` and `NodeProperty` (esp32-firmware's `include/node/`, `src/node/`), the driver contract, stay in the firmware for two reasons. `include/node/NodeEntity.h` includes `platform/log_stream.h`, which is what routes every `lib/` driver's `Serial.printf()` through the UART + TCP tee; moving it needs a replacement for that include that keeps the tee. And both resolve properties against `theDevice` and `g_active_setup_device` (`include/homie/homie_globals.h`), globals the application defines in `src/platform/config.cpp`.

## Next step

Expand Down
12 changes: 6 additions & 6 deletions doc/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The eBus mDNS contract (framework.md, "Detail: mDNS Discovery") as code: the bro
| `ebus/discovery/txt_records.h` | `EbusIdentity`, `TxtRecord`, and the builders for `_ebus._tcp`, `_device-info._tcp`, `_http._tcp` and `_telnet._tcp` |
| `ebus/discovery/mdns_backend.h` | `MdnsBackend`, the interface a port implements: advertise, browse, resolve |
| `ebus/discovery/broker_browse.h` | `broker_browse()` and `broker_reresolve()`, broker discovery over an `MdnsBackend` |
| `ebus/discovery/broker_discovery.h` | Broker service names, DNS-SD types and default ports; the browse list (esp32-sdk's `mqtt.discover`); host name trimming; the re-resolve count |
| `ebus/discovery/broker_discovery.h` | Broker service names, DNS-SD types and default ports; the browse list (esp32-firmware's `mqtt.discover`); host name trimming; the re-resolve count |
| `ebus/discovery/mdns_strings.h` | Service types, TXT keys and fixed values |

`<platform/broker_discovery.h>` and `<platform/mdns_strings.h>`, the earlier paths of two of them, forward to these headers.
Expand Down Expand Up @@ -49,15 +49,15 @@ The rules match the Python library's `Identity` and `HttpService`:
- `ebus_version` defaults to `EBUS_SPEC_VERSION` (`0.9`), `path` to `/api/v1`, `homie_version` to `5`. The HTTP `version` (the API's OpenAPI `info.version`) is required.
- A `key=value` string over 255 bytes (`TXT_TOO_LONG`), a key that is empty, not printable ASCII or holds `=` (`TXT_INVALID`), and a missing required value (`TXT_MISSING`) fail the build, which leaves the record empty.

`homie_domain`, `homie_version` and `homie_roles` are not in framework.md 0.9; they are the keys SPAN panels advertise, and esp32-sdk advertises them too. `homie_roles` is `roles` restricted to `device` and `controller`.
`homie_domain`, `homie_version` and `homie_roles` are not in framework.md 0.9; they are the keys SPAN panels advertise, and esp32-firmware advertises them too. `homie_roles` is `roles` restricted to `device` and `controller`.

`txt_record_add()` appends a key the builders do not know, after a build; it refuses a key already in the record (keys compare case-insensitively, RFC 6763 6.4) and a full record. `txt_record_wire_size()` gives the bytes a record takes on the wire; RFC 6763 6.2 advises keeping it under 1300 (`EBUS_TXT_TOTAL_ADVISED`). `txt_find()` looks a key up in a record or in a browsed instance's pairs.

### Across implementations

`test/test_discovery_txt` builds the records of the Python library's TXT tests and those esp32-sdk's `network.cpp` advertises, and expects the same pairs in the same order. Remaining differences:
`test/test_discovery_txt` builds the records of the Python library's TXT tests and those esp32-firmware's `network.cpp` advertises, and expects the same pairs in the same order. Remaining differences:

| Item | esp32-sdk | ebus-service-discovery | framework.md |
| Item | esp32-firmware | ebus-service-discovery | framework.md |
|---|---|---|---|
| `_ebus._tcp` port | `mqtt.port` | the first HTTP port, else 0 | not specified |
| `homie_*` keys | advertised | not advertised (an `extra_ebus_txt` can add them) | not defined |
Expand Down Expand Up @@ -86,13 +86,13 @@ class MdnsBackend {
};
```

Service types are full DNS-SD types (`_ebus._tcp`, `MDNS_TYPE_EBUS`; `broker_service_type()` for the brokers); a backend splits them as its stack needs. `advertise()` registers the service under the host name the port claimed, with that name as the instance name, as framework.md and esp32-sdk do. `browse()` passes each instance to the callback until it returns false; the instance's strings belong to the backend and last only for the call, so nothing is allocated on either side. `resolve()` takes the bare host name, without `.local`. Claiming the host name is part of each stack's own setup (`MDNS.begin()` on ESP32), so a port does it before using the backend.
Service types are full DNS-SD types (`_ebus._tcp`, `MDNS_TYPE_EBUS`; `broker_service_type()` for the brokers); a backend splits them as its stack needs. `advertise()` registers the service under the host name the port claimed, with that name as the instance name, as framework.md and esp32-firmware do. `browse()` passes each instance to the callback until it returns false; the instance's strings belong to the backend and last only for the call, so nothing is allocated on either side. `resolve()` takes the bare host name, without `.local`. Claiming the host name is part of each stack's own setup (`MDNS.begin()` on ESP32), so a port does it before using the backend.

## Broker discovery

`broker_browse(backend, list, timeout_ms, &found)` browses each service type of a `BrokerDiscoveryList` in order (the default is `_secure-mqtt._tcp`, then `_mqtt._tcp`) and takes the first instance that has an address. `found` holds the service, the address, the SRV port (the service's default port when the record has none) and the bare host name (empty when the SRV target is empty or does not fit). Repeating a browse that found nothing, and how often, is the caller's, since it needs a clock.

After `BROKER_RERESOLVE_AFTER_FAILURES` consecutive failed reconnects (`broker_reconnect_failed()`), `broker_reresolve(backend, &found, timeout_ms)` sends one host query for `found.hostname`: `BROKER_RERESOLVE_MOVED` with the new address in `found`, `BROKER_RERESOLVE_UNCHANGED`, or `BROKER_RERESOLVE_FAILED`, which leaves `found` as it was. These are esp32-sdk's rules (`mdns_query_one_service()` and `mdns_reresolve_broker()`; its [doc/MDNS.md](https://github.com/electrification-bus/esp32-sdk/blob/main/doc/MDNS.md#broker-discovery)).
After `BROKER_RERESOLVE_AFTER_FAILURES` consecutive failed reconnects (`broker_reconnect_failed()`), `broker_reresolve(backend, &found, timeout_ms)` sends one host query for `found.hostname`: `BROKER_RERESOLVE_MOVED` with the new address in `found`, `BROKER_RERESOLVE_UNCHANGED`, or `BROKER_RERESOLVE_FAILED`, which leaves `found` as it was. These are esp32-firmware's rules (`mdns_query_one_service()` and `mdns_reresolve_broker()`; its [doc/MDNS.md](https://github.com/electrification-bus/esp32-firmware/blob/main/doc/MDNS.md#broker-discovery)).

## A POSIX backend

Expand Down
2 changes: 1 addition & 1 deletion doc/link.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A link copies up to three property values into one settable property: a sensor reading onto a display, a state onto a relay, a reading from one device onto another device. Each link is one `EbusLink` (`ebus/link/link.h`), configured with strings, so a port can build links from a config file or command-line flags without code per link. The pure functions underneath (reference parsing, `*` matching, rounding, `%` substitution, retry backoff) are in `ebus/link/link_core.h`.

The logic comes from Doug Mendon莽a's `logic/link` component in [esp32-sdk](https://github.com/electrification-bus/esp32-sdk).
The logic comes from Doug Mendon莽a's `logic/link` component in [esp32-firmware](https://github.com/electrification-bus/esp32-firmware).

```cpp
#include <ebus/link/link.h>
Expand Down
Loading
Loading