Sparkplug B C++ Library 1.0.0
Modern C++-23 implementation of Eclipse Sparkplug B 2.2 specification
Loading...
Searching...
No Matches
host_application.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 <memory>
14#include <mutex>
15#include <optional>
16#include <span>
17#include <string>
18#include <unordered_map>
19#include <utility>
20#include <vector>
21
22#include <MQTTAsync.h>
23
24namespace sparkplug {
25
33using MessageCallback =
34 std::function<void(const Topic&, const org::eclipse::tahu::protobuf::Payload&)>;
35
100public:
104 struct TlsOptions {
105 std::string trust_store;
106 std::string key_store;
107 std::string private_key;
109 std::string
112 bool verify_hostname = true;
113 };
114
118 struct DeviceState {
119 bool is_online{false};
120 uint64_t last_seq{255};
121 bool birth_received{false};
122 uint64_t offline_timestamp{0};
123 bool metrics_stale{false};
124 std::unordered_map<uint64_t, std::string>
126 };
127
132 using is_transparent = void;
133 using hash_type = std::hash<std::string_view>;
134 [[nodiscard]] size_t operator()(std::string_view str) const noexcept {
135 return hash_type{}(str);
136 }
137 [[nodiscard]] size_t operator()(const std::string& str) const noexcept {
138 return hash_type{}(str);
139 }
140 };
141
145 struct NodeState {
146 bool is_online{false};
147 uint64_t last_seq{255};
148 uint64_t bd_seq{0};
149 uint64_t birth_timestamp{0};
150 bool birth_received{false};
151 std::unordered_map<std::string, DeviceState, TransparentStringHash, std::equal_to<>>
153 std::unordered_map<uint64_t, std::string>
155 };
156
160 struct Config {
161 std::string broker_url;
163 std::string client_id;
164 std::string host_id;
165 int qos = 1;
167 true;
169 int max_inflight = 100;
172 true;
173 std::optional<TlsOptions>
174 tls{};
175 std::optional<std::string>
177 std::optional<std::string>
179 MessageCallback message_callback{};
180 LogCallback log_callback{};
181 size_t max_payload_bytes = 1 << 20;
182 size_t max_tracked_nodes = 4096;
183 size_t max_metrics_per_birth = 10000;
184 };
185
195
200
201 HostApplication(const HostApplication&) = delete;
202 HostApplication& operator=(const HostApplication&) = delete;
204 HostApplication& operator=(HostApplication&&) noexcept;
205
214 void set_credentials(std::optional<std::string> username,
215 std::optional<std::string> password);
216
225 void set_tls(std::optional<TlsOptions> tls);
226
234 void set_message_callback(MessageCallback callback);
235
243 void set_log_callback(LogCallback callback);
244
256 [[nodiscard]] stdx::expected<void, std::string> connect();
257
266 [[nodiscard]] stdx::expected<void, std::string> disconnect();
267
281 [[nodiscard]] stdx::expected<void, std::string> subscribe_all_groups();
282
294 [[nodiscard]] stdx::expected<void, std::string>
295 subscribe_group(std::string_view group_id);
296
309 [[nodiscard]] stdx::expected<void, std::string>
310 subscribe_node(std::string_view group_id, std::string_view edge_node_id);
311
325 [[nodiscard]] stdx::expected<void, std::string>
326 subscribe_state(std::string_view host_id);
327
335 bool is_online{false};
336 uint64_t last_seq{255};
337 uint64_t bd_seq{0};
338 uint64_t birth_timestamp{0};
339 bool birth_received{false};
340 };
341
352 [[nodiscard]] std::optional<NodeStateSnapshot>
353 get_node_state(std::string_view group_id, std::string_view edge_node_id) const;
354
371 [[nodiscard]] std::optional<std::string> get_metric_name(std::string_view group_id,
372 std::string_view edge_node_id,
373 std::string_view device_id,
374 uint64_t alias) const;
375
402 [[nodiscard]] stdx::expected<void, std::string> publish_state_birth(uint64_t timestamp);
403
431 [[nodiscard]] stdx::expected<void, std::string> publish_state_death(uint64_t timestamp);
432
460 [[nodiscard]] stdx::expected<void, std::string>
461 publish_node_command(std::string_view group_id,
462 std::string_view target_edge_node_id,
463 PayloadBuilder& payload);
464
486 [[nodiscard]] stdx::expected<void, std::string>
487 publish_device_command(std::string_view group_id,
488 std::string_view target_edge_node_id,
489 std::string_view target_device_id,
490 PayloadBuilder& payload);
491
498 void log(LogLevel level, std::string_view message) const noexcept;
499
500private:
501 Config config_;
502 MQTTAsyncHandle client_;
503 std::atomic<bool> is_connected_{false};
504
505 // MQTT connection options that must outlive async operations
506 MQTTAsync_SSLOptions ssl_opts_{};
507
508 // Secrets adopted from Config into scrub-on-free buffers so plaintext does
509 // not linger in std::string heap (CWE-316).
510 detail::SecureBuffer password_;
511 detail::SecureBuffer tls_key_password_;
512
513 // Node state tracking
514 struct NodeKey {
515 std::string group_id;
516 std::string edge_node_id;
517
518 [[nodiscard]] bool operator==(const NodeKey& other) const noexcept {
519 return group_id == other.group_id && edge_node_id == other.edge_node_id;
520 }
521 };
522
523 struct NodeKeyHash {
524 using is_transparent = void;
525 [[nodiscard]] size_t operator()(const NodeKey& key) const noexcept {
526 size_t h1 = std::hash<std::string>{}(key.group_id);
527 size_t h2 = std::hash<std::string>{}(key.edge_node_id);
528 return h1 ^ (h2 << 1);
529 }
530 [[nodiscard]] size_t
531 operator()(std::pair<std::string_view, std::string_view> key) const noexcept {
532 size_t h1 = std::hash<std::string_view>{}(key.first);
533 size_t h2 = std::hash<std::string_view>{}(key.second);
534 return h1 ^ (h2 << 1);
535 }
536 };
537
538 struct NodeKeyEqual {
539 using is_transparent = void;
540 [[nodiscard]] bool operator()(const NodeKey& lhs, const NodeKey& rhs) const noexcept {
541 return lhs == rhs;
542 }
543 [[nodiscard]] bool
544 operator()(const NodeKey& lhs,
545 std::pair<std::string_view, std::string_view> rhs) const noexcept {
546 return lhs.group_id == rhs.first && lhs.edge_node_id == rhs.second;
547 }
548 [[nodiscard]] bool operator()(std::pair<std::string_view, std::string_view> lhs,
549 const NodeKey& rhs) const noexcept {
550 return lhs.first == rhs.group_id && lhs.second == rhs.edge_node_id;
551 }
552 };
553
554 std::unordered_map<NodeKey, NodeState, NodeKeyHash, NodeKeyEqual> node_states_;
555 mutable std::mutex node_states_mutex_; // Protects node_states_ only
556
557 // State-tracking caps copied from Config at construction; written only under
558 // both mutexes (move), read under node_states_mutex_ in validate_message.
559 size_t max_tracked_nodes_{4096};
560 size_t max_metrics_per_birth_{10000};
561
562 // Mutex for thread-safe access to config and other mutable state
563 mutable std::mutex mutex_;
564
565 // Caller must hold mutex_ (or be the constructor). Zeroes the Config
566 // string copies after adopting them into password_/tls_key_password_.
567 void adopt_secrets();
568
569 [[nodiscard]] stdx::expected<void, std::string>
570 publish_raw_message(std::string_view topic,
571 std::span<const uint8_t> payload_data,
572 int qos,
573 bool retain);
574
575 [[nodiscard]] stdx::expected<void, std::string>
576 publish_command_message(std::string_view topic, std::span<const uint8_t> payload_data);
577
578 // Deferred logs: validate_message runs under node_states_mutex_ and must not
579 // invoke the log callback there (user loggers may call public getters that
580 // take the same mutex — deadlock, CWE-667). Caller flushes after unlock.
581 bool validate_message(const Topic& topic,
582 const org::eclipse::tahu::protobuf::Payload& payload,
583 std::vector<std::pair<LogLevel, std::string>>& deferred_logs);
584
585 // Static MQTT callback for message arrived
586 static int on_message_arrived(void* context,
587 char* topicName,
588 int topicLen,
589 MQTTAsync_message* message);
590
591 static void on_connection_lost(void* context, char* cause);
592};
593
594} // namespace sparkplug
Sparkplug B Host Application for SCADA/Primary Applications.
stdx::expected< void, std::string > subscribe_state(std::string_view host_id)
Subscribes to STATE messages from another primary application.
stdx::expected< void, std::string > publish_state_death(uint64_t timestamp)
Publishes a STATE death message to indicate Host Application is offline.
void log(LogLevel level, std::string_view message) const noexcept
Internal logging method accessible from C bindings.
std::optional< NodeStateSnapshot > get_node_state(std::string_view group_id, std::string_view edge_node_id) const
Gets the current state of a specific edge node.
void set_tls(std::optional< TlsOptions > tls)
Configures TLS/SSL options for secure MQTT connections.
void set_log_callback(LogCallback callback)
Sets the log callback for receiving library diagnostic messages.
stdx::expected< void, std::string > publish_device_command(std::string_view group_id, 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 an Edge Node.
stdx::expected< void, std::string > subscribe_all_groups()
Subscribes to all Sparkplug B messages across all groups.
stdx::expected< void, std::string > publish_node_command(std::string_view group_id, std::string_view target_edge_node_id, PayloadBuilder &payload)
Publishes an NCMD (Node Command) message to an Edge Node.
stdx::expected< void, std::string > disconnect()
Gracefully disconnects from the MQTT broker.
HostApplication(Config config)
Constructs a HostApplication with the given configuration.
stdx::expected< void, std::string > subscribe_group(std::string_view group_id)
Subscribes to messages from a specific group.
void set_message_callback(MessageCallback callback)
Sets the message callback for receiving Sparkplug messages.
stdx::expected< void, std::string > publish_state_birth(uint64_t timestamp)
Publishes a STATE birth message to indicate Host Application is online.
stdx::expected< void, std::string > connect()
Connects to the MQTT broker.
std::optional< std::string > get_metric_name(std::string_view group_id, std::string_view edge_node_id, std::string_view device_id, uint64_t alias) const
Resolves a metric alias to its name for a specific node or device.
void set_credentials(std::optional< std::string > username, std::optional< std::string > password)
Sets MQTT username and password for authentication.
~HostApplication()
Destroys the HostApplication and cleans up MQTT resources.
stdx::expected< void, std::string > subscribe_node(std::string_view group_id, std::string_view edge_node_id)
Subscribes to messages from a specific edge node in a group.
Type-safe builder for Sparkplug B payloads with automatic type detection.
Configuration parameters for the Sparkplug B Host Application.
std::optional< TlsOptions > tls
TLS/SSL options (required if broker_url uses ssl://)
std::optional< std::string > username
MQTT username for authentication (optional)
size_t max_payload_bytes
Max accepted inbound payload size; larger messages are dropped (default 1 MiB, CWE-400)
std::string host_id
Host Application identifier (for STATE messages)
size_t max_metrics_per_birth
Max aliases recorded per NBIRTH/DBIRTH (default 10000, CWE-400)
int max_inflight
Maximum number of QoS 1/2 messages allowed in-flight (default: 100, paho default: 10)
bool clean_session
MQTT clean session flag (should be true per Sparkplug spec)
LogCallback log_callback
Optional callback for library log messages.
std::optional< std::string > password
MQTT password for authentication (optional)
int qos
MQTT QoS for STATE messages and commands (default: 1)
size_t max_tracked_nodes
Max distinct edge nodes tracked; further nodes are ignored (default 4096, CWE-400)
MessageCallback message_callback
Callback for received Sparkplug messages.
std::string client_id
Unique MQTT client identifier.
int keep_alive_interval
MQTT keep-alive interval in seconds (default: 60)
bool validate_sequence
Enable sequence number validation (detects packet loss)
std::string broker_url
MQTT broker URL (e.g., "tcp://localhost:1883" or "ssl://localhost:8883")
Tracks the state of a device attached to an edge node.
bool is_online
True if DBIRTH received and device is online.
std::unordered_map< uint64_t, std::string > alias_map
Maps metric alias to name (from DBIRTH)
uint64_t offline_timestamp
Timestamp when device went offline (from DDEATH)
bool metrics_stale
True if metrics marked stale after DDEATH.
bool birth_received
True if DBIRTH has been received.
uint64_t last_seq
Last received device sequence number.
Lightweight snapshot of node state (scalars only, no maps).
Tracks the state of an individual edge node.
std::unordered_map< std::string, DeviceState, TransparentStringHash, std::equal_to<> > devices
Attached devices (device_id -> state)
std::unordered_map< uint64_t, std::string > alias_map
Maps metric alias to name (from NBIRTH)
uint64_t last_seq
Last received node sequence number (starts at 255)
uint64_t bd_seq
Current birth/death sequence number.
bool is_online
True if NBIRTH received and node is online.
bool birth_received
True if NBIRTH has been received.
uint64_t birth_timestamp
Timestamp of last NBIRTH.
TLS/SSL configuration options for secure MQTT connections.
bool verify_hostname
Verify certificate identity matches broker_url host (default: true)
bool enable_server_cert_auth
Verify server certificate (default: true)
std::string private_key
Path to client private key file (PEM format, optional)
std::string key_store
Path to client certificate file (PEM format, optional)
std::string enabled_cipher_suites
Colon-separated list of cipher suites (optional)
std::string trust_store
Path to CA certificate file (PEM format)
std::string private_key_password
Password for encrypted private key (optional)
Transparent hash for string keys to enable heterogeneous lookup.