Covers Engine 2.0.0
C++ Share Client
Exchange channels, commands, and ARGUS events between a flight computer and a ground engine.
The C++ Share client makes your application a named peer of a DARTWIC engine. A flight computer can report measurements to a ground engine, receive ground measurements, accept operator commands, and command channels owned by the ground engine over the same connection. It can also send and receive ARGUS messages, warnings, and errors. Its built-in TEMPEST transport handles registration, heartbeat detection, and automatic reconnect.
For custom radio, serial, or CAN delivery, use the same channel APIs with the Share Protocol SDK and a custom transport.
Link the client
Download C++ Share Client from Downloads. Its source archive includes TEMPEST and the Share Protocol SDK. Use CMake 3.20+, a C++20 compiler, cppzmq/ZeroMQ, nlohmann JSON, and platform threads. Configure dependencies through your toolchain or CMAKE_PREFIX_PATH, then build and install the extracted archive:
cmake -S . -B build -DBUILD_TESTING=OFF -DCMAKE_INSTALL_PREFIX=./installed
cmake --build build --config Release
cmake --install build --config ReleasePoint your application at that install prefix:
cmake_minimum_required(VERSION 3.20)
project(share_reader LANGUAGES CXX)
find_package(DARTWICShareClient CONFIG REQUIRED)
add_executable(share_reader main.cpp)
target_compile_features(share_reader PRIVATE cxx_std_20)
target_link_libraries(share_reader PRIVATE DARTWIC::ShareClient)The client library embeds the lightweight TEMPEST client and does not link the engine server. The bundle builds and installs its Share Protocol dependency with the client. A shared ZeroMQ build also requires its runtime library when your application starts.
Example: a flight computer connected to a ground engine
The example has two parts: Channels uses share.channels() for measurements and commands; Events adds share.argus() for ARGUS notifications on the same connection.
Channels
| Direction, from the flight computer | API | Example purpose |
|---|---|---|
| Send telemetry to the ground node | channels().publishTelemetry(...) | Report battery voltage for display and recording. |
| Receive telemetry from the ground node | ChannelHandlers::telemetry | Read a ground sensor such as ambient temperature. |
| Send a command to the ground node | channels().upsert(...) | Update a channel owned by the ground engine. |
| Receive a command from the ground node | ChannelHandlers::upsert | Apply an operator-requested telemetry rate in the flight application. |
Telemetry reports the source’s state without an acknowledgement. A command requests a change and waits for the receiving handler to complete. Implement incoming handlers before start() so Hello advertises the capabilities your application supports. One setHandlers call installs the complete handler bundle.
Set DARTWIC_PASSWORD, point host and port at the ground engine, and build this as main.cpp. This is a runnable application-state example: battery_voltage is simulated, and the rate command changes this program’s publication loop. Replace that state with your driver’s measurements and command handling when integrating hardware.
#include <dartwic/share/DARTWICShareClient.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <thread>
#include <type_traits>
using namespace DARTWIC::Share;
uint64_t nowNs() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
}
int main() {
try {
const char* password = std::getenv("DARTWIC_PASSWORD");
if (!password) throw std::runtime_error("Set DARTWIC_PASSWORD");
// State must outlive the client and its callbacks.
std::atomic<double> rateHz{2.0};
DARTWICShareClient share({
.node_name = "FLIGHT_COMPUTER", .host = "127.0.0.1",
.port = 7000, .password = password,
});
ChannelHandlers handlers;
// Receive commands from the ground node.
handlers.upsert = [&](const ChannelUpsert& command) {
if (command.owner_node != "FLIGHT_COMPUTER" ||
command.channel != "telemetry_rate_hz" || command.field != "value")
throw std::runtime_error("Unsupported flight-computer command");
const double rate = std::visit([](const auto& value) -> double {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_arithmetic_v<T> && !std::is_same_v<T, bool>)
return static_cast<double>(value);
throw std::runtime_error("Expected a numeric rate");
}, command.value.storage());
if (!std::isfinite(rate) || rate < 1 || rate > 20)
throw std::runtime_error("Example rate must be between 1 and 20 Hz");
rateHz.store(rate);
};
// Receive telemetry from the ground node.
handlers.telemetry = [](const ChannelTelemetry& message) {
if (message.kind != ChannelTelemetry::Kind::Upsert) return;
if (message.upsert.channel == "ambient_temperature") {
if (const auto* value = std::get_if<double>(&message.upsert.value.storage()))
std::cout << "Ground ambient temperature: " << *value << '\n';
}
};
// Expose current state for discovery and snapshots after reconnect.
handlers.query = [&](const ChannelQuery& request) {
ChannelQueryResult result;
Value::Object state = {{"battery_voltage", 12.4}, {"telemetry_rate_hz", rateHz.load()}};
for (const auto& [name, value] : state) {
if (!request.channels.empty() && std::find(request.channels.begin(),
request.channels.end(), name) == request.channels.end()) continue;
ChannelSnapshot snapshot;
snapshot.owner_node = "FLIGHT_COMPUTER";
snapshot.channel = name;
if (request.fields.empty() || std::find(request.fields.begin(),
request.fields.end(), "value") != request.fields.end())
snapshot.channel_data["value"] = value;
if (request.fields.empty() || std::find(request.fields.begin(),
request.fields.end(), "control_policy") != request.fields.end())
snapshot.channel_data["control_policy"] = name == "telemetry_rate_hz"
? "free" : "observe_only";
result.channels.push_back(std::move(snapshot));
}
return result;
};
share.channels().setHandlers(std::move(handlers));
share.start();
share.waitUntilReady(std::chrono::seconds(5));
// Send one command to a channel owned by the ground node.
try {
share.channels().upsert({.owner_node = share.remoteNode(),
.channel = "flight_link_demo", .value = 1.0});
} catch (const std::exception& error) {
std::cerr << "Ground command failed: " << error.what() << '\n';
}
// Send telemetry for 60 seconds, including after a transient reconnect.
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(60);
while (std::chrono::steady_clock::now() < end) {
if (share.protocolReady()) {
for (const auto& [name, value] : Value::Object{
{"battery_voltage", 12.4}, {"telemetry_rate_hz", rateHz.load()}}) {
if (name == "telemetry_rate_hz") {
// Advertise this channel as an operator command endpoint.
ChannelTelemetry policy;
policy.upsert.channel = name;
policy.upsert.field = "control_policy";
policy.upsert.value = "free";
share.channels().publishTelemetry(policy);
}
ChannelTelemetry sample;
sample.upsert.channel = name;
sample.upsert.value = value;
sample.upsert.timestamp = nowNs();
share.channels().publishTelemetry(sample);
}
}
std::this_thread::sleep_for(std::chrono::duration<double>(1.0 / rateHz.load()));
}
share.stop();
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}Send telemetry to the connected node
In the interface attached to the ground engine, open Channel Search and find FLIGHT_COMPUTER:battery_voltage and FLIGHT_COMPUTER:telemetry_rate_hz. The program publishes both repeatedly. Configure recording on the engine if you also want history.
publishTelemetry assigns omitted owner, session, and revision information from the local Share node. The engine receives this as remote state owned by FLIGHT_COMPUTER. It does not command the ground node’s local channel named battery_voltage.
For a driver that collects a batch of samples, publish them together for one channel:
ChannelTelemetry batch;
batch.kind = ChannelTelemetry::Kind::BulkUpsert;
batch.bulk.channel = "battery_voltage";
const auto timestamp = nowNs();
batch.bulk.samples = {{12.4, timestamp - 1'000'000}, {12.3, timestamp}};
share.channels().publishTelemetry(batch);These example samples are one millisecond apart. Supply your acquisition timestamps in real code. ChannelBulkUpsert contains samples for one channel; send other channels separately.
Receive telemetry from the connected node
Create or update ambient_temperature on the ground engine. With the channel included in the Share connection’s RAPID sharing settings, the flight application’s telemetry handler prints incoming upserts. The callback handles Upsert explicitly; also handle BulkUpsert, Remove, or fixed_generation when your peer publishes those forms.
The handler receives pushed state. channels().query(...) requests a snapshot when you need to read current state explicitly. Keep application storage if you want a persistent local view; the protocol SDK does not create a channel database for you.
Send a command to the connected node
The example sends flight_link_demo = 1 to the node named by share.remoteNode(). Look for that local channel on the ground engine. Set owner_node to the node that owns the target and pass the channel name separately. upsert waits for acknowledgement and throws on failure; engine command authority still applies.
Receive a command from the connected node
In the ground interface, set FLIGHT_COMPUTER:telemetry_rate_hz to a value between 1 and 20. The engine routes the command to the owning flight application. Its upsert handler validates the target and value, changes rateHz, and returns. Subsequent telemetry reports the accepted rate back to the ground node.
Remote channels default to observe_only. The example explicitly advertises control_policy = "free" for the rate channel in snapshots and telemetry so the ground operator can command it. The battery measurement stays observe-only. The loop republishes the rate policy after reconnect as well as its current value; choose an authority policy appropriate to your application when replacing this demonstration.
That return produces the protocol acknowledgement. Throwing reports a remote handler error. For actual hardware, distinguish accepting a command from observing its physical result: publish measured feedback separately. The example’s range is just its application policy.
Events
Use ARGUS events to tell the ground operator what happened: the flight application started, battery voltage crossed a warning threshold, or a sensor read failed. Receive ground-node events in the flight application to log or display messages from the other side of the link.
The following additions use the same share object and nowNs() helper as the Channels example. Keep the channel handlers; channels().setHandlers(...) and argus().setHandlers(...) configure separate API groups.
Receive ARGUS events from the ground node
Insert this before share.start(), alongside the channel-handler registration:
ArgusHandlers eventHandlers;
eventHandlers.telemetry = [](const ArgusEventTelemetry& update) {
const auto& event = update.event;
if (update.kind == ArgusEventTelemetry::Kind::Removed) {
std::cout << "Ground event removed: " << event.event_id << '\n';
return;
}
const char* change = update.kind == ArgusEventTelemetry::Kind::Created
? "created" : "updated";
std::cout << "Ground event " << change << " [" << event.type << "] "
<< event.title << ": " << event.description
<< " (status: " << event.status << ")\n";
};
share.argus().setHandlers(std::move(eventHandlers));This receives event creation, updates, and removal, including messages, warnings, and errors. With ARGUS sharing enabled for the connection, trigger a message or warning on the ground engine and watch the flight application’s output. These callbacks run on the same protocol callback thread as channel telemetry; keep them short and protect state shared with your main loop.
Registering the handler before startup advertises event-receive support in Hello. No per-event subscription call is required.
Send messages, warnings, and errors to the ground node
Send an informational event after waitUntilReady(...), once when the application starts:
share.argus().message({
.title = "Flight application ready",
.description = "The flight application is publishing telemetry.",
.timestamp = nowNs(),
});Call the warning helper when your application detects a condition worth investigating:
share.argus().warning({
.title = "Battery voltage low",
.description = "Battery voltage crossed the application's warning threshold.",
.timestamp = nowNs(),
.channels = {"|battery_voltage|"},
});channels contains ARGUS channel references, including the |...| delimiters. Here |battery_voltage| means a channel owned by the event’s source, FLIGHT_COMPUTER. When the ground engine receives the event, it automatically stores that reference as |FLIGHT_COMPUTER:battery_voltage|. You do not need to add your own node name when publishing a reference to your own channel.
An explicit node prefix identifies the owner, not “a node remote from the publisher.” For an event sent by FLIGHT_COMPUTER to an engine named GROUND:
| Reference sent by the flight application | Reference on the ground engine |
|---|---|
|battery_voltage| | |FLIGHT_COMPUTER:battery_voltage| |
|FLIGHT_COMPUTER:battery_voltage| | |FLIGHT_COMPUTER:battery_voltage| |
|GROUND:ambient_temperature| | |ambient_temperature| |
Bare strings such as battery_voltage do not undergo this ARGUS reference conversion. This syntax is specific to event references; channel commands and telemetry above use separate owner_node and channel fields.
Call the error helper when an operation fails:
share.argus().error({
.title = "Sensor read failed",
.description = "The flight application could not read the battery sensor.",
.timestamp = nowNs(),
.details = {{"sensor", "battery_voltage"}, {"reason", "read_timeout"}},
});These warning/error calls illustrate application conditions; the simulated channel example does not detect them automatically. Emit an event when the condition occurs rather than once per telemetry iteration. Each call with an omitted event_id creates a new ID. The helper sets the event type and fills an omitted owner with FLIGHT_COMPUTER.
details is an optional Value::Object field on ArgusEvent, accepted by all three helpers. It carries extra JSON-compatible metadata such as sensor and reason. The Share codec encodes these entries as extra event fields, and the receiving engine retains them in the ARGUS record’s payload; it does not require a nested wire field called details. Use the named event fields for standard properties such as title, type, and channels.
The engine interprets these event timestamps as Unix nanoseconds, so pass nowNs() directly, just as for channel samples. The received ARGUS record exposes the creation time as created_at_ns. Open the ground interface’s Events or Events Board and filter by the flight node or event title to find the received records.
Events across reconnect
Event publication is one-way. Returning from message, warning, or error does not acknowledge delivery; events published while disconnected are dropped and are not replayed automatically. Keep the receive handler installed while the transport reconnects. For current events already known to the ground engine, explicitly query after readiness returns:
auto warnings = share.argus().query({.type = "warning", .limit = 20});
for (const auto& event : warnings.events)
std::cout << event.event_id << ": " << event.title << '\n';The query waits for a response and can throw if the connection fails. It retrieves matching events held by the peer; it cannot recover events that never reached that peer. An external application that needs its own event store to be queryable implements ArgusHandlers::query. Incoming event actions, such as status changes or prompt responses, use ArgusHandlers::action separately from the telemetry callback.
See ArgusEvent, ArgusHandlers, and the Protocol API for the full event fields, queries, and actions.
Reconnect and application lifetime
The managed TEMPEST transport detects connection loss and retries with backoff. A new Hello restores protocol readiness; handlers remain installed. The example keeps running through a transient outage and resumes publishing current values once protocolReady() becomes true. The query handler also allows the engine to fetch a fresh snapshot.
Automatic reconnect does not replay commands or telemetry missed during the outage. A request timeout leaves its outcome uncertain; inspect state before retrying. A link can fail after a readiness check, so still handle request errors. waitUntilReady throws on startup failure/timeout; a longer startup retry policy belongs in your application.
State and telemetry callbacks run serially on the protocol’s callback thread. Incoming command/query handlers run in transport request handling, so protect state shared with your main loop and telemetry callbacks. Stop outside callbacks, keep callbacks short, and keep captured state alive until stop() completes.
See ShareClientConfig, DARTWICShareClient, and the inherited Protocol API for full signatures.