Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
Dual-Transfer State Negotiation

Which of two redundant instances is active, and how well-backed is the standby one (ADR-020). More...

Files

file  src/redundancy/dual/sapi_dual_channel.c
 "DualChannel" layer of ADR-020 - see sapi_dual_channel.h.
file  src/redundancy/dual/sapi_dual_msgchannel.c
 "Channel" layer of ADR-020 - see sapi_dual_msgchannel.h.
file  src/redundancy/dual/sapi_dual_negotiator.c
 Dual state negotiator of ADR-020 - see sapi_dual_negotiator.h.
file  src/redundancy/dual/sapi_dual_types.c
 Diagnostics-only string rendering for the sapi_dual module's shared enums (ADR-020).

Data Structures

struct  sapi_dual_channel_t
 One sapi_dual_channel_t instance's state. Caller-owned storage; every field is private - reach it only through the functions below. More...
struct  sapi_dual_channel_config_t
 Configuration for sapi_dual_channel_init(). More...
struct  sapi_dual_frame_header_t
 Common 4-byte header prefixing every Layer-2 frame; kept a fixed 4 bytes (not just 1) so the fields that follow in sapi_dual_ack_frame_t/sapi_dual_state_frame_t stay naturally aligned. More...
struct  sapi_dual_ack_frame_t
 SAPI_DUAL_FRAME_KIND_ACK payload. More...
struct  sapi_dual_state_frame_t
 SAPI_DUAL_FRAME_KIND_STATE payload - sapi_dual_negotiator_t's own periodic beacon. More...
struct  sapi_dual_heartbeat_frame_t
 SAPI_DUAL_FRAME_KIND_HEARTBEAT payload - connection maintenance heartbeat. More...
struct  sapi_dual_msgchannel_t
 One sapi_dual_msgchannel_t instance's state. Caller-owned storage (REQ-DUAL-MSGCHANNEL-001); opaque in practice, exposed here (not via SAFEAPI_DECLARE_STORAGE) only because its size is already small and fixed - callers must still treat every field as private and only reach it through the functions below. More...
struct  sapi_dual_msgchannel_config_t
 Configuration for sapi_dual_msgchannel_init(). More...
struct  sapi_dual_negotiator_t
 One sapi_dual_negotiator_t instance's state. Caller-owned storage; every field is private - reach it only through the functions below. More...
struct  sapi_dual_negotiator_config_t
 Configuration for sapi_dual_negotiator_init(). More...

Macros

#define SAPI_DUAL_CHANNEL_MAX_LINKS   4U
 Maximum number of redundant links one sapi_dual_channel_t may be configured with. Fixed, not dynamic (REQ-OAL-COMMON-010).
#define SAPI_DUAL_CHANNEL_MAX_PAYLOAD   (SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD - 4U)
 Max application payload bytes usable via sapi_dual_channel_send()/_receive() - Layer-1's own SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD minus this layer's 4-byte sapi_dual_frame_header_t.
#define SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD   248U
 Max payload bytes usable via sapi_dual_msgchannel_send()/ _receive() - bounded by sapi_vital_message_t's own fixed 248-byte payload field (sapi_checksum.h).

Typedefs

typedef void(*) sapi_dual_channel_status_callback_t(sapi_dual_channel_status_t new_status, sapi_dual_channel_status_t old_status, void *user_ctx)
 Optional callback invoked whenever sapi_dual_channel_get_status()'s value changes (ADR-020 section 2's "indicate to the user via callback if registered" requirement). Called synchronously from inside sapi_dual_channel_send() (the only place aggregate status is recomputed) - keep this fast; it is on the same call path as the send it was triggered by.
typedef void(*) sapi_dual_negotiator_state_change_callback_t(sapi_dual_state_t new_own_state, sapi_dual_state_t old_own_state, sapi_dual_state_t new_peer_state, sapi_dual_state_t old_peer_state, void *user_ctx)
 Optional callback invoked whenever either the own or the peer sapi_dual_state_t changes, from inside sapi_dual_negotiator_execute().

Enumerations

enum  sapi_dual_frame_kind_t { SAPI_DUAL_FRAME_KIND_DATA = 0 , SAPI_DUAL_FRAME_KIND_ACK = 1 , SAPI_DUAL_FRAME_KIND_STATE = 2 , SAPI_DUAL_FRAME_KIND_HEARTBEAT = 3 }
 Which kind of Layer-2 frame a given Layer-1 payload holds. Stored as the first byte of every sapi_dual_channel_t frame so DATA, ACK, and STATE traffic sharing the same redundant links can never be misinterpreted as each other. More...
