Interface control document
1. Identification
| Item | Value |
|---|---|
| Document | ICD-CMSONO-001 rev 1 |
| Product | SonoMarc module (Sonomarc Module in the module registry and in the settings section) |
| Product versions covered | 8347c3b |
| Peer | SonoMarc Core server, HTTP REST API and Server-Sent Events endpoint |
| Host | HEAT Capture, HEAT.Capture.ModuleSDK 1.0.153 |
| Data model | SimulationFrame carrying one EntityState per discovered seat, plus queued EntityEvents |
| Date | 2026-09-02 |
| Owner | VRAI Simulation |
2. Scope
This document defines the two interfaces the module holds with a SonoMarc Core server, the REST resources and JSON properties it reads, the event subscription it registers, and the components and events it publishes to HEAT Capture.
The module is read-only with respect to simulation state. It issues no REST call that changes seat, asset or session state. The only writes it performs are the two requests that create and populate its own event subscription.
3. Interface inventory
| ID | Name | Direction | Transport | Peer |
|---|---|---|---|---|
| IF-CMSONO-01 | Session, seat, asset and platform state | Request/response, module initiates | HTTP/1.1 REST over TCP | SonoMarc Core |
| IF-CMSONO-02 | Event subscription and event stream | Module registers, service pushes | HTTP POST and PUT, then a long-lived text/event-stream GET | SonoMarc Core |
| IF-CMSONO-03 | Frame publication | Module to host | Module SDK publication path | HEAT Capture |
4. IF-CMSONO-01 REST state interface
4.1 Transport and addressing
| Item | Value |
|---|---|
| Base address | http://{Host}:{Port}/api/, built in InitialiseModule from the Host and Port settings |
| Scheme | Plain HTTP. The scheme is a compile-time literal; TLS cannot be selected through settings. |
| Per-request timeout | 10 seconds (HttpClient.Timeout) |
| Request method | GET only on this interface |
| Path escaping | Session, platform and seat names are escaped with Uri.EscapeDataString in every polling call |
| Response format | JSON, parsed with System.Text.Json |
| Client lifetime | One HttpClient created in InitialiseModule and disposed in EndCapture |
4.2 Endpoints
Paths are relative to the base address above.
| Method | Path | Called | Purpose |
|---|---|---|---|
| GET | sessions | Repeatedly during StartCapture until a match is found | Find a session whose state is Running and whose name matches SessionName |
| GET | sessions/{session}/platforms | Once during StartCapture | Enumerate platform names |
| GET | sessions/{session}/platforms/{platform}/seats | Once per platform during StartCapture | Enumerate seats and their bindings |
| GET | sessions/{session}/platforms/{platform}/seats/{seat} | Once per seat, on the initial refresh and on every capture cycle | Seat state: malfunctions, SIP and call state, push-to-talk, squelch |
| GET | sessions/{session}/platforms/{platform}/seats/{seat}/assets | Once per seat, on the initial refresh and on every capture cycle | Radio, intercom and relay asset state |
| GET | sessions/{session}/platforms/{platform} | Once per seat, on the initial refresh and on every capture cycle, best effort | Platform callsign and geographic position source |
The platform request is issued once per seat rather than once per platform, so in WholeSession mode
the same platform resource is fetched as many times per cycle as it has seats.
4.3 Connection lifecycle
InitialiseModule only constructs the HTTP client; it performs no request and cannot fail on an
unreachable server.
StartCapture runs four steps in order: poll sessions until a running session is found, discover
platforms and seats, perform one full snapshot refresh so asset names are known, then register the
event subscription described in section 5. It returns false only when the session poll ends
without a session, which happens only on cancellation. Zero discovered seats is a warning, not a
failure: the module starts and publishes frames that contain no entities.
StartCapture is bounded by the SDK StartupTimeout, which this module does not override, so the
whole of that sequence must complete within 5 seconds. SessionPollRetryMs defaults to 5000, so if
the first sessions response contains no matching running session the retry delay alone exhausts the
startup budget and the module does not start. See section 4.7 for the consequence.
Each capture cycle re-polls every known seat. The seat set is fixed at StartCapture: a seat added
to the SonoMarc session later is never discovered, and a seat removed continues to be polled and to
log a refresh warning every cycle.
EndCapture unhooks the three receiver callbacks, stops the receiver, disposes the HTTP client, and
clears the active session, the entity-id map and the seat snapshots.
4.4 Authentication
None. No credential, token, API key or client certificate is sent on any request, and no setting
exists to supply one. Any host able to reach Host:Port can read the same resources.
4.5 JSON properties read
Only the properties listed here are read. Everything else in a response is ignored.
GET sessions:
| Property | Type | Presence | Use |
|---|---|---|---|
sessions[].name | string | Required; a missing property throws and the poll retries | Compared against SessionName case-insensitively, or accepted unconditionally when SessionName is blank; retained as the active session name |
sessions[].state | string | Required | Must equal Running, compared case-insensitively |
GET sessions/{session}/platforms:
| Property | Type | Presence | Use |
|---|---|---|---|
platforms[].name | string | Required | Platform half of the seat key, and the input to the LinkedEntityComponent parent id |
GET sessions/{session}/platforms/{platform}/seats:
| Property | Type | Presence | Use |
|---|---|---|---|
seats[].name | string | Required | Seat half of the seat key |
seats[].binding | string | Required | PerSeat filter only. A seat matches when its binding contains the configured SeatBinding, or the local hostname when that setting is empty. Never published. |
seats[].type | string | Required | Retained in the snapshot, never published |
seats[].volume | integer | Optional, defaults to 100 | Retained in the snapshot, never published |
GET sessions/{session}/platforms/{platform}/seats/{seat}:
| Property | Type | Presence | Use |
|---|---|---|---|
malfunctions.microphone | boolean | Optional | Edge-detected against the previous cycle; a change publishes a FaultEvent |
malfunctions.speaker | boolean | Optional | Same, as a separate fault type |
sip_state | string | Optional | Retained in the snapshot, never published |
call_state | string | Optional | Retained in the snapshot, never published |
ptt | string | Optional, defaults to off | Retained in the snapshot, never published |
squelch | string | Optional; true only when exactly on, compared case-sensitively | Retained in the snapshot, never published |
GET sessions/{session}/platforms/{platform}/seats/{seat}/assets:
| Property | Type | Presence | Use |
|---|---|---|---|
assets[] | array | Optional; an absent array yields zero assets and no warning | Asset list |
assets[].type | string | Required in practice | Filter and published value. Only radio, intercom and relay are kept; every other asset type is discarded silently. |
assets[].name | string | Optional, defaults to empty | Published asset name; also the key the SSE handler uses to look the asset up |
assets[].frequency | string | Optional | Published as the transmit frequency, for radio and relay only |
assets[].frequency_rx | string | Optional | Published as the receive frequency, for radio and relay only |
assets[].frequency_dual | string | Optional; true only when it equals on, compared case-insensitively | Published as the dual-frequency flag, for radio and relay only |
assets[].radio_type | string | Optional | Published as the radio band, for radio and relay only |
assets[].secure.enabled | boolean | Optional | Published as the secure flag, for radio and relay only |
assets[].volume | integer | Optional, defaults to 100 | Published volume, for every retained asset type |
assets[].squelch.level | integer | Optional, defaults to 0 | Published squelch level, for radio and relay only |
An intercom asset therefore publishes only its name, type and volume. Its frequency, band, secure
and squelch fields are published as empty strings, false and 0 regardless of what the service
reports for them, because the parser gates those reads on the asset being radio or relay.
GET sessions/{session}/platforms/{platform}:
| Property | Type | Presence | Use |
|---|---|---|---|
callsign | string | Optional, defaults to empty | Parsed into the platform snapshot, never published |
postition_source | object | Optional; absence returns no position at all | Gate for the entire WorldPositionComponent |
postition_source.source | string | Optional | Must be geographic. geocentric is recognised and then rejected without conversion. Any other value, or absence, yields no position. |
postition_source.lat | number | Optional, defaults to 0 | Published latitude |
postition_source.long | number | Optional, defaults to 0 | Published longitude |
postition_source.alt | number | Optional, defaults to 0 | Published altitude |
The parser reads the property spelled postition_source while the comment immediately above it names
position_source. If the service does not send that exact misspelling, the lookup fails, no position
snapshot is produced, and the seat entity is published with no WorldPositionComponent: valid
position data is silently omitted with no log line. This is audit finding SONO-01. Which spelling the
service actually sends is not determinable from this module’s source and has not been confirmed
against a live SonoMarc payload, so the defect is the divergence between the code and its own comment
rather than a proven mismatch with the service.
4.6 Timing and cadence
| Item | Value |
|---|---|
| Poll cadence | The SDK Interval, default 1000 ms, re-read every cycle |
| Requests per cycle | Three sequential GETs per known seat, awaited one after another; no concurrency and no batching |
| Effective cadence | Poll duration plus Interval, because the SDK delays Interval after HandleCapture returns rather than on a fixed schedule |
HandleCapture timeout | None. A slow or hanging server stretches the cycle up to the 10-second per-request timeout multiplied by the number of outstanding requests. |
| Fault edge detection | Computed between consecutive polls only. A microphone or speaker fault that both starts and clears inside one cycle produces no event. |
| Session poll cadence | SessionPollRetryMs, default 5000 ms, inside StartCapture |
4.7 Error behaviour
| Condition | Behaviour | Log |
|---|---|---|
sessions request fails, or the response is missing sessions, name or state | Retry after SessionPollRetryMs, indefinitely | Warning beginning Sonomarc: session poll error, with the exception message |
| No session matches the name and state criteria | Retry after SessionPollRetryMs, indefinitely | Debug message containing Sonomarc: no running session yet and {Delay}ms |
| Session poll loop ends without a session, which requires cancellation | StartCapture returns false and the module does not start | Error beginning Sonomarc Module: no running session found |
StartCapture exceeds the 5-second StartupTimeout | The SDK stops waiting and the module does not start. It does not cancel the operation: the abandoned StartCapture continues in the background, so the session poll keeps running, the subscription is eventually registered, and the event stream begins queueing events that no capture loop ever drains. | SDK warning, "{Module} StartCapture timed out after {Timeout}. Module did not start." |
| Any failure during platform or seat discovery, including one malformed seat entry | Discovery aborts for that call and every remaining platform and seat. StartCapture continues with whatever was collected. | Error beginning Sonomarc: seat discovery failed, with the exception message |
| No seat matches the capture-mode criteria | The module starts and publishes frames containing no entities | Warning, "Sonomarc Module: no seats found matching capture mode criteria" |
| Seat or asset request fails during a refresh | That seat retains its previous snapshot for the cycle and is still published with stale values. Other seats continue. | Warning beginning Sonomarc: failed to refresh seat, with {Key} and the exception message |
| Platform request fails, or the platform JSON cannot be parsed | Swallowed by an empty catch. The callsign and position snapshots become null for that cycle, so WorldPositionComponent disappears from the entity without any indication. | None |
HandleCapture throws | The SDK logs it and the capture loop continues | SDK, "Error in module" |
A stale snapshot and a fresh one are indistinguishable in the published frame: the module carries no per-seat freshness or validity flag, and the frame capture time is always the current host clock.
5. IF-CMSONO-02 Server-Sent Events interface
5.1 Transport and addressing
| Item | Value |
|---|---|
| Base address | http://{Host}:{Port}/api, passed to the receiver separately from the REST client and trimmed of a trailing slash |
| Client | A second HttpClient with an infinite timeout, because the stream is long-lived |
| Register | POST {base}/events/subscriptions |
| Populate | PUT {base}/events/subscriptions/:{id} |
| Stream | GET {base}/events/stream/:{id} with Accept: text/event-stream, read with ResponseHeadersRead so the body streams |
The colon before the subscription id in the PUT and GET paths is a literal character in the URL
the module builds, not placeholder notation. Whether the SonoMarc API expects that leading colon is a
service property that cannot be established from this module’s source; the vendor’s own API
documentation is the authority.
5.2 Subscription registration
The POST sends an empty body with no content type and expects a JSON object with a string id
property. A non-success status, a missing id, or an empty id aborts registration.
The PUT sends a flat JSON object mapping each subscribed event path to the string "enable".
The paths are built from the seats and assets discovered at start:
- one seat base path,
sessions/{session}/platforms/{platform}/seats/{seat} - one path per discovered asset,
sessions/{session}/platforms/{platform}/seats/{seat}/assets/{asset}
Unlike the polling calls, these path segments are inserted unescaped, so a session, platform, seat or asset name containing a slash produces a malformed subscription path and a seat key that cannot be resolved back.
5.3 Subscription set actually enabled
For every supplied path the registration code enables {path}/rx_activity and {path}/tx_activity.
It enables selected, secure, squelch and ptt only inside a branch guarded by the path ending
in /assets. The path builder never produces a path ending in /assets: it produces the seat base
and fully qualified .../assets/{asset} paths. That branch is therefore unreachable in this revision,
and the subscription set is exactly:
| Enabled event path | Per |
|---|---|
sessions/{session}/platforms/{platform}/seats/{seat}/rx_activity | Seat |
sessions/{session}/platforms/{platform}/seats/{seat}/tx_activity | Seat |
sessions/{session}/platforms/{platform}/seats/{seat}/assets/{asset}/rx_activity | Asset |
sessions/{session}/platforms/{platform}/seats/{seat}/assets/{asset}/tx_activity | Asset |
Push-to-talk, asset selection, secure toggling and squelch are never subscribed, so the dispatcher and mapping table entries for them are only reachable if the service pushes events that were not requested. The registration log line reports the enabled count and can be used to check this: it is twice the number of seats plus assets.
Assets discovered after StartCapture receive no subscription, because the set is registered once and
never revised.
5.4 Connection lifecycle and reconnect
StartAsync registers the subscription and then runs the stream loop on a background task. If
registration fails, StartAsync returns without starting the loop and never retries: the capture
session runs to completion with no events at all, while REST polling continues normally.
Once the loop is running, an ended stream or any exception other than cancellation is logged as a
warning and retried after SseReconnectDelayMs, default 3000 ms, indefinitely. Reconnection reuses
the subscription id from the original registration and does not re-register or re-PUT the path map.
Whether the service retains a subscription across a dropped stream is a service property that cannot
be established from this module’s source; if it does not, reconnection produces a stream that
delivers nothing.
StopAsync cancels the loop, awaits the stream task, and disposes the streaming client.
5.5 Authentication
None, on all three requests. There is no setting for a credential on this interface either.
5.6 Wire format
The reader handles two SSE fields and dispatches on a blank line:
| Line prefix | Handling |
|---|---|
event: | Trimmed remainder is taken as the event path, matched case-insensitively on the prefix |
data: | Trimmed remainder is taken as the event payload |
| Blank line | Dispatches the block when both an event path and a payload were seen, then clears both |
Anything else, including id:, retry: and comment lines | Ignored |
A data: line assigns rather than appends, so a payload split across multiple data: lines is
truncated to its last line. This breaks the SSE convention that multiple data: lines concatenate.
The payload must be a JSON object. Only one property is read from it on activity events:
| Property | Type | Use |
|---|---|---|
value | string | Read as the event value; absent or non-string yields an empty string |
On a virtual-platform command event the payload is instead read for virtual_platform.name, and the
raw JSON text is forwarded whole.
The event path itself is parsed positionally: the segment after platforms and the segment after
seats form the seat key, the last segment is the event type, and the second-to-last segment is taken
as the asset name. On a seat-level path such as .../seats/{seat}/rx_activity the second-to-last
segment is the seat name, so the asset lookup fails and the published event carries an empty
frequency, an empty band, secure false, and a target of {platform}/{seat}/{seat}. Seat-level
activity events are subscribed, so this is reachable in normal operation.
5.7 Event mapping
| Event path suffix | Subscribed | Tracking event | Comms event |
|---|---|---|---|
rx_activity | Yes | RECEIVED when the value is on; none when it is off | Yes, voice |
tx_activity | Yes | VOICE_TRANSMISSION_STARTED when on, VOICE_TRANSMISSION_ENDED when off | Yes, voice |
ptt | No | VOICE_TRANSMISSION_ENDED for off, VOICE_TRANSMISSION_STARTED for every other value | Yes, voice |
selected | No | INTERCOM_STATE_CHANGED, value ignored | No |
secure | No | TRANSMITTER_STATE_CHANGED, both branches identical so the direction is not carried | No |
squelch | No | INTERCOM_STATE_CHANGED, value ignored | No |
| Any other suffix | No | None | No |
Path containing /virtual_platforms/ and ending /command | No | None | Yes, text-to-speech, plus a JSON event |
A value counts as on when it equals on, or when it is neither off nor empty. An empty value counts
as off. The push-to-talk value is instead matched literally, and normal, priority and emergency
all map to the same started event, so the priority level is lost and is not carried into the comms
event either. Receive ending produces nothing at all, so a reception has a start with no end.
Values arriving on selected never reach the published data: the selected flag on the radio component
is hard-coded false in the parser, and no SSE handler writes back into the snapshot.
5.8 Error behaviour
| Condition | Behaviour | Log |
|---|---|---|
POST /events/subscriptions returns a non-success status | Registration aborts, stream never starts | Error, "SonomarcSseReceiver: POST /events/subscriptions returned {Code}" |
Response carries no id, or an empty one | Registration aborts | Error, "SonomarcSseReceiver: subscription POST returned no id" |
PUT returns a non-success status | Registration aborts | Error, "SonomarcSseReceiver: PUT subscription set returned {Code}" |
| Any exception during registration | Registration aborts | Error beginning SonomarcSseReceiver: subscription registration error, with the exception message |
| Registration returned nothing | Stream loop is never started and never retried | Error, "SonomarcSseReceiver: failed to register event subscription" |
| Stream ends or throws | Reconnect after SseReconnectDelayMs, indefinitely, reusing the same subscription id | Warning beginning SonomarcSseReceiver: stream disconnected, with the exception message |
| Event payload is not valid JSON, or dispatch throws | Event dropped | Debug message beginning SonomarcSseReceiver: failed to dispatch event, with {Path} and the exception message |
Event path has no platforms or seats segment | Event dropped with no log | None |
6. IF-CMSONO-03 Frame publication and data model reference
6.1 Entity model
| Item | Value |
|---|---|
| Frame | One SimulationFrame per capture cycle, with a capture time in UTC Unix milliseconds taken when the frame is built |
| Entities | One EntityState per known seat, published every cycle whether or not anything changed |
| Entity name | {platform}/{seat} |
| Entity id | GetHashCode of the seat key under OrdinalIgnoreCase, masked to 63 bits |
| Platform entity | Never published. The parent id on each seat points at a hash of the platform name for which no EntityState exists. |
The entity id is derived from the .NET string hash, which is seeded randomly per process. Ids are consistent within one run of HEAT Capture and change between runs for the same seat name, so they cannot be used to correlate entities across sessions. The parent id on the linked-entity component is computed the same way and stays consistent with the seat ids inside a single run.
6.2 Published components and fields
WorldPositionComponent, published only when a geographic position was resolved for the platform:
| Field | Source | Value |
|---|---|---|
latitude | postition_source.lat | Decimal degrees, read as a double and narrowed to float |
longitude | postition_source.long | Decimal degrees, same narrowing |
altitude | postition_source.alt | Passed through unconverted |
heading | None | Always 0. No heading property is read from the service. |
LinkedEntityComponent, published on every seat entity unconditionally:
| Field | Source | Value |
|---|---|---|
parent_entity_id | Platform name | Masked hash of the platform name |
RadioComponent, one per retained asset:
| Field | Source |
|---|---|
asset_name | assets[].name |
asset_type | assets[].type |
frequency_tx | assets[].frequency, radio and relay only |
frequency_rx | assets[].frequency_rx, radio and relay only |
frequency_dual | assets[].frequency_dual equal to on, radio and relay only |
radio_band | assets[].radio_type, radio and relay only |
secure_enabled | assets[].secure.enabled, radio and relay only |
selected | Nothing. Always false. |
volume | assets[].volume |
squelch_level | assets[].squelch.level, radio and relay only |
6.3 Events published
Events are enqueued when they occur and drained into the next frame, so an event can be published up to one full cycle after it happened. Each event carries its own timestamp taken at the moment it was enqueued, which is the SSE arrival time or the poll at which a fault edge was detected, not the frame capture time.
| Trigger | Published |
|---|---|
Subscribed rx_activity, tx_activity, or an unsubscribed ptt event | A voice comms event carrying the asset’s current transmit frequency, band and secure flag from the last poll, and a target of {platform}/{seat}/{asset} |
The same events, plus selected, secure and squelch where the mapping yields one | A tracking event of the mapped type |
| A microphone or speaker malfunction changing between two polls | A fault event with a type of microphone or speaker, activated or resolved |
| A virtual-platform command event | Two events at once: a text-to-speech comms event whose target is the virtual person’s name and whose content is the raw command JSON, and a JSON event named sonomarc_voice_command carrying the same JSON |
Comms and tracking events reference the seat entity id. Virtual-platform command events instead reference a hash of the platform name taken from the event path, which is not a published entity.
6.4 Polled but not published
The following are read from the service and retained in the module’s snapshot on every cycle, and then never reach a published field: the seat type, the seat binding, the seat-level volume, the SIP state, the call state, the seat-level push-to-talk value, the seat-level squelch state, and the platform callsign.
7. Units and conventions
| Concept | Convention |
|---|---|
| Frequencies | Opaque display strings, passed through with no parsing, normalisation or validation. RadioComponent documents the shape as "243.000 MHz"; the module does not enforce it. |
| Radio band | String from the service’s radio_type. RadioComponent documents HF, VHF, UHF and UHF SATCOM; the module does not validate against that set. |
| Volume | Integer passed through. RadioComponent documents a 0 to 100 range; the module does not range check, and an absent property publishes 100. |
| Squelch level | Integer passed through. RadioComponent documents 0 as off and 1 to 100 otherwise. An absent property publishes 0, which is indistinguishable from a reported 0. |
| Latitude and longitude | Decimal degrees, positive north and east, per WorldPositionComponent. Read as JSON doubles and narrowed to float32, which quantises to roughly 0.3 to 1 metre. |
| Altitude | Metres per WorldPositionComponent. The module applies no conversion, so the published value is whatever unit the service reports. |
| Coordinate frame | The module neither declares nor checks a datum, and the component does not carry one. It relies on the service reporting geographic coordinates already in decimal degrees. |
| Heading | Degrees per WorldPositionComponent, but always published as 0 by this module. |
| Time base | Host wall clock, UTC Unix milliseconds. No service-supplied timestamp is read or carried on any frame or event. |
| Boolean strings | The service’s on and off strings. The asset dual-frequency flag is compared case-insensitively; the seat squelch state is compared case-sensitively. |
8. Configuration affecting the interfaces
| Setting | Default | Interface effect |
|---|---|---|
Host | localhost | REST and SSE host for both clients |
Port | 80 | REST and SSE port for both clients |
SessionName | empty | Session filter. Empty accepts the first session found in the Running state, in whatever order the service returns them. |
SessionPollRetryMs | 5000 | Session poll retry delay inside StartCapture. At the default it equals the whole 5-second startup budget. |
CaptureMode | WholeSession | WholeSession polls and subscribes to every seat on every platform. PerSeat restricts both to seats whose binding matches. |
SeatBinding | empty, meaning the local hostname | The PerSeat match value. The match is a substring test, so a short binding value can select more than one seat. |
SseReconnectDelayMs | 3000 | Delay between stream reconnection attempts. Does not affect subscription registration, which is never retried. |
Interval | 1000 ms | REST poll cadence, fault edge-detection resolution, and the maximum queueing delay for an event |
Enabled | true | Whether the module runs at all |
The scheme, the 10-second REST request timeout, the infinite SSE timeout, and the 5-second startup budget are not configurable. See Manage capture modules for the generic settings and the Module SDK ICD for the lifecycle contract they belong to.
9. Compatibility and versioning
There is no version negotiation on either interface. The module assumes the resource paths, JSON property names and event-path grammar documented above. It is tolerant of missing optional properties and substitutes defaults for them, which means a renamed property degrades silently rather than failing: a renamed asset frequency publishes an empty string, a renamed volume publishes 100, and a renamed position source publishes no position component. The properties read with a required accessor are the session name and state, the platform name, and the seat name, binding and type; renaming one of those produces a logged failure instead.
The event grammar is parsed positionally on the segments named platforms and seats, so a change to
the service’s path structure silently redirects or drops events rather than raising an error.
10. Security considerations
Both interfaces are unauthenticated plain HTTP with no setting to change either property, so credentials, tokens and TLS cannot be introduced through configuration. Any host with network access to the SonoMarc API can read the same session, seat and asset state, register its own event subscription, and observe the same event stream. Traffic is readable and modifiable in transit by anything on the path.
Captured data includes radio frequencies, band assignments, secure-mode flags and platform positions, and the recognised text of virtual-person voice commands is carried verbatim in the published event content. Restrict the SonoMarc service to an isolated network segment and treat captured sessions according to the deployment’s own classification of that material.
11. Conformance
- With a SonoMarc session in the
Runningstate, start a capture session. Confirm the log records the starting mode and host, the selected session name, the discovered seat count, and the registered subscription id and path count. - Confirm the frame carries one entity per expected seat, named
{platform}/{seat}, and that the radio components match the assets on those seats. - Confirm the registered path count equals twice the number of seats plus assets. Exercise push-to-talk, asset selection, secure toggling and squelch on the service and confirm that none of them produces an event, which is the observable consequence of the unreachable expansion branch in section 5.3.
- Exercise transmit and receive and confirm the paired comms and tracking events appear. Confirm a reception produces a start with no matching end.
- Capture the raw response of
GET sessions/{session}/platforms/{platform}and inspect the spelling of the position source property. If it is notpostition_source, no seat entity carries a position component, which is SONO-01. Record the observed spelling so the finding can be closed either way. - Where a position component is published, confirm the heading is 0 and that latitude, longitude and altitude match the service’s reported values without conversion.
- Confirm the selected flag is false on every radio component even while an asset is selected on the service.
- Restart HEAT Capture without changing the SonoMarc configuration and confirm the seat entity ids differ from the previous run, so that no downstream analysis keys on them across sessions.
- Stop the SonoMarc service before starting capture. Confirm the SDK startup timeout warning and that no frames are published, then confirm whether the process continues to issue session polls, which is the abandoned startup operation described in section 4.7.
- With capture running, interrupt the network to the service. Confirm the refresh warnings, the stream disconnect warning, and whether events resume after reconnection without a capture restart. This is the test that determines whether the service retains a subscription across a dropped stream.
12. Revision history
| Rev | Date | Product version | Change |
|---|---|---|---|
| 1 | 2026-09-02 | 8347c3b | Initial issue. |