sconnector_logo

SConnector C++ library

v1.0.0

Table of contents

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 = true you must supply a tlsCaFile explicitly.
  • 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() and stop() 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 / updateStatusReport concurrently with init() / stop() is supported but the sends will return false once the link is being torn down. The library is one worker thread per SConnector instance — there is no shared thread pool.

API contracts to be aware of

  • sendDetectionReport() and sendAlert() return false if both location and rangeBearing are unset — SAPIENT mandates a position on both messages.
  • StatusInfo::mode must be non-empty (a strict fusion node rejects a StatusReport without a mode).
  • RegistrationMode::settleTimeSec and concurrentTasks must be > 0 and >= 1 respectively (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:

  1. A task callback — invoked on SConnector’s worker thread for every Task the fusion node sends; here it just prints the task name and accepts it (return TaskDecision::Rejected with a reason to refuse a command).
  2. Minimal configuration — only the fusion address, a node name and a non-empty status mode are 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.
  3. Periodic reporting — once a second, while the link is Active, it sends one DetectionReport and one Alert.

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): FREE or TRACKING, with an X/Y target in a FullHD (1920×1080) frame. A capture latches the target and switches to TRACKING; a reset returns to FREE.
  • Object detectorON/OFF; while ON it 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

  1. Registration. On start, the template fills an EdgeNodeConfig that declares the node as a Camera, lists its capabilities (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.
  2. Connection. init() starts the worker thread, which connects to the fusion node, registers and reaches the Active state. State changes are printed via the connection-state callback.
  3. Commands. Incoming Tasks are delivered to the task callback, which applies them to the simulation and returns Accepted/Rejected — the library sends the matching TaskAck automatically. Standard SAPIENT command types are honoured (see below); commands meant for mobile nodes (MOVE_TO/PATROL/FOLLOW) are rejected because the head is fixed.
  4. Reporting. A loop advances the simulation (zoom/focus drives, pan/tilt motion) every 50 ms. Once per second it refreshes the StatusReport with the current device state (zoom, focus, field of view, pan, tilt, tracker mode, detector state) via updateStatusReport(), and — while the detector is ON — sends DetectionReports for the random objects. When a detection exceeds a high-confidence threshold the template also raises an Alert via sendAlert().

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:

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.

  1. Start the DMM Simulator from the test harness (follow its own README).
  2. Run the example node against the Data Agent port:

    ./build/bin/SConnectorEdgeNodeTemplate 127.0.0.1 14000
    
  3. Watch the DMM Simulator log. In order, you should see: the Registration accepted (RegistrationAck { acceptance: true }), periodic StatusReports, the Tasks the DMM sends (for example LOOK_AT) answered with a TaskAck, and — once the detector is tasked on — DetectionReports and Alerts. No Error message 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 for DisconnectIntervalSecs (default 60 s) and a quick restart is refused with “Another ASM is using this ID”. Pass a different nodeId as the third argument to run a second node alongside the first.
  • The DMM addresses its Tasks to the registered node via destinationId; 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 of StatusReport / DetectionReport / Alert / TaskAck and the full mandatory Registration tree, plus decoding of RegistrationAck and Task (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, periodic StatusReports, sendDetectionReport / sendAlert, an inbound Task reaching the callback with the matching automatic TaskAck, the SYSTEM_GOODBYE sent on a clean stop(), the destructor forcibly closing the link (with SYSTEM_GOODBYE) when a SConnector goes out of scope without an explicit stop(), 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 reaching Active over 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.


Table of contents