enum  sapi_dual_state_t {
  SAPI_DUAL_STATE_IDLE = 0 , SAPI_DUAL_STATE_UNKNOWN = 1 , SAPI_DUAL_STATE_ONLINE = 2 , SAPI_DUAL_STATE_HOTSTANDBY = 3 ,
  SAPI_DUAL_STATE_COLDSTANDBY = 4
}
 State of one instance in a dual (two-redundant-instance) relationship, as decided by sapi_dual_negotiator_t (ADR-020 section 3). Both this instance's own state and its last-known view of the peer's state use this same enum - see sapi_dual_negotiator_get_own_state()/_get_peer_state(). More...
enum  sapi_dual_channel_status_t { SAPI_DUAL_CHANNEL_STATUS_DOWN = 0 , SAPI_DUAL_CHANNEL_STATUS_DEGRADED = 1 , SAPI_DUAL_CHANNEL_STATUS_FULL = 2 }
 Aggregate connection status of a sapi_dual_channel_t across all of its configured redundant links (ADR-020 section 2). Independent of sapi_dual_state_t: losing some (not all) of the redundant links degrades this status without by itself forcing any sapi_dual_state_t transition - only total loss (DOWN) is a negotiator-relevant liveness event. More...

Functions

sapi_status_t sapi_dual_channel_init (sapi_dual_channel_t *channel, const sapi_dual_channel_config_t *config)
 Initializes a sapi_dual_channel_t: initializes every configured redundant link (sapi_dual_msgchannel_init()) and starts sapi_dual_channel_get_status() at SAPI_DUAL_CHANNEL_STATUS_DOWN (no traffic has been sent/received yet).
sapi_status_t sapi_dual_channel_send (sapi_dual_channel_t *channel, const uint8_t *payload, uint8_t payload_size, uint32_t *out_ack_link_count)
 Sends payload as a DATA frame on every configured redundant link - never gated by any negotiated state (ADR-020 section 2: the real payload traffic itself is the liveness check). For each link, waits up to config->ack_timeout_ms for that link's own ACK (matched by sequence number, not just "an ACK arrived") before moving on; a DATA or STATE frame received from the peer while waiting is still auto-ACKed/staged as a side effect, not dropped.
