
SConnector C++ library
v1.0.0
Table of contents
- Overview
- SAPIENT support matrix
- Versions
- Library files
- SConnector class description
- Data model
- Limitations
- Example
- SConnectorEdgeNodeTemplate
- Testing against the DMM Simulator
- Build and connect to your project
Overview
The SConnector library implements the SAPIENT edge-node interface (UK MOD / Dstl, BSI Flex 335 v2.0). It lets a sensor edge node (for example a camera) connect to a SAPIENT fusion node, register its capabilities and tasking contract, stream status and detections, and receive and acknowledge tasking commands. The library is self-managing: a single call to init() validates the configuration and starts an internal worker thread that connects, registers, keeps the TCP link alive (reconnecting as needed), sends StatusReports on a timer, delivers incoming Tasks to a user callback and acknowledges them automatically. The library built with C++17 and is cross-platform (Linux and Windows) — the connector uses raw sockets internally, with no external transport library. SAPIENT messages are serialised with nanopb (supplied as source under 3rdparty/, under the zlib license), so the default build has no system dependencies. An optional TLS transport, built on top of a vendored copy of Mbed TLS (also under 3rdparty/, Apache-2.0), is available behind a CMake flag — even with TLS enabled the library has no system dependencies.
SAPIENT support matrix
The tables below list the SAPIENT edge-node capabilities defined by BSI Flex 335 v2.0 and how each is covered by SConnector. Legend: ✅ supported, ⚠️ partial, ❌ not supported.
Table 1 - Messages and protocol features.
| SAPIENT capability | Direction | SConnector | Notes |
|---|---|---|---|
Registration message | edge → fusion | ✅ | Full mandatory tree + advertised command contract. |
RegistrationAck handling | fusion → edge | ✅ | Drives the Active state; rejection triggers reconnect. |
StatusReport (periodic heartbeat) | edge → fusion | ✅ | Sent on a timer; updatable at runtime. |
StatusReport SYSTEM_GOODBYE | edge → fusion | ✅ | Sent automatically on a clean stop(), so the fusion node releases the node ID at once. |
DetectionReport | edge → fusion | ✅ | Sent on demand. |
Alert | edge → fusion | ✅ | Sent on demand. |
AlertAck handling | fusion → edge | ⚠️ | Decoded; no automatic action taken. |
Task (incoming command) | fusion → edge | ✅ | All command types decoded and delivered to the callback. |
TaskAck | edge → fusion | ✅ | Sent automatically from the callback’s decision. |
Error message | both | ✅ | Inbound decoded and delivered to the error callback; not auto-generated for malformed input. |
| TCP transport + length-prefix framing | — | ✅ | 4-byte little-endian length prefix + protobuf. |
| Automatic connect / reconnect (FSM) | — | ✅ | Disconnected → Connecting → Registering → Active. |
| TLS / mutual TLS | — | ✅ | Optional (vendored Mbed TLS, <PARENT>_SCONNECTOR_WITH_TLS). |
| UUID (v4) / ULID identifier generation | — | ✅ | Strict format (lowercase UUID v4, uppercase ULID). |
| Sensor edge-node role (TCP client) | — | ✅ | Primary use case. |
| Effector tasking | fusion → edge | ⚠️ | Commands decoded; effector behaviour is application-level. |
| Fusion-node role (server) | — | ❌ | Out of scope — this is an edge-node library. |
| Hierarchical fusion (child/parent nodes) | — | ❌ | dependent_nodes / reporting_region not modelled. |
| UDP / other bearers | — | ❌ | SAPIENT is carried over TCP in practice. |
Table 2 - Message content coverage (fields modelled by the C++ data model).
| Message | Supported fields | Not modelled yet |
|---|---|---|
Registration | node definition (type, sub-types), capabilities, status definition (interval), modes (name/type/settle time, task with concurrent-tasks → region definition, command list), configuration data | per-mode detection definitions, class/behaviour filters, performance values, scan/tracking types, dependent nodes, reporting region, taxonomy |
StatusReport | report id, system state, info state, mode, active task id, node location (geo), power level, free-form status entries | power source/status enums, field of view, coverage, obscuration |
DetectionReport | report id, object id (ULID), task id, location (geo) or range/bearing, detection confidence, classification (type + confidence), behaviour, state, colour, object’s own id (e.g. tail number), track_info / object_info free-form attributes, RF signal, ENU velocity, predicted location, associated/derived detections, associated files | classification sub-class recursion |
Alert | alert id (user-overridable), alert type, status, priority, description, location (geo) or range/bearing, region id, ranking, confidence, additional information, associated files, associated detections | — |
Task (decode) | task id/name/description, start/end times, control (start/stop/pause), region geometry (id, type, area, name), REQUEST, MODE_CHANGE, detection/report-rate/classification thresholds, LOOK_AT (azimuth/elevation or geo), MOVE_TO/PATROL (waypoints), FOLLOW (object id) | region class/behaviour filters, full range/bearing cone extents |
TaskAck | task id, status (accepted/rejected/completed/failed), reason | associated file |
The “not modelled” fields are valid SAPIENT but not yet surfaced in the C++ model; they can be added without changing the wire codec’s structure. The generated nanopb code already covers the complete BSI Flex 335 v2.0 schema, so missing fields are an API-surface limit, not a wire-format one.
Versions
Table 3 - Library versions.
| Version | Release date | What’s new |
|---|---|---|
| 1.0.0 | 27.05.2026 | First version. Edge-node connector for BSI Flex 335 v2.0. |
Library files
The library is supplied as source code only, as a CMake project. The repository structure is shown below:
CMakeLists.txt --------------- Top-level CMake file of the library.
README.md -------------------- This file.
3rdparty --------------------- Vendored dependencies.
CMakeLists.txt
nanopb ------------------- Protobuf runtime (zlib license).
mbedtls ------------------ Mbed TLS (Apache-2.0); built only with TLS on.
src -------------------------- Library source code.
CMakeLists.txt ----------- CMake file.
SConnector.h ------------- Public class declaration.
SConnector.cpp ----------- Public class implementation (FSM + worker).
SConnectorTypes.h -------- Public data model (config, messages, enums).
SConnectorVersion.h ------ Version header (generated).
SConnectorVersion.h.in --- Template for the version header.
sapient ------------------ Generated SAPIENT v2.0 message code (nanopb).
internal ----------------- Implementation detail (not public API):
ITransport.h --------- Transport abstraction.
TcpLink.h/.cpp ------- Plain TCP transport.
TlsLink.h/.cpp ------- TLS transport (Mbed TLS, optional).
FrameCodec.h/.cpp ---- Length-prefix framing and reassembly.
SapientCodec.h/.cpp -- C++ model <-> nanopb encode/decode.
Ulid.h/.cpp ---------- ULID generator.
Uuid.h/.cpp ---------- UUID generator.
test ------------------------- CTest suite (built standalone):
CMakeLists.txt ----------- CMake file.
TestUnit.cpp ------------- Unit tests: framing, ULID/UUID, codec, registration.
TestIntegration.cpp ------ End-to-end FSM over TCP against a mock fusion node.
TestTls.cpp -------------- End-to-end TLS handshake + registration (TLS build only).
SConnectorEdgeNodeTemplate --- Full example edge node (simulated camera head).
example ---------------------- Minimal example edge node (SConnectorExample).
SConnector class description
Class declaration
The SConnector class is declared in SConnector.h and lives in the cr::sapient namespace. It hides all networking and protobuf details behind a small public API; the user only ever deals with the plain C++ structs from SConnectorTypes.h.
namespace cr {
namespace sapient
{
class SConnector
{
public:
/// Decides the fate of an incoming task; drives the automatic TaskAck.
using TaskCallback = std::function<
TaskDecision(const SapientTask& task, std::string& reason)>;
/// Notified whenever the connection state changes.
using ConnectionStateCallback = std::function<void(ConnectionState state)>;
/// Notified when an Error message arrives from the fusion node.
using ErrorCallback = std::function<void(const ErrorInfo& error)>;
/// Get class version method.
static std::string getVersion();
/// Set incoming TASK commands callback.
void setTaskCallback(TaskCallback cb);
/// Set connection state change callback.
void setConnectionStateCallback(ConnectionStateCallback cb);
/// Set inbound Error message callback (diagnostics).
void setErrorCallback(ErrorCallback cb);
/// Initialization method.
bool init(const EdgeNodeConfig& config);
/// Stop connection.
void stop();
/// Get connection status.
ConnectionState getConnectionState() const;
/// Update status information.
void updateStatusReport(const StatusInfo& status);
/// Send detection report.
bool sendDetectionReport(const Detection& detection);
/// Send alert.
bool sendAlert(const Alert& alert);
};
}
}
getVersion method
The getVersion() method returns the library version string. Method declaration:
static std::string getVersion();
Returns: version string in the format "Major.Minor.Patch".
The method can be called without an SConnector instance. Example:
cout << "SConnector class version: " << SConnector::getVersion() << endl;
Console output:
SConnector class version: 1.0.0
init method
The init(…) method validates the configuration, tears down any previous session and starts the internal worker thread. Returns true when the parameters are valid and the worker started — it does not block until connected; the connection is attempted continuously in the background. Calling init() again applies a new configuration (useful when the deployment or registration content changes at runtime). Method declaration:
bool init(const EdgeNodeConfig& config);
| Parameter | Description |
|---|---|
| config | Edge-node configuration (fusion address, identity, registration). |
EdgeNodeConfig top-level fields (full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
fusionIp | std::string | Fusion node IP / host. Mandatory — init() rejects an empty value. |
fusionPort | int | Fusion node TCP port. Mandatory — must be > 0. |
nodeId | std::string | Node UUID v4. Generated by SConnector when left empty (a fixed value keeps the node identity across restarts). |
statusIntervalMs | int | Period between periodic StatusReports (default 1000 ms). |
reconnectIntervalMs | int | Pause between connect attempts when the link is down (default 2000 ms). |
connectTimeoutMs | int | Timeout for one TCP connect attempt (default 1000 ms). |
registrationTimeoutMs | int | Maximum wait for RegistrationAck before treating the session as failed (default 5000 ms). |
maxMessageSize | uint32_t | Inbound frame size cap in bytes — defence against oversized peers (default 1 MiB). |
tlsEnabled | bool | If true, wrap the link in TLS. Effective only in builds with <PARENT>_SCONNECTOR_WITH_TLS=ON. |
tlsCaFile | std::string | Path to a PEM bundle used to verify the fusion-node certificate (no system trust store). |
tlsCertFile | std::string | Optional client certificate (for mTLS). |
tlsKeyFile | std::string | Optional client private key (for mTLS). |
tlsVerifyPeer | bool | When true, verify the server certificate chain and hostname. |
registration | RegistrationInfo | Content of the Registration message sent on connect. See SConnectorTypes.h for the nested fields. |
initialStatus | StatusInfo | First StatusReport content (its mode must be non-empty). See SConnectorTypes.h for the nested fields. |
Returns: true if the parameters are valid and the worker started, otherwise false.
stop method
The stop() method stops the connector: disconnects from the fusion node and joins the worker thread. Called automatically by the destructor. If the node is registered when stop() is called, a final StatusReport with SystemStatus::Goodbye (SYSTEM_GOODBYE) is sent first, so the fusion node releases the node ID immediately instead of waiting for its own disconnect timeout. Method declaration:
void stop();
getConnectionState method
The getConnectionState() method returns the current connection state. The connector is registered and operational only in the ConnectionState::Active state. Method declaration:
ConnectionState getConnectionState() const;
Returns: current ConnectionState (Disconnected, Connecting, Registering or Active).
updateStatusReport method
The updateStatusReport(…) method replaces the StatusReport content. The new content is sent on the next worker cycle and used for all subsequent periodic reports. Method declaration:
void updateStatusReport(const StatusInfo& status);
| Parameter | Description |
|---|---|
| status | New status report content. |
StatusInfo top-level fields (full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
system | SystemStatus | Overall node health: Ok / Warning / Error. Goodbye is set by the library on a clean stop(); the user normally leaves Ok. |
info | InfoStatus | Whether the report content is New or Unchanged since the last report. |
mode | std::string | Current operating mode name. Mandatory non-empty — strict fusion nodes reject a StatusReport without it. |
activeTaskId | std::string | ULID of the task currently being executed (optional). |
location | std::optional<GeoPoint> | Current node geographic position (lon/lat/alt). |
fieldOfView | std::optional<FieldOfView> | Current sensor field of view (cone: pointing direction + horizontal/vertical extents + range). |
power | std::optional<PowerInfo> | Power-supply state (battery level 0..100). |
values | std::vector<StatusValue> | Repeated free-form {level, type, value} entries for anything the standard doesn’t model directly (PTZ position, sensor flags, …). |
sendDetectionReport method
The sendDetectionReport(…) method sends a DetectionReport immediately. The objectId is filled with a fresh ULID if left empty. Method declaration:
bool sendDetectionReport(const Detection& detection);
| Parameter | Description |
|---|---|
| detection | Detection content. |
Detection top-level fields (full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
objectId | std::string | ULID of the detected object. Stable across reports of the same object — use the same id while tracking. Generated by the library if empty. |
taskId | std::string | ULID of the task this detection relates to (e.g. an active FOLLOW). Optional. |
location | std::optional<GeoPoint> | Object position in geographic coordinates. One of location / rangeBearing is required (sendDetectionReport() rejects when both unset). |
rangeBearing | std::optional<Bearing> | Object position in the sensor’s range/bearing frame (alternative to location). |
confidence | std::optional<float> | Detection confidence in [0..1]. |
classification | std::vector<Classification> | Class hypotheses (type + confidence) — repeated; one per candidate class. |
behaviour | std::vector<Behaviour> | Behaviour hypotheses (type + confidence) — e.g. "loitering". |
state | std::string | Free-form state, e.g. "tracking" / "lost". |
colour | std::string | Object colour (optional). |
id | std::string | The object’s own identifier (e.g. an aircraft tail number) — distinct from objectId. |
trackInfo | std::vector<ObjectInfo> | Repeated {type, value, error} free-form attributes mapped to the proto’s track_info[]. |
objectInfo | std::vector<ObjectInfo> | Repeated {type, value, error} free-form attributes for data the standard does not model (e.g. pixel coordinates, RCS, ML features). |
signal | std::vector<SignalInfo> | RF signal characteristics (amplitude, frequencies, pulse duration). |
associatedFile | std::vector<AssociatedFile> | URLs of external files related to this detection (image, audio, …). |
associatedDetection | std::vector<AssociatedDetection> | Cross-references to detections on other nodes (with parent/child/sibling relation). |
derivedDetection | std::vector<AssociatedDetection> | Detections this one was derived from (e.g. a fused track from per-sensor detections). |
velocity | std::optional<VelocityENU> | Object velocity vector in the ENU frame. |
prediction | std::optional<PredictedLocation> | Where the object is expected to be at a future timestamp. |
Returns: true if the message was written to the socket (requires the Active state), otherwise false.
sendAlert method
The sendAlert(…) method sends an Alert immediately. Method declaration:
bool sendAlert(const Alert& alert);
| Parameter | Description |
|---|---|
| alert | Alert content. |
Alert top-level fields (full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
alertId | std::string | ULID of the alert. Generated by the library if empty; supply your own to correlate alerts with external state. |
alertType | std::optional<AlertType> | Severity class: Information / Warning / Critical / Error / Fatal / ModeChange. |
status | AlertStatusType | Alert state: Active (default) / Acknowledge / Reject / Ignore / Clear. |
priority | std::optional<AlertPriority> | Discrete priority: Low / Medium / High. |
description | std::string | Human-readable description, typically shown on a fusion-node GUI. |
location | std::optional<GeoPoint> | Alert position in geographic coordinates. One of location / rangeBearing is required (sendAlert() rejects when both unset). |
rangeBearing | std::optional<Bearing> | Alert position in the sensor’s range/bearing frame (alternative to location). |
regionId | std::string | ULID of the region this alert is from (optional). |
ranking | std::optional<float> | Alert ranking in [0..1]. |
confidence | std::optional<float> | Confidence that the alert is not a false alarm, in [0..1]. |
additionalInformation | std::string | Free-form additional note (optional). |
associatedFile | std::vector<AssociatedFile> | URLs of related files (image, audio, …). |
associatedDetection | std::vector<AssociatedDetection> | Detections this alert is associated with (parent/child/sibling). |
Returns: true if the message was written to the socket (requires the Active state), otherwise false.
setTaskCallback method
The setTaskCallback(…) method sets the callback invoked for each incoming Task. The callback runs on the internal worker thread; the TaskDecision it returns drives the automatic TaskAck the library sends back. Populate the reason argument to explain a rejection. Method declaration:
void setTaskCallback(TaskCallback cb);
| Parameter | Description |
|---|---|
| cb | TaskDecision(const SapientTask& task, std::string& reason). |
SapientTask top-level fields (the value delivered to the callback; full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
taskId | std::string | ULID assigned by the fusion node; echo it in Detection::taskId while you act on the task. |
taskName | std::string | Human-readable task name. |
taskDescription | std::string | Free-form description. |
taskStartTimeSec | std::optional<double> | Requested start time (UTC seconds since epoch). |
taskEndTimeSec | std::optional<double> | Requested end time (UTC seconds since epoch). |
control | TaskControl | Lifecycle action: Start / Stop / Pause. A Stop for a running task means cancel it. |
commandType | TaskCommandType | Which command the task carries — selects which payload field below is populated. |
request | std::string | Set when commandType == Request: vendor-specific verb (e.g. "FIRE", "DAY_ZOOM_IN"). |
modeChange | std::string | Set when commandType == ModeChange: target mode name. |
commandParameter | std::string | Generic parameter string accompanying any command (e.g. "x,y" pixel coords). |
threshold | DiscreteThreshold | Set when commandType ∈ {DetectionThreshold, DetectionReportRate, ClassificationThreshold}: Low / Medium / High. |
lookAt | LookAtTarget | Set when commandType == LookAt: target as geographic location or range/bearing. |
locations | std::vector<GeoPoint> | Set when commandType ∈ {MoveTo, Patrol}: waypoint list. |
followObjectId | std::string | Set when commandType == Follow: ULID of the object to track (the same objectId your detector previously reported). |
region | std::vector<TaskRegion> | Optional geo-fence regions attached to the task (id, type, area polygon, name). Filters inside each region are not surfaced. |
setConnectionStateCallback method
The setConnectionStateCallback(…) method sets the callback invoked whenever the connection state changes. Method declaration:
void setConnectionStateCallback(ConnectionStateCallback cb);
| Parameter | Description |
|---|---|
| cb | void(ConnectionState state). |
setErrorCallback method
The setErrorCallback(…) method sets the callback invoked when an Error message is received from the fusion node — useful for diagnosing rejected messages. Method declaration:
void setErrorCallback(ErrorCallback cb);
| Parameter | Description |
|---|---|
| cb | void(const ErrorInfo& error). |
ErrorInfo top-level fields (the value delivered to the callback; full declaration in src/SConnectorTypes.h):
| Field | Type | Purpose |
|---|---|---|
messages | std::vector<std::string> | One or more diagnostic messages from the fusion node explaining what it could not accept (e.g. mandatory-field rejections). |
Data model
All message types are plain C++ structs declared in SConnectorTypes.h; the generated nanopb types stay hidden inside the library.
| Type | Purpose |
|---|---|
EdgeNodeConfig | Passed to init(): fusion address/port, node UUID, intervals, TLS settings, registration content, initial status. |
RegistrationInfo | Node identity, capabilities, status interval, modes (with the declared command contract) and configuration data. |
RegistrationMode | One operating mode; commands advertises the accepted commands (CommandDef). |
StatusInfo | StatusReport content: system/info state, mode, active task id, location, power, free-form StatusValue entries. |
Detection | DetectionReport content: object/task ids, position (location or rangeBearing), confidence, classification, behaviour, free-form trackInfo/objectInfo (e.g. pixel coordinates), ENU velocity, prediction, RF signal, associated/derived detections, files, colour, id. |
Alert | Alert content: alert id (user-overridable), alert type, status, priority, description, position (location or rangeBearing), region id, ranking, confidence, additional info, associated files/detections. |
SapientTask | Incoming command from the fusion node: ids, start/end times, control, geo-fence regions, commandType plus the matching payload (request, modeChange, threshold, lookAt, locations, followObjectId). |
IncomingMessage | Generic decoded inbound message (used internally for acks/errors). |
The connection state, task decision and SAPIENT enums (ConnectionState, TaskDecision, TaskCommandType, NodeType, etc.) are also declared here.
Limitations
SConnector covers the edge-node side of BSI Flex 335 v2.0 end-to-end and is accepted as a full SAPIENT node by the official Dstl DMM Simulator. The wire codec is generated from the complete v2.0 schema, so every field that is not modelled below is an API-surface limitation, not a wire-format one — adding it is a small extension to SapientCodec and SConnectorTypes.h.
Architectural scope
- Edge-node role only. The library implements the edge (ASM) side: connect → register → status / detection / alert / task. The fusion-node role (accepting connections from other nodes, sending tasks, validating registrations) is out of scope.
- No hierarchical fusion. A SAPIENT fusion node may register child nodes via
dependent_nodes/reporting_region; SConnector does not surface these. A child node connecting to an SConnector-based parent is not possible. - Plain TCP transport. SAPIENT is bearer-agnostic but the only deployed bearer is TCP, and that is what SConnector implements (with optional TLS via vendored Mbed TLS). UDP and other transports are not provided.
SAPIENT message fields not yet surfaced
| Message | Not modelled | Workaround |
|---|---|---|
Registration | dependent_nodes, reporting_region, per-mode detection_definition, scan/tracking types, performance values, taxonomy | None — purely additive when needed. |
StatusReport | coverage[] / obscuration[] repeated areas; Power.source / Power.status enums (only the level is exposed) | field_of_view (single cone) is supported via StatusInfo::fieldOfView. |
DetectionReport | Recursive Classification.sub_class[] hierarchy | Use flat classification[]; embed depth hints in the type string if needed. |
Alert | — | Full coverage. |
Task (inbound) | Task.Region.classFilter / behaviourFilter | Region geometry (id / type / area / name) is decoded; filter strings can be added on request. |
AlertAck (inbound) | The library decodes the message but does not yet expose it through a public callback. | No application-level access today; a setAlertAckCallback entry point will be added when there is a concrete need. |
Build and platform
- C++17 is required (uses
std::optional, structured bindings). - TLS uses the vendored Mbed TLS (no system OpenSSL). There is no system trust store: with
tlsVerifyPeer = trueyou must supply atlsCaFileexplicitly. - The integration test (
SConnectorTestIntegration) is POSIX-only — it builds a mock fusion node over raw sockets. On Windows it is a no-op stub. The library itself (TcpLink/TlsLink) runs on both Linux and Windows.
Concurrency model
init()andstop()are serialised against each other by an internal mutex (safe to call from any thread). The destructor force-closes the link.- The task / connection-state / error callbacks are invoked on the library’s worker thread; do not block them for long.
- Calling
sendDetectionReport/sendAlert/updateStatusReportconcurrently withinit()/stop()is supported but the sends will returnfalseonce the link is being torn down. The library is one worker thread perSConnectorinstance — there is no shared thread pool.
API contracts to be aware of
sendDetectionReport()andsendAlert()returnfalseif bothlocationandrangeBearingare unset — SAPIENT mandates a position on both messages.StatusInfo::modemust be non-empty (a strict fusion node rejects aStatusReportwithout a mode).RegistrationMode::settleTimeSecandconcurrentTasksmust be> 0and>= 1respectively (proto3 presence: zero is treated as “unset”).
Example
The smallest complete edge node built on SConnector lives in example/ and is built as the SConnectorExample target. In under 90 lines it connects to a fusion node, registers as a video camera, prints the name of every incoming Task, and sends a DetectionReport and an Alert once per second — the kind of minimal node the DMM Simulator accepts as a full edge node.
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <thread>
#include <iostream>
#include "SConnector.h"
// Link namespaces.
using namespace std;
using namespace std::chrono;
using namespace cr::sapient;
// Entry point.
int main(int argc, char** argv)
{
// SConnector object.
SConnector conn;
// Set Task callback. The fusion node sends Tasks to command the edge node;
// Print the name of every Task the fusion node sends, and accept it.
conn.setTaskCallback(
[](const SapientTask& task, std::string&) -> TaskDecision
{
cout << "[task] " << task.taskName << endl;
return TaskDecision::Accepted;
});
// Minimal configuration: an address, a name and a non-empty status mode.
// SConnector fills the rest of the mandatory registration tree
// (capabilities, mode, region, config data) with sensible defaults.
EdgeNodeConfig cfg;
cfg.fusionIp = "172.23.144.1"; // Put your fusion node IP.
cfg.fusionPort = 14000; // Put your fusion node port.
cfg.registration.name = "Example Camera";
cfg.registration.nodeType = NodeType::Camera;
cfg.initialStatus.mode = "default";
// Init SConnector with the minimal configuration.
if (!conn.init(cfg))
{
std::cerr << "init failed: invalid configuration" << std::endl;
return 1;
}
cout << "SConnector v" << SConnector::getVersion() << " initialized for " <<
cfg.fusionIp << ":" << cfg.fusionPort << " ..." << endl;
// A fixed site location shared by the detection and the alert.
const GeoPoint site{-1.2577, 51.7520, {}};
// Once a second, while registered, send one DetectionReport and one Alert.
while (true)
{
// Each second (for tests) send detection + alert to fusion node..
this_thread::sleep_for(seconds(1));
// Check connection.
if (conn.getConnectionState() != ConnectionState::Active)
continue;
// Send detection results.
Detection detection;
detection.location = site;
detection.confidence = 0.9f;
detection.classification.push_back({"vehicle", 0.8f});
conn.sendDetectionReport(detection);
// Send alert about the detection.
Alert alert;
alert.description = "Object detected";
alert.priority = AlertPriority::Medium;
alert.location = site;
conn.sendAlert(alert);
cout << "[sent] detection + alert\n";
}
return 0;
}
The whole node fits into three steps:
- A task callback — invoked on SConnector’s worker thread for every
Taskthe fusion node sends; here it just prints the task name and accepts it (returnTaskDecision::Rejectedwith a reason to refuse a command). - Minimal configuration — only the fusion address, a node name and a non-empty status
modeare set. SConnector fills the rest of the mandatory SAPIENT registration tree (capabilities, an operating mode with its region, and config data) with sensible defaults, so this minimal node still registers with a strict fusion node. - Periodic reporting — once a second, while the link is
Active, it sends oneDetectionReportand oneAlert.
That is the entire public surface needed to get a node on the bus: setTaskCallback(), init(), getConnectionState(), sendDetectionReport(), sendAlert() and stop().
A standalone build produces ./build/bin/SConnectorExample. The fusion address is set in main.cpp (cfg.fusionIp / cfg.fusionPort); port 14000 is the DMM Simulator’s Data Agent port, so the example points at the test harness out of the box (see Testing against the DMM Simulator). The example loops indefinitely and is meant to be stopped with SIGTERM / Ctrl-C; for a graceful SYSTEM_GOODBYE on shutdown, wire a signal handler that calls conn.stop() (see SConnectorEdgeNodeTemplate for a full lifecycle example).
./build/bin/SConnectorExample
SConnectorEdgeNodeTemplate
SConnectorEdgeNodeTemplate/ is a complete, worked example edge node built on the public SConnector API. It is both a usage template to copy from and a manual integration test against a fusion node.
What it simulates
The template simulates (it does not render any image) a pan/tilt camera sensor head consisting of:
- Day camera — controllable zoom (0..65535 units) and focus (0..65535 units). The field of view is derived from the zoom: 20°..1° horizontally and 12°..0.5° vertically (wide at zoom 0, telephoto at zoom 65535).
- Thermal camera — identical zoom/focus/field-of-view behaviour.
- Pan/tilt platform — carries both cameras; pan wraps to [-180°, 180°), tilt clamps to [-90°, 90°], with a commanded speed of up to 200°/s.
- Video tracker — state only (no real tracking):
FREEorTRACKING, with an X/Y target in a FullHD (1920×1080) frame. A capture latches the target and switches toTRACKING; a reset returns toFREE. - Object detector —
ON/OFF; whileONit emits a batch of detections with random coordinates roughly once per second, and raises an Alert when an object is detected with high confidence.
How it works
- Registration. On start, the template fills an
EdgeNodeConfigthat declares the node as aCamera, lists itscapabilities(day camera, thermal camera, pan/tilt platform, video tracker, object detector) and advertises its command contract (RegistrationMode::commands). The fusion node reads this contract to discover which commands the head accepts. - Connection.
init()starts the worker thread, which connects to the fusion node, registers and reaches theActivestate. State changes are printed via the connection-state callback. - Commands. Incoming
Tasks are delivered to the task callback, which applies them to the simulation and returnsAccepted/Rejected— the library sends the matchingTaskAckautomatically. Standard SAPIENT command types are honoured (see below); commands meant for mobile nodes (MOVE_TO/PATROL/FOLLOW) are rejected because the head is fixed. - Reporting. A loop advances the simulation (zoom/focus drives, pan/tilt motion) every 50 ms. Once per second it refreshes the
StatusReportwith the current device state (zoom, focus, field of view, pan, tilt, tracker mode, detector state) viaupdateStatusReport(), and — while the detector is ON — sendsDetectionReports for the random objects. When a detection exceeds a high-confidence threshold the template also raises anAlertviasendAlert().
The template exercises the entire public SConnector API, including getVersion() (printed at startup), init()/stop(), getConnectionState(), updateStatusReport(), sendDetectionReport(), sendAlert() and both callbacks.
Command vocabulary
The head maps to standard SAPIENT command types where they exist, and uses the generic REQUEST channel for camera-specific verbs that have no standard equivalent:
| SAPIENT command type | Used for |
|---|---|
LOOK_AT | Aim the platform at an azimuth/elevation (or location). |
DETECTION_REPORT_RATE | Detector reporting rate (turns the detector on). |
DETECTION_THRESHOLD | Detector sensitivity. |
CLASSIFICATION_THRESHOLD | Classification sensitivity. |
MODE_CHANGE | Switch the active sensing mode. |
REQUEST | Zoom / focus / continuous PTZ speed / tracker / detector verbs. |
The REQUEST verbs are carried in Task.command.request plus Task.command_parameter. With <CAM> standing for DAY or THERMAL:
<CAM>_ZOOM_IN | <CAM>_ZOOM_OUT | <CAM>_ZOOM_STOP
<CAM>_ZOOM_TO_POSITION param: 0..65535
<CAM>_ZOOM_TO_FOV param: horizontal degrees (1..20)
<CAM>_FOCUS_FAR | <CAM>_FOCUS_NEAR | <CAM>_FOCUS_STOP
<CAM>_FOCUS_TO_POSITION param: 0..65535
PLATFORM_MOVE_PAN_SPEED param: -200..200 (deg/s)
PLATFORM_MOVE_TILT_SPEED param: -200..200 (deg/s)
PLATFORM_MOVE_TO_HORIZONTAL param: degrees
PLATFORM_MOVE_TO_VERTICAL param: degrees
PLATFORM_STOP
TRACKER_RESET
TRACKER_CAPTURE param: "x,y" (FullHD pixels 0..1919,0..1079)
DETECTOR_ON | DETECTOR_OFF
Running it
./build/bin/SConnectorEdgeNodeTemplate <fusionIp> <fusionPort> [nodeId] # default 127.0.0.1 5000
nodeId is optional — if omitted, the template uses a fixed UUID so the fusion node tasks the same identity across restarts. A clean shutdown (Ctrl-C) sends SYSTEM_GOODBYE, which releases the ID at once, so re-running does not trip an “ID already in use” conflict. Pass an explicit nodeId to override it, or an empty string to have SConnector generate a unique one per run.
On startup the program prints the linked library version (SConnector v1.0.0). It then prints connection-state changes, received commands, detector activity, raised alerts and any errors from the fusion node, and runs until interrupted (Ctrl-C).
Testing against the DMM Simulator
The most practical conformance check is to run an edge node against the official Dstl test harness. Its DMM Simulator (Decision Making Module) plays the role of a SAPIENT fusion node: it accepts the edge node’s connection, validates every message against BSI Flex 335 v2.0, sends Tasks and reports any non-conformance with an Error. Reaching a steady exchange with no Error is a strong signal that the node is wire-compatible with the standard.
Links:
- DMM Simulator / test harness: https://github.com/dstl/BSI-Flex-335-v2-Test-Harness
- Dstl SAPIENT resources on GitHub: https://github.com/dstl
The edge node connects to the harness over plain TCP on the DMM Data Agent port, which is 14000 by default (the harness setting DACommunicationPort). Note that this differs from SConnectorEdgeNodeTemplate’s own default of 5000, so the port must be given explicitly.
- Start the DMM Simulator from the test harness (follow its own README).
-
Run the example node against the Data Agent port:
./build/bin/SConnectorEdgeNodeTemplate 127.0.0.1 14000 - Watch the DMM Simulator log. In order, you should see: the
Registrationaccepted (RegistrationAck { acceptance: true }), periodicStatusReports, theTasks the DMM sends (for exampleLOOK_AT) answered with aTaskAck, and — once the detector is tasked on —DetectionReports andAlerts. NoErrormessage means the node’s messages are accepted.
Notes:
- The template registers with a fixed node ID so the DMM tasks the same identity across restarts. On a clean Ctrl-C it sends
SYSTEM_GOODBYE, releasing the ID immediately; otherwise the harness holds it forDisconnectIntervalSecs(default 60 s) and a quick restart is refused with “Another ASM is using this ID”. Pass a differentnodeIdas the third argument to run a second node alongside the first. - The DMM addresses its
Tasks to the registered node viadestinationId; if you change the node ID, make sure the harness is tasking the same one. - TLS is not required for the harness — it speaks plain TCP on the Data Agent port. The library’s optional TLS transport is for deployments that secure the link to the fusion node.
Build and connect to your project
The library requires CMake ≥ 3.13 and a C++17 compiler.
cmake -S . -B build
cmake --build build -j
As a standalone project this also builds the tests and the SConnectorEdgeNodeTemplate and SConnectorExample programs. When SConnector is included as a submodule, all three are off by default.
CMake options:
| Option | Default | Effect |
|---|---|---|
<PARENT>_SCONNECTOR_WITH_TLS | OFF | Build the TLS transport on the vendored Mbed TLS (no system dependency). |
<PARENT>_SCONNECTOR_TEST | ON standalone | Build the CTest suite. |
<PARENT>_SCONNECTOR_TEMPLATE | ON standalone | Build the SConnectorEdgeNodeTemplate example. |
<PARENT>_SCONNECTOR_EXAMPLE | ON standalone | Build the minimal SConnectorExample. |
These options live in the CONFIGURATION block of the top-level CMakeLists.txt and follow the house convention: they are force-written to the cache (controlled by <PARENT>_SUBMODULE_CACHE_OVERWRITE, ON by default). <PARENT> is the namespace a parent project sets; in a standalone build it is empty, so the cache entries are _SCONNECTOR_WITH_TLS, _SCONNECTOR_TEST, etc.
Enabling TLS. Flip the flag to ON in the CONFIGURATION block of the top-level CMakeLists.txt, or have a parent project set it. Because the option is force-written in a standalone build, a plain -D is overridden on reconfigure; to override from the command line, also disable the forced rewrite:
cmake -S . -B build -D_SUBMODULE_CACHE_OVERWRITE=OFF -D_SCONNECTOR_WITH_TLS=ON
Tests:
The CTest suite has three programs (the CTest names are unit, integration, tls):
SConnectorTestUnit(CTest:unit) — pure-logic unit tests, no sockets, cross-platform. Verifies the length-prefix framing (4-byte little-endian prefix, reassembly across reads, oversize-frame rejection), the UUID v4 and ULID generators (format, bulk uniqueness, ULID monotonicity), and the SAPIENT codec: encoding ofStatusReport/DetectionReport/Alert/TaskAckand the full mandatoryRegistrationtree, plus decoding ofRegistrationAckandTask(REQUEST,LOOK_AT) and graceful handling of unsupported / garbage input.SConnectorTestIntegration(CTest:integration) — end-to-end test of the connection FSM over a real TCP loopback against a mock fusion node (POSIX only; a no-op on Windows). Verifies config validation, connect → register →Active, periodicStatusReports,sendDetectionReport/sendAlert, an inboundTaskreaching the callback with the matching automaticTaskAck, theSYSTEM_GOODBYEsent on a cleanstop(), the destructor forcibly closing the link (withSYSTEM_GOODBYE) when aSConnectorgoes out of scope without an explicitstop(), and reconnection after re-init().SConnectorTestTls(CTest:tls) — end-to-end TLS test, built only with TLS enabled (skipped on Windows). SConnector connects as a TLS client (vendored Mbed TLS) to a mock fusion node that terminates TLS with Mbed TLS and its built-in test certificate, and verifies the handshake, registration and reachingActiveover TLS.
cmake -S . -B build # TLS off: runs the unit + integration tests
cmake --build build -j
cd build && ctest --output-on-failure
Enable TLS as shown above to additionally build and run the tls test.
Use as a submodule: add the repository to your project (for example under a 3rdparty/ folder), then in your CMake:
add_subdirectory(SConnector)
target_link_libraries(YourTarget PUBLIC SConnector)
Linking the SConnector target brings in its public include path transitively. SConnector is licensed under Apache 2.0; the SAPIENT protobuf definitions are Crown Copyright (Apache 2.0), nanopb is under the zlib license, and the vendored Mbed TLS (used only when TLS is enabled) is under Apache 2.0.