Sparkplug B C++ Library 1.0.0
Modern C++-23 implementation of Eclipse Sparkplug B 2.2 specification
Loading...
Searching...
No Matches
edge_node.hpp
1#pragma once
2
3#include "detail/compat.hpp"
4#include "detail/secure_buffer.hpp"
5#include "logging.hpp"
6#include "mqtt_handle.hpp"
7#include "payload_builder.hpp"
8#include "sparkplug_b.pb.h"
9#include "topic.hpp"
10
11#include <atomic>
12#include <functional>
13#include <mutex>
14#include <optional>
15#include <span>
16#include <string>
17#include <unordered_map>
18#include <vector>
19
20#include <MQTTAsync.h>
21
22namespace sparkplug {
23
30using CommandCallback =
31 std::function<void(const Topic&, const org::eclipse::tahu::protobuf::Payload&)>;
32
97class EdgeNode {
98public:
102 struct TlsOptions {
103 std::string trust_store;
104 std::string key_store;
105 std::string private_key;
107 std::string
110 bool verify_hostname = true;
111 };
112
116 struct Config {
117 std::string broker_url;
119 std::string client_id;
120 std::string group_id;
121 std::string edge_node_id;
122 int data_qos = 0;
124 int death_qos = 1;
125 bool clean_session = true;
127 60;
128 std::optional<TlsOptions>
129 tls{};
130 std::optional<std::string>
132 std::optional<std::string>
134 std::optional<CommandCallback> command_callback{};
135 std::optional<std::string> primary_host_id{};
136 std::optional<LogCallback> log_callback{};
137 size_t max_payload_bytes = 1 << 20;
138 };
139
149
154
155 EdgeNode(const EdgeNode&) = delete;
156 EdgeNode& operator=(const EdgeNode&) = delete;
157 EdgeNode(EdgeNode&&) noexcept;
158 EdgeNode& operator=(EdgeNode&&) noexcept;
159
168 void set_credentials(std::optional<std::string> username,
169 std::optional<std::string> password);
170
179 void set_tls(std::optional<TlsOptions> tls);
180
181 void set_log_callback(std::optional<LogCallback> callback);
182
195 [[nodiscard]] stdx::expected<void, std::string> connect();
196
206 [[nodiscard]] stdx::expected<void, std::string> disconnect();
207
228 [[nodiscard]] stdx::expected<void, std::string> publish_birth(PayloadBuilder& payload);
229
251 [[nodiscard]] stdx::expected<void, std::string> publish_data(PayloadBuilder& payload);
252
263 [[nodiscard]] stdx::expected<void, std::string> publish_death();
264
280 [[nodiscard]] stdx::expected<void, std::string> rebirth();
281
289 [[nodiscard]] uint64_t get_seq() const {
290 std::scoped_lock lock(mutex_);
291 return seq_num_;
292 }
293
301 [[nodiscard]] uint64_t get_bd_seq() const {
302 std::scoped_lock lock(mutex_);
303 return bd_seq_num_;
304 }
305
315 [[nodiscard]] bool is_primary_host_online() const {
316 std::scoped_lock lock(mutex_);
317 return primary_host_online_;
318 }
319
338 [[nodiscard]] stdx::expected<void, std::string>
339 publish_device_birth(std::string_view device_id, PayloadBuilder& payload);
340
360 [[nodiscard]] stdx::expected<void, std::string>
361 publish_device_data(std::string_view device_id, PayloadBuilder& payload);
362
374 [[nodiscard]] stdx::expected<void, std::string>
375 publish_device_death(std::string_view device_id);
376
402 [[nodiscard]] stdx::expected<void, std::string>
403 publish_node_command(std::string_view target_edge_node_id, PayloadBuilder& payload);
404
423 [[nodiscard]] stdx::expected<void, std::string>
424 publish_device_command(std::string_view target_edge_node_id,
425 std::string_view target_device_id,
426 PayloadBuilder& payload);
427
428 void log(LogLevel level, std::string_view message) const noexcept;
429
430private:
434 struct DeviceState {
435 std::vector<uint8_t> last_birth_payload; // Last DBIRTH for rebirth
436 bool is_online{false}; // True if DBIRTH sent and device online
437 };
438
439 Config config_;
440 MQTTAsyncHandle client_;
441 uint64_t seq_num_{0}; // Node message sequence (0-255)
442 uint64_t bd_seq_num_{0}; // Birth/Death sequence
443
444 // Store the NDEATH payload for the MQTT Will
445 std::vector<uint8_t> death_payload_data_;
446 std::string death_topic_str_; // Topic string for MQTT Will (must outlive async connect)
447 MQTTAsync_willOptions will_opts_; // Will options struct (must outlive async connect)
448 MQTTAsync_SSLOptions ssl_opts_{};
449
450 // Secrets adopted from Config into scrub-on-free buffers so plaintext does
451 // not linger in std::string heap (CWE-316).
452 detail::SecureBuffer password_;
453 detail::SecureBuffer tls_key_password_;
454
455 // Store last NBIRTH for rebirth command
456 std::vector<uint8_t> last_birth_payload_;
457
458 // Hash and equality functors that support heterogeneous lookup (string_view)
459 struct StringHash {
460 using is_transparent = void;
461 [[nodiscard]] size_t operator()(std::string_view sv) const noexcept {
462 return std::hash<std::string_view>{}(sv);
463 }
464 };
465
466 struct StringEqual {
467 using is_transparent = void;
468 [[nodiscard]] bool operator()(std::string_view lhs,
469 std::string_view rhs) const noexcept {
470 return lhs == rhs;
471 }
472 };
473
474 // Track state of attached devices (device_id -> state, with heterogeneous lookup)
475 std::unordered_map<std::string, DeviceState, StringHash, StringEqual> device_states_;
476
477 std::atomic<bool> is_connected_{false};
478 std::atomic<bool> primary_host_online_{
479 false}; // True if primary host is online (or no primary host configured)
480
481 // Mutex for thread-safe access to all mutable state
482 mutable std::mutex mutex_;
483
484 // Caller must hold mutex_ (or be the constructor). Zeroes the Config
485 // string copies after adopting them into password_/tls_key_password_.
486 void adopt_secrets();
487
488 [[nodiscard]] static stdx::expected<void, std::string>
489 publish_message(MQTTAsync client,
490 const std::string& topic_str,
491 std::span<const uint8_t> payload_data,
492 int qos,
493 bool retain);
494
495 // Static MQTT callback for message arrived (NCMD)
496 static int on_message_arrived(void* context,
497 char* topicName,
498 int topicLen,
499 MQTTAsync_message* message);
500
501 // Static MQTT callback for connection lost
502 static void on_connection_lost(void* context, char* cause);
503};
504
505} // namespace sparkplug
Sparkplug B Edge Node implementing the complete message lifecycle.
Definition edge_node.hpp:97
stdx::expected< void, std::string > connect()
Connects to the MQTT broker and establishes a Sparkplug B session.
~EdgeNode()
Destroys the EdgeNode and cleans up MQTT resources.
void set_credentials(std::optional< std::string > username, std::optional< std::string > password)
Sets MQTT username and password for authentication.
void set_tls(std::optional< TlsOptions > tls)
Configures TLS/SSL options for secure MQTT connections.
stdx::expected< void, std::string > publish_node_command(std::string_view target_edge_node_id, PayloadBuilder &payload)
Publishes an NCMD (Node Command) message to another edge node.
stdx::expected< void, std::string > publish_death()
Publishes an NDEATH (Node Death) message.
stdx::expected< void, std::string > publish_device_death(std::string_view device_id)
Publishes a DDEATH (Device Death) message.
bool is_primary_host_online() const
Checks if the primary host application is online.
stdx::expected< void, std::string > publish_data(PayloadBuilder &payload)
Publishes an NDATA (Node Data) message.
uint64_t get_seq() const
Gets the current message sequence number.
stdx::expected< void, std::string > publish_device_data(std::string_view device_id, PayloadBuilder &payload)
Publishes a DDATA (Device Data) message.
stdx::expected< void, std::string > disconnect()
Gracefully disconnects from the MQTT broker.
stdx::expected< void, std::string > rebirth()
Triggers a rebirth by publishing a new NBIRTH with incremented bdSeq.
stdx::expected< void, std::string > publish_device_command(std::string_view target_edge_node_id, std::string_view target_device_id, PayloadBuilder &payload)
Publishes a DCMD (Device Command) message to a device on another edge node.
uint64_t get_bd_seq() const
Gets the current birth/death sequence number.
stdx::expected< void, std::string > publish_birth(PayloadBuilder &payload)
Publishes an NBIRTH (Node Birth) message.
EdgeNode(Config config)
Constructs an EdgeNode with the given configuration.
stdx::expected< void, std::string > publish_device_birth(std::string_view device_id, PayloadBuilder &payload)
Publishes a DBIRTH (Device Birth) message.
Type-safe builder for Sparkplug B payloads with automatic type detection.
Configuration parameters for the Sparkplug B Edge Node.
std::string client_id
Unique MQTT client identifier.
int keep_alive_interval
MQTT keep-alive interval in seconds (Sparkplug recommends 60)
bool clean_session
MQTT clean session flag.
int death_qos
MQTT QoS for NDEATH Will Message. Sparkplug requires 1.
std::string group_id
Sparkplug group ID (topic namespace)
std::optional< TlsOptions > tls
TLS/SSL options (required if broker_url uses ssl://)
int data_qos
MQTT QoS for data messages (NBIRTH/NDATA/DBIRTH/DDATA).
std::optional< std::string > password
MQTT password for authentication (optional)
std::optional< std::string > username
MQTT username for authentication (optional)
std::string broker_url
MQTT broker URL (e.g., "tcp://localhost:1883" or "ssl://localhost:8883")
std::string edge_node_id
Edge node identifier within the group.
size_t max_payload_bytes
Max accepted inbound payload size; larger messages are dropped (default 1 MiB, CWE-400)
TLS/SSL configuration options for secure MQTT connections.
std::string private_key_password
Password for encrypted private key (optional)
std::string private_key
Path to client private key file (PEM format, optional)
std::string trust_store
Path to CA certificate file (PEM format)
bool verify_hostname
Verify certificate identity matches broker_url host (default: true)
std::string key_store
Path to client certificate file (PEM format, optional)
bool enable_server_cert_auth
Verify server certificate (default: true)
std::string enabled_cipher_suites
Colon-separated list of cipher suites (optional)