sapi_status_t sapi_dual_channel_receive (sapi_dual_channel_t *channel, uint8_t *out_payload, uint8_t max_size, sapi_duration_ms_t timeout_ms, uint8_t *out_size)
 Returns the most recently staged inbound DATA frame, actively polling the configured links (each auto-ACked on arrival - see sapi_dual_channel_send()'s own doc) if none was already staged from a previous sapi_dual_channel_send() call's own incidental polling.
sapi_status_t sapi_dual_channel_send_heartbeat (sapi_dual_channel_t *channel, uint32_t *out_ack_count)
 Sends a sapi_dual_state_frame_t (sapi_dual_frames.h) on every configured redundant link - fire-and-forget, no ACK wait (unlike sapi_dual_channel_send()'s DATA frames), matching the periodic-beacon nature of state negotiation. Intended to be called by a sapi_dual_negotiator_t, not directly by application code - see ADR-020 section 3.
sapi_status_t sapi_dual_channel_send_state_frame (sapi_dual_channel_t *channel, sapi_dual_state_t state, bool channel_degraded, uint64_t timestamp_ms)
sapi_status_t sapi_dual_channel_receive_state_frame (sapi_dual_channel_t *channel, sapi_duration_ms_t timeout_ms, sapi_dual_state_frame_t *out_frame)
 Returns the most recently staged inbound STATE frame, actively polling the configured links if none was already staged. Intended to be called by a sapi_dual_negotiator_t - see ADR-020 section 3.
sapi_dual_channel_status_t sapi_dual_channel_get_status (const sapi_dual_channel_t *channel)
 Returns the aggregate connection status across every configured redundant link, as of the most recent sapi_dual_channel_send() call (ADR-020 section 2). SAPI_DUAL_CHANNEL_STATUS_DOWN before the first send.
bool sapi_dual_channel_is_link_up (const sapi_dual_channel_t *channel, uint32_t link_index)
 Returns whether one specific configured link is currently considered up, as of the most recent sapi_dual_channel_send() call - for diagnostics/logging (e.g. which specific redundant path is the one that's down), not a safety-decision input on its own (see sapi_dual_channel_get_status() for the aggregate).
sapi_status_t sapi_dual_msgchannel_init (sapi_dual_msgchannel_t *channel, const sapi_dual_msgchannel_config_t *config)
 Initializes a sapi_dual_msgchannel_t: starts both sequence counters at 0.
sapi_status_t sapi_dual_msgchannel_send (sapi_dual_msgchannel_t *channel, const uint8_t *payload, uint8_t payload_size, sapi_duration_ms_t timeout_ms, uint32_t *out_sequence)
 Wraps payload in a sapi_vital_message_t (this channel's own sender_id and next sequence_number) and sends it over config->link, blocking at most timeout_ms.
sapi_status_t sapi_dual_msgchannel_receive (sapi_dual_msgchannel_t *channel, uint8_t *out_payload, uint8_t payload_max_size, sapi_duration_ms_t timeout_ms, uint8_t *out_payload_size, uint32_t *out_sequence)
 Receives one frame over config->link, blocking at most timeout_ms, verifies its CRC-64 and sequence continuity (sapi_checksum_vital_message_verify() against channel->expected_sequence) and its sender_id against config->expected_peer_id, and extracts the payload.
sapi_status_t sapi_dual_msgchannel_reset_sequence (sapi_dual_msgchannel_t *channel)
 Resets both sequence counters to 0. Intended to be called by the caller (typically sapi_dual_channel_t) exactly once, immediately after config->link has been freshly (re)established with the peer - mirrors safeAPIRBC2oo2's own precedent (channel_ab.c's cycle_resync_requested handling after a peer link reconnects) for why a fresh link needs a fresh, mutually agreed starting sequence rather than fighting over whatever counters were left over from before the disconnect.
sapi_status_t sapi_dual_negotiator_init (sapi_dual_negotiator_t *negotiator, const sapi_dual_negotiator_config_t *config)
 Initializes a sapi_dual_negotiator_t. Captures this instance's own startup timestamp once (sapi_timer_now(), degrading to 0 if no timer backend is registered - same best-effort posture as sapi_log_write_event()'s Timestamp field) for use on every beacon this negotiator ever sends - see sapi_dual_state_frame_t's own doc on why this must stay fixed. Both own and peer state start at SAPI_DUAL_STATE_IDLE.
sapi_status_t sapi_dual_negotiator_execute (sapi_dual_negotiator_t *negotiator, sapi_duration_ms_t receive_timeout_ms)
 Drives one round of state negotiation: refreshes own_channel_degraded from the attached channel's current sapi_dual_channel_get_status(), sends this instance's own beacon, drains and processes every currently-pending inbound STATE frame, then recomputes both own and peer sapi_dual_state_t:
sapi_dual_state_t sapi_dual_negotiator_get_own_state (const sapi_dual_negotiator_t *negotiator)
 Returns this instance's own current sapi_dual_state_t.
sapi_dual_state_t sapi_dual_negotiator_get_peer_state (const sapi_dual_negotiator_t *negotiator)
 Returns this instance's last-known view of the peer's sapi_dual_state_t.
const char * sapi_dual_state_to_string (sapi_dual_state_t state)
 Returns a short, static, human-readable string for a sapi_dual_state_t. Intended for diagnostics/logging only (e.g. as the Info= field of a sapi_log_write_event() call); never on a safety-decision path.
const char * sapi_dual_channel_status_to_string (sapi_dual_channel_status_t status)
 Returns a short, static, human-readable string for a sapi_dual_channel_status_t. Same diagnostics-only contract as sapi_dual_state_to_string().

Detailed Description

Which of two redundant instances is active, and how well-backed is the standby one (ADR-020).

Macro Definition Documentation

◆ SAPI_DUAL_CHANNEL_MAX_LINKS

#define SAPI_DUAL_CHANNEL_MAX_LINKS   4U

Maximum number of redundant links one sapi_dual_channel_t may be configured with. Fixed, not dynamic (REQ-OAL-COMMON-010).

Definition at line 66 of file sapi_dual_channel.h.

◆ SAPI_DUAL_CHANNEL_MAX_PAYLOAD

#define SAPI_DUAL_CHANNEL_MAX_PAYLOAD   (SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD - 4U)

Max application payload bytes usable via sapi_dual_channel_send()/_receive() - Layer-1's own SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD minus this layer's 4-byte sapi_dual_frame_header_t.

Definition at line 72 of file sapi_dual_channel.h.

◆ SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD

#define SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD   248U

Max payload bytes usable via sapi_dual_msgchannel_send()/ _receive() - bounded by sapi_vital_message_t's own fixed 248-byte payload field (sapi_checksum.h).

Definition at line 58 of file sapi_dual_msgchannel.h.

Typedef Documentation

◆ sapi_dual_channel_status_callback_t

typedef void(*) sapi_dual_channel_status_callback_t(sapi_dual_channel_status_t new_status, sapi_dual_channel_status_t old_status, void *user_ctx)

Optional callback invoked whenever sapi_dual_channel_get_status()'s value changes (ADR-020 section 2's "indicate to the user via callback if registered" requirement). Called synchronously from inside sapi_dual_channel_send() (the only place aggregate status is recomputed) - keep this fast; it is on the same call path as the send it was triggered by.

Parameters
new_statusStatus just transitioned to.
old_statusStatus just transitioned from.
user_ctxCaller-supplied context from sapi_dual_channel_config_t.

Definition at line 85 of file sapi_dual_channel.h.

◆ sapi_dual_negotiator_state_change_callback_t

typedef void(*) sapi_dual_negotiator_state_change_callback_t(sapi_dual_state_t new_own_state, sapi_dual_state_t old_own_state, sapi_dual_state_t new_peer_state, sapi_dual_state_t old_peer_state, void *user_ctx)

Optional callback invoked whenever either the own or the peer sapi_dual_state_t changes, from inside sapi_dual_negotiator_execute().

Parameters
new_own_stateOwn state just transitioned to.
old_own_stateOwn state just transitioned from.
new_peer_statePeer state just transitioned to.
old_peer_statePeer state just transitioned from.
user_ctxCaller-supplied context from sapi_dual_negotiator_config_t.

Definition at line 71 of file sapi_dual_negotiator.h.

Enumeration Type Documentation

◆ sapi_dual_frame_kind_t

Which kind of Layer-2 frame a given Layer-1 payload holds. Stored as the first byte of every sapi_dual_channel_t frame so DATA, ACK, and STATE traffic sharing the same redundant links can never be misinterpreted as each other.

Explicit numeric values are fixed and part of the wire format: do not renumber existing entries, only append.

Enumerator
SAPI_DUAL_FRAME_KIND_DATA 

Application payload (sapi_dual_channel_send()/_receive()); expects a SAPI_DUAL_FRAME_KIND_ACK frame back.

SAPI_DUAL_FRAME_KIND_ACK 

Acknowledges one DATA frame's sequence_number (acked_sequence below) - checked, not just "some ACK arrived", against the specific send it corresponds to (ADR-020 section 1).

SAPI_DUAL_FRAME_KIND_STATE 

sapi_dual_negotiator_t's own state beacon - fire-and-forget, periodic, no ACK expected (unlike DATA).

SAPI_DUAL_FRAME_KIND_HEARTBEAT 

Liveness heartbeat frame - sent periodically to maintain connection, expects a SAPI_DUAL_FRAME_KIND_ACK frame back.

Definition at line 41 of file sapi_dual_frames.h.

◆ sapi_dual_state_t

State of one instance in a dual (two-redundant-instance) relationship, as decided by sapi_dual_negotiator_t (ADR-020 section 3). Both this instance's own state and its last-known view of the peer's state use this same enum - see sapi_dual_negotiator_get_own_state()/_get_peer_state().

Explicit numeric values are fixed and part of the ABI (this value travels on the wire inside sapi_dual_state_frame_t::state): do not renumber existing entries, only append.

Enumerator
SAPI_DUAL_STATE_IDLE 

Never yet negotiated with a peer - startup transient, before the first valid STATE frame has been processed either way.

SAPI_DUAL_STATE_UNKNOWN 

Had a negotiated state before but contact with the peer has been lost long enough (config.peer_lost_timeout_ms) that the state can no longer be trusted. Not itself a safety reaction - see ADR-020 section 4's "no automatic safety reaction" non-goal.

SAPI_DUAL_STATE_ONLINE 

This instance is the active one.

SAPI_DUAL_STATE_HOTSTANDBY 

This instance is standby, and the peer's own redundancy is currently full (peer's last-reported channel_degraded == 0).

SAPI_DUAL_STATE_COLDSTANDBY 

This instance is standby, and the peer's own redundancy is currently degraded (peer's last-reported channel_degraded == 1).

Definition at line 37 of file sapi_dual_types.h.

◆ sapi_dual_channel_status_t

Aggregate connection status of a sapi_dual_channel_t across all of its configured redundant links (ADR-020 section 2). Independent of sapi_dual_state_t: losing some (not all) of the redundant links degrades this status without by itself forcing any sapi_dual_state_t transition - only total loss (DOWN) is a negotiator-relevant liveness event.

Enumerator
SAPI_DUAL_CHANNEL_STATUS_DOWN 

No configured redundant link is currently up (every link's most recent send timed out waiting for its ACK).

SAPI_DUAL_CHANNEL_STATUS_DEGRADED 

Some, but not all, configured redundant links are up.

SAPI_DUAL_CHANNEL_STATUS_FULL 

Every configured redundant link is up.

Definition at line 65 of file sapi_dual_types.h.

Function Documentation

◆ sapi_dual_channel_init()

sapi_status_t sapi_dual_channel_init ( sapi_dual_channel_t * channel,
const sapi_dual_channel_config_t * config )

Initializes a sapi_dual_channel_t: initializes every configured redundant link (sapi_dual_msgchannel_init()) and starts sapi_dual_channel_get_status() at SAPI_DUAL_CHANNEL_STATUS_DOWN (no traffic has been sent/received yet).

Parameters
channelCaller-owned storage to initialize. Must not be NULL.
configConfiguration. Must not be NULL; config->link_count must be in [1, SAPI_DUAL_CHANNEL_MAX_LINKS]; every config->links[0..link_count-1] must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_INVALID_PARAM otherwise.

Definition at line 183 of file sapi_dual_channel.c.

◆ sapi_dual_channel_send()

sapi_status_t sapi_dual_channel_send ( sapi_dual_channel_t * channel,
const uint8_t * payload,
uint8_t payload_size,
uint32_t * out_ack_link_count )

Sends payload as a DATA frame on every configured redundant link - never gated by any negotiated state (ADR-020 section 2: the real payload traffic itself is the liveness check). For each link, waits up to config->ack_timeout_ms for that link's own ACK (matched by sequence number, not just "an ACK arrived") before moving on; a DATA or STATE frame received from the peer while waiting is still auto-ACKed/staged as a side effect, not dropped.

After every configured link has been tried, recomputes sapi_dual_channel_get_status() and invokes config->status_callback if it changed (ADR-020 section 2).

Parameters
channelInitialized channel. Must not be NULL.
payloadPayload to send. May be NULL only if payload_size is 0.
payload_sizePayload size in bytes; must be <= SAPI_DUAL_CHANNEL_MAX_PAYLOAD.
out_ack_link_countOptional; if not NULL, receives how many of the configured links ACKed this send. May be NULL.
Returns
SAPI_STATUS_OK if at least one link ACKed; SAPI_STATUS_TIMEOUT if none did (data was still transmitted-attempted on every link; this reports delivery confirmation, not transmission attempt); SAPI_STATUS_INVALID_PARAM for a bad argument.

Definition at line 232 of file sapi_dual_channel.c.

◆ sapi_dual_channel_receive()

sapi_status_t sapi_dual_channel_receive ( sapi_dual_channel_t * channel,
uint8_t * out_payload,
uint8_t max_size,
sapi_duration_ms_t timeout_ms,
uint8_t * out_size )

Returns the most recently staged inbound DATA frame, actively polling the configured links (each auto-ACked on arrival - see sapi_dual_channel_send()'s own doc) if none was already staged from a previous sapi_dual_channel_send() call's own incidental polling.

Parameters
channelInitialized channel. Must not be NULL.
out_payloadDestination buffer. Must not be NULL.
max_sizeUsable size of out_payload; must be > 0.
timeout_msMaximum total time to actively poll the configured links if nothing was already staged (split evenly across links); 0 = check only what is already staged.
out_sizeReceives the actual payload size. Must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_TIMEOUT if nothing arrived in time; SAPI_STATUS_INVALID_PARAM if a staged frame is larger than max_size (not silently truncated) or for a bad argument.

Definition at line 409 of file sapi_dual_channel.c.

◆ sapi_dual_channel_send_heartbeat()

sapi_status_t sapi_dual_channel_send_heartbeat ( sapi_dual_channel_t * channel,
uint32_t * out_ack_count )

Sends a sapi_dual_state_frame_t (sapi_dual_frames.h) on every configured redundant link - fire-and-forget, no ACK wait (unlike sapi_dual_channel_send()'s DATA frames), matching the periodic-beacon nature of state negotiation. Intended to be called by a sapi_dual_negotiator_t, not directly by application code - see ADR-020 section 3.

Parameters
channelInitialized channel. Must not be NULL.
stateThis instance's own sapi_dual_state_t to advertise.
channel_degradedThis instance's own current sapi_dual_channel_get_status() != FULL, as a single bit (so the peer can tell HOTSTANDBY from COLDSTANDBY for itself).
timestamp_msCaller-supplied value for the frame's own timestamp_ms field. This function does not source it from sapi_timer_now() itself: a sapi_dual_negotiator_t needs the same fixed value on every beacon it ever sends (captured once at its own init) for its startup tie-break to remain stable call to call - see sapi_dual_state_frame_t's own doc.
Returns
SAPI_STATUS_OK if the frame was transmitted on at least one link; SAPI_STATUS_TIMEOUT if every link's send failed/timed out; SAPI_STATUS_INVALID_PARAM if channel is NULL.

Transmits a connection maintenance heartbeat frame across every configured link and waits up to ack_timeout_ms for an ACK on each.

Parameters
channelInitialized channel. Must not be NULL.
out_ack_countOptional; receives the number of links that ACKed.
Returns
SAPI_STATUS_OK if at least one link acknowledged; SAPI_STATUS_TIMEOUT if no link acknowledged within timeout; SAPI_STATUS_HARDWARE_FAULT if every link encountered a hard fault; SAPI_STATUS_INVALID_PARAM for a bad argument.

Definition at line 474 of file sapi_dual_channel.c.

◆ sapi_dual_channel_send_state_frame()

sapi_status_t sapi_dual_channel_send_state_frame ( sapi_dual_channel_t * channel,
sapi_dual_state_t state,
bool channel_degraded,
uint64_t timestamp_ms )

Definition at line 594 of file sapi_dual_channel.c.

◆ sapi_dual_channel_receive_state_frame()

sapi_status_t sapi_dual_channel_receive_state_frame ( sapi_dual_channel_t * channel,
sapi_duration_ms_t timeout_ms,
sapi_dual_state_frame_t * out_frame )

Returns the most recently staged inbound STATE frame, actively polling the configured links if none was already staged. Intended to be called by a sapi_dual_negotiator_t - see ADR-020 section 3.

Parameters
channelInitialized channel. Must not be NULL.
timeout_msMaximum total time to actively poll if nothing was already staged (split evenly across links); 0 = check only what is already staged.
out_frameReceives the frame. Must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_TIMEOUT if none arrived in time; SAPI_STATUS_INVALID_PARAM for a bad argument.

Definition at line 628 of file sapi_dual_channel.c.

◆ sapi_dual_channel_get_status()

sapi_dual_channel_status_t sapi_dual_channel_get_status ( const sapi_dual_channel_t * channel)

Returns the aggregate connection status across every configured redundant link, as of the most recent sapi_dual_channel_send() call (ADR-020 section 2). SAPI_DUAL_CHANNEL_STATUS_DOWN before the first send.

Parameters
channelChannel to query. May be NULL (returns DOWN, defensive default).
Returns
The current aggregate status.

Definition at line 662 of file sapi_dual_channel.c.

◆ sapi_dual_channel_is_link_up()

bool sapi_dual_channel_is_link_up ( const sapi_dual_channel_t * channel,
uint32_t link_index )

Returns whether one specific configured link is currently considered up, as of the most recent sapi_dual_channel_send() call - for diagnostics/logging (e.g. which specific redundant path is the one that's down), not a safety-decision input on its own (see sapi_dual_channel_get_status() for the aggregate).

Parameters
channelChannel to query. May be NULL (returns false).
link_indexIndex into the configured links, 0..link_count-1.
Returns
true if that link's most recent send was ACKed in time; false if link_index is out of range or channel is NULL.

Definition at line 671 of file sapi_dual_channel.c.

◆ sapi_dual_msgchannel_init()

sapi_status_t sapi_dual_msgchannel_init ( sapi_dual_msgchannel_t * channel,
const sapi_dual_msgchannel_config_t * config )

Initializes a sapi_dual_msgchannel_t: starts both sequence counters at 0.

Parameters
channelCaller-owned storage to initialize. Must not be NULL.
configConfiguration. Must not be NULL; config->link must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_INVALID_PARAM for a NULL argument.

Local makros Local types declarations Local variables declarations Global variables declarations Local function declarations Global functions

Definition at line 20 of file sapi_dual_msgchannel.c.

◆ sapi_dual_msgchannel_send()

sapi_status_t sapi_dual_msgchannel_send ( sapi_dual_msgchannel_t * channel,
const uint8_t * payload,
uint8_t payload_size,
sapi_duration_ms_t timeout_ms,
uint32_t * out_sequence )

Wraps payload in a sapi_vital_message_t (this channel's own sender_id and next sequence_number) and sends it over config->link, blocking at most timeout_ms.

Parameters
channelInitialized channel. Must not be NULL.
payloadPayload to send. May be NULL only if payload_size is 0.
payload_sizePayload size in bytes; must be <= SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD.
timeout_msMaximum time to wait for the underlying sapi_netlink_send() to complete (REQ-DUAL-MSGCHANNEL-002).
out_sequenceOptional; if not NULL, receives the sequence_number this frame was sent with (the caller needs this to correlate a later ACK - see sapi_dual_channel.h). May be NULL.
Returns
SAPI_STATUS_OK (channel->next_sequence has been incremented); SAPI_STATUS_INVALID_PARAM; whatever sapi_netlink_send() or sapi_checksum_vital_message_create() returned on failure - channel->next_sequence is NOT incremented on failure, so a retried send reuses the same sequence_number.

Definition at line 37 of file sapi_dual_msgchannel.c.

◆ sapi_dual_msgchannel_receive()

sapi_status_t sapi_dual_msgchannel_receive ( sapi_dual_msgchannel_t * channel,
uint8_t * out_payload,
uint8_t payload_max_size,
sapi_duration_ms_t timeout_ms,
uint8_t * out_payload_size,
uint32_t * out_sequence )

Receives one frame over config->link, blocking at most timeout_ms, verifies its CRC-64 and sequence continuity (sapi_checksum_vital_message_verify() against channel->expected_sequence) and its sender_id against config->expected_peer_id, and extracts the payload.

Parameters
channelInitialized channel. Must not be NULL.
out_payloadDestination buffer. Must not be NULL.
payload_max_sizeUsable size of out_payload; must be > 0.
timeout_msMaximum time to wait for a frame to arrive.
out_payload_sizeReceives the actual payload size. Must not be NULL.
out_sequenceOptional; receives the frame's own sequence_number (e.g. so a DATA frame's sequence can be echoed back in an ACK - see sapi_dual_channel.h). May be NULL.
Returns
SAPI_STATUS_OK (channel->expected_sequence has been advanced to this frame's sequence_number + 1); SAPI_STATUS_INVALID_PARAM; SAPI_STATUS_TIMEOUT if no frame arrives in time; SAPI_STATUS_DATA_CORRUPTION if the frame's CRC-64 or sequence continuity check fails (channel->expected_sequence is left unchanged - see sapi_dual_msgchannel_reset_sequence() for how to recover after a link (re)establishment rather than staying permanently out of sync); SAPI_STATUS_HARDWARE_FAULT if a frame with an unexpected sender_id arrives (possible masquerade - treated the same as a link fault, not silently dropped-and-retried, since this is a defended-integrity violation rather than ordinary transient loss).

Definition at line 80 of file sapi_dual_msgchannel.c.

◆ sapi_dual_msgchannel_reset_sequence()

sapi_status_t sapi_dual_msgchannel_reset_sequence ( sapi_dual_msgchannel_t * channel)

Resets both sequence counters to 0. Intended to be called by the caller (typically sapi_dual_channel_t) exactly once, immediately after config->link has been freshly (re)established with the peer - mirrors safeAPIRBC2oo2's own precedent (channel_ab.c's cycle_resync_requested handling after a peer link reconnects) for why a fresh link needs a fresh, mutually agreed starting sequence rather than fighting over whatever counters were left over from before the disconnect.

Parameters
channelChannel to reset. Must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_INVALID_PARAM if channel is NULL.

Definition at line 135 of file sapi_dual_msgchannel.c.

◆ sapi_dual_negotiator_init()

sapi_status_t sapi_dual_negotiator_init ( sapi_dual_negotiator_t * negotiator,
const sapi_dual_negotiator_config_t * config )

Initializes a sapi_dual_negotiator_t. Captures this instance's own startup timestamp once (sapi_timer_now(), degrading to 0 if no timer backend is registered - same best-effort posture as sapi_log_write_event()'s Timestamp field) for use on every beacon this negotiator ever sends - see sapi_dual_state_frame_t's own doc on why this must stay fixed. Both own and peer state start at SAPI_DUAL_STATE_IDLE.

Parameters
negotiatorCaller-owned storage to initialize. Must not be NULL.
configConfiguration. Must not be NULL; config->channel must not be NULL.
Returns
SAPI_STATUS_OK; SAPI_STATUS_INVALID_PARAM otherwise.

Definition at line 127 of file sapi_dual_negotiator.c.

◆ sapi_dual_negotiator_execute()

sapi_status_t sapi_dual_negotiator_execute ( sapi_dual_negotiator_t * negotiator,
sapi_duration_ms_t receive_timeout_ms )

Drives one round of state negotiation: refreshes own_channel_degraded from the attached channel's current sapi_dual_channel_get_status(), sends this instance's own beacon, drains and processes every currently-pending inbound STATE frame, then recomputes both own and peer sapi_dual_state_t:

  • Never yet heard from the peer: both states are SAPI_DUAL_STATE_IDLE.
  • Heard from the peer before but not within config->peer_lost_timeout_ms: peer_state becomes SAPI_DUAL_STATE_UNKNOWN; own_state becomes SAPI_DUAL_STATE_UNKNOWN too UNLESS it was already SAPI_DUAL_STATE_ONLINE (an active instance keeps acting ONLINE without needing continuous peer confirmation - see ADR-020 section 4's "no automatic safety reaction": this negotiator reports UNKNOWN, it does not itself decide to stop).
  • Peer contact current, own_state still IDLE or UNKNOWN: decides ONLINE vs STANDBY via the older-startup-timestamp-wins / own_id-vs-peer_id tie-break (ADR-020 section 3). Losing the tie-break (this instance is STANDBY) sets own_state to HOTSTANDBY/COLDSTANDBY from the peer's own last-reported channel_degraded bit (they are ONLINE - their degradation is what determines how well-backed this standby instance is). Winning it (this instance is ONLINE) sets peer_state to HOTSTANDBY/COLDSTANDBY from this instance's own sapi_dual_channel_get_status() instead - the peer is standing by for this instance, so it is this instance's own degradation that determines the peer's HOT/COLD, not whatever the peer last reported about itself.
  • Peer contact current, own_state already decided: stays decided; whichever side's HOTSTANDBY/COLDSTANDBY value is meaningful (the STANDBY side's own_state, or the ONLINE side's peer_state) refines each call using the same own-degradation-determines-the-*other*-side's-HOT/COLD rule above.

Invokes config->state_change_callback if either state changed.

Parameters
negotiatorInitialized negotiator. Must not be NULL.
receive_timeout_msMaximum time to wait for at least one inbound STATE frame if none is already pending on the attached channel (passed through to sapi_dual_channel_receive_state_frame()); 0 = do not block, only process what is already pending.
Returns
SAPI_STATUS_OK; SAPI_STATUS_INVALID_PARAM if negotiator is NULL.

Definition at line 161 of file sapi_dual_negotiator.c.

◆ sapi_dual_negotiator_get_own_state()

sapi_dual_state_t sapi_dual_negotiator_get_own_state ( const sapi_dual_negotiator_t * negotiator)

Returns this instance's own current sapi_dual_state_t.

Parameters
negotiatorNegotiator to query. May be NULL (returns SAPI_DUAL_STATE_IDLE, defensive default).
Returns
The current own state.

Definition at line 196 of file sapi_dual_negotiator.c.

◆ sapi_dual_negotiator_get_peer_state()

sapi_dual_state_t sapi_dual_negotiator_get_peer_state ( const sapi_dual_negotiator_t * negotiator)

Returns this instance's last-known view of the peer's sapi_dual_state_t.

Parameters
negotiatorNegotiator to query. May be NULL (returns SAPI_DUAL_STATE_IDLE, defensive default).
Returns
The current peer state.

Definition at line 205 of file sapi_dual_negotiator.c.

◆ sapi_dual_state_to_string()

const char * sapi_dual_state_to_string ( sapi_dual_state_t state)

Returns a short, static, human-readable string for a sapi_dual_state_t. Intended for diagnostics/logging only (e.g. as the Info= field of a sapi_log_write_event() call); never on a safety-decision path.

Parameters
stateValue to render; an unrecognized value (defensive only - not reachable through the public enum) renders as "UNKNOWN_STATE".
Returns
A NUL-terminated string literal - static storage, never NULL; caller must not modify or free it.

Local makros Local types declarations Local variables declarations Global variables declarations Local function declarations Global functions

Definition at line 21 of file sapi_dual_types.c.

◆ sapi_dual_channel_status_to_string()

const char * sapi_dual_channel_status_to_string ( sapi_dual_channel_status_t status)

Returns a short, static, human-readable string for a sapi_dual_channel_status_t. Same diagnostics-only contract as sapi_dual_state_to_string().

Parameters
statusValue to render; an unrecognized value renders as "UNKNOWN_STATUS".
Returns
A NUL-terminated string literal - static storage, never NULL.

Definition at line 50 of file sapi_dual_types.c.