Safe API Framework 0.1.0 build 384e6ae
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/rte_dual_channel.c
 "DualChannel" layer of ADR-020 - see rte_dual_channel.h.
file  src/redundancy/dual/rte_dual_msgchannel.c
 "Channel" layer of ADR-020 - see rte_dual_msgchannel.h.
file  src/redundancy/dual/rte_dual_negotiator.c
 Dual state negotiator of ADR-020 - see rte_dual_negotiator.h.
file  src/redundancy/dual/rte_dual_types.c
 Diagnostics-only string rendering for the rte_dual module's shared enums (ADR-020).

Data Structures

struct  rte_dual_channel_t
 One rte_dual_channel_t instance's state. Caller-owned storage; every field is private - reach it only through the functions below. More...
struct  rte_dual_channel_config_t
 Configuration for rte_dual_channel_init(). More...
struct  rte_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 rte_dual_ack_frame_t/rte_dual_state_frame_t stay naturally aligned. More...
struct  rte_dual_ack_frame_t
 RTE_DUAL_FRAME_KIND_ACK payload. More...
struct  rte_dual_state_frame_t
 RTE_DUAL_FRAME_KIND_STATE payload - rte_dual_negotiator_t's own periodic beacon. More...
struct  rte_dual_heartbeat_frame_t
 RTE_DUAL_FRAME_KIND_HEARTBEAT payload - connection maintenance heartbeat. More...
struct  rte_dual_msgchannel_t
 One rte_dual_msgchannel_t instance's state. Caller-owned storage (REQ-DUAL-MSGCHANNEL-001); opaque in practice, exposed here (not via RTE_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  rte_dual_msgchannel_config_t
 Configuration for rte_dual_msgchannel_init(). More...
struct  rte_dual_negotiator_t
 One rte_dual_negotiator_t instance's state. Caller-owned storage; every field is private - reach it only through the functions below. More...
struct  rte_dual_negotiator_config_t
 Configuration for rte_dual_negotiator_init(). More...

Macros

#define RTE_DUAL_CHANNEL_MAX_LINKS   4U
 Maximum number of redundant links one rte_dual_channel_t may be configured with. Fixed, not dynamic (REQ-OAL-COMMON-010).
#define RTE_DUAL_CHANNEL_MAX_PAYLOAD   (RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD - 4U)
 Max application payload bytes usable via rte_dual_channel_send()/_receive() - Layer-1's own RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD minus this layer's 4-byte rte_dual_frame_header_t.
#define RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD   248U
 Max payload bytes usable via rte_dual_msgchannel_send()/ _receive() - bounded by rte_vital_message_t's own fixed 248-byte payload field (rte_checksum.h).

Typedefs

typedef void(*) rte_dual_channel_status_callback_t(rte_dual_channel_status_t new_status, rte_dual_channel_status_t old_status, void *user_ctx)
 Optional callback invoked whenever rte_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 rte_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(*) rte_dual_negotiator_state_change_callback_t(rte_dual_state_t new_own_state, rte_dual_state_t old_own_state, rte_dual_state_t new_peer_state, rte_dual_state_t old_peer_state, void *user_ctx)
 Optional callback invoked whenever either the own or the peer rte_dual_state_t changes, from inside rte_dual_negotiator_execute().

Enumerations

enum  rte_dual_frame_kind_t { RTE_DUAL_FRAME_KIND_DATA = 0 , RTE_DUAL_FRAME_KIND_ACK = 1 , RTE_DUAL_FRAME_KIND_STATE = 2 , RTE_DUAL_FRAME_KIND_HEARTBEAT = 3 }
 Which kind of Layer-2 frame a given Layer-1 payload holds. Stored as the first byte of every rte_dual_channel_t frame so DATA, ACK, and STATE traffic sharing the same redundant links can never be misinterpreted as each other. More...
enum  rte_dual_state_t {
  RTE_DUAL_STATE_IDLE = 0 , RTE_DUAL_STATE_UNKNOWN = 1 , RTE_DUAL_STATE_ONLINE = 2 , RTE_DUAL_STATE_HOTSTANDBY = 3 ,
  RTE_DUAL_STATE_COLDSTANDBY = 4
}
 State of one instance in a dual (two-redundant-instance) relationship, as decided by rte_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 rte_dual_negotiator_get_own_state()/_get_peer_state(). More...
enum  rte_dual_channel_status_t { RTE_DUAL_CHANNEL_STATUS_DOWN = 0 , RTE_DUAL_CHANNEL_STATUS_DEGRADED = 1 , RTE_DUAL_CHANNEL_STATUS_FULL = 2 }
 Aggregate connection status of a rte_dual_channel_t across all of its configured redundant links (ADR-020 section 2). Independent of rte_dual_state_t: losing some (not all) of the redundant links degrades this status without by itself forcing any rte_dual_state_t transition - only total loss (DOWN) is a negotiator-relevant liveness event. More...

Functions

rte_status_t rte_dual_channel_init (rte_dual_channel_t *channel, const rte_dual_channel_config_t *config)
 Initializes a rte_dual_channel_t: initializes every configured redundant link (rte_dual_msgchannel_init()) and starts rte_dual_channel_get_status() at RTE_DUAL_CHANNEL_STATUS_DOWN (no traffic has been sent/received yet).
rte_status_t rte_dual_channel_send (rte_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.
rte_status_t rte_dual_channel_receive (rte_dual_channel_t *channel, uint8_t *out_payload, uint8_t max_size, rte_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 rte_dual_channel_send()'s own doc) if none was already staged from a previous rte_dual_channel_send() call's own incidental polling.
rte_status_t rte_dual_channel_send_heartbeat (rte_dual_channel_t *channel, uint32_t *out_ack_count)
 Transmits a connection maintenance heartbeat frame across every configured link and waits up to ack_timeout_ms for an ACK on each.
rte_status_t rte_dual_channel_send_state_frame (rte_dual_channel_t *channel, rte_dual_state_t state, bool channel_degraded, uint64_t timestamp_ms)
 Sends a rte_dual_state_frame_t (rte_dual_frames.h) on every configured redundant link - fire-and-forget, no ACK wait (unlike rte_dual_channel_send()'s DATA frames), matching the periodic-beacon nature of state negotiation. Intended to be called by a rte_dual_negotiator_t, not directly by application code - see ADR-020 section 3.
rte_status_t rte_dual_channel_receive_state_frame (rte_dual_channel_t *channel, rte_duration_ms_t timeout_ms, rte_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 rte_dual_negotiator_t - see ADR-020 section 3.
rte_dual_channel_status_t rte_dual_channel_get_status (const rte_dual_channel_t *channel)
 Returns the aggregate connection status across every configured redundant link, as of the most recent rte_dual_channel_send() call (ADR-020 section 2). RTE_DUAL_CHANNEL_STATUS_DOWN before the first send.
bool rte_dual_channel_is_link_up (const rte_dual_channel_t *channel, uint32_t link_index)
 Returns whether one specific configured link is currently considered up, as of the most recent rte_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 rte_dual_channel_get_status() for the aggregate).
rte_status_t rte_dual_channel_set_resync_on_sequence_error (rte_dual_channel_t *channel, bool enable)
rte_status_t rte_dual_msgchannel_set_resync_on_sequence_error (rte_dual_msgchannel_t *channel, bool enable)
 Turns sequence resynchronisation on or off (default off after init).
rte_status_t rte_dual_msgchannel_init (rte_dual_msgchannel_t *channel, const rte_dual_msgchannel_config_t *config)
 Initializes a rte_dual_msgchannel_t: starts both sequence counters at 0.
rte_status_t rte_dual_msgchannel_send (rte_dual_msgchannel_t *channel, const uint8_t *payload, uint8_t payload_size, rte_duration_ms_t timeout_ms, uint32_t *out_sequence)
 Wraps payload in a rte_vital_message_t (this channel's own sender_id and next sequence_number) and sends it over config->link, blocking at most timeout_ms.
rte_status_t rte_dual_msgchannel_receive (rte_dual_msgchannel_t *channel, uint8_t *out_payload, uint8_t payload_max_size, rte_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 (rte_checksum_vital_message_verify() against channel->expected_sequence) and its sender_id against config->expected_peer_id, and extracts the payload.
rte_status_t rte_dual_msgchannel_reset_sequence (rte_dual_msgchannel_t *channel)
 Resets both sequence counters to 0. Intended to be called by the caller (typically rte_dual_channel_t) exactly once, immediately after config->link has been freshly (re)established with the peer - the same idea as a mechanism RBC_GP once had and has since removed (2026-10-04): a cycle-counter reset after a peer-link reconnect. A fresh link needs a fresh, mutually agreed starting sequence rather than fighting over whatever counters were left over from before the disconnect.
rte_status_t rte_dual_negotiator_init (rte_dual_negotiator_t *negotiator, const rte_dual_negotiator_config_t *config)
 Initializes a rte_dual_negotiator_t. Captures this instance's own startup timestamp once (rte_timer_now(), degrading to 0 if no timer OSAdapter is registered - same best-effort posture as rte_log_write_event()'s Timestamp field) for use on every beacon this negotiator ever sends - see rte_dual_state_frame_t's own doc on why this must stay fixed - UNLESS config->resume_own_state/resume_own_startup_timestamp_ms opt into resuming a prior identity instead (see their own doc). peer_state always starts at RTE_DUAL_STATE_IDLE; own_state does too unless a resume was requested.
rte_status_t rte_dual_negotiator_execute (rte_dual_negotiator_t *negotiator, rte_duration_ms_t receive_timeout_ms)
 Drives one round of state negotiation: refreshes own_channel_degraded from the attached channel's current rte_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 rte_dual_state_t:
rte_dual_state_t rte_dual_negotiator_get_own_state (const rte_dual_negotiator_t *negotiator)
 Returns this instance's own current rte_dual_state_t.
rte_dual_state_t rte_dual_negotiator_get_peer_state (const rte_dual_negotiator_t *negotiator)
 Returns this instance's last-known view of the peer's rte_dual_state_t.
uint64_t rte_dual_negotiator_get_own_startup_timestamp_ms (const rte_dual_negotiator_t *negotiator)
 Returns this instance's own startup timestamp (captured at rte_dual_negotiator_init() time, or resumed from config - see rte_dual_negotiator_config_t::resume_own_startup_timestamp_ms). Diagnostic use (e.g. logging alongside the peer's own, below) - never itself consulted by this module's own tie-break logic after init.
uint64_t rte_dual_negotiator_get_peer_startup_timestamp_ms (const rte_dual_negotiator_t *negotiator)
 Returns this instance's last-known view of the peer's own startup timestamp (0/unset until the first valid STATE frame arrives). Diagnostic use only, same posture as rte_dual_negotiator_get_own_startup_timestamp_ms() above.
uint32_t rte_dual_negotiator_get_own_id (const rte_dual_negotiator_t *negotiator)
 Returns this instance's own tie-break identifier (rte_dual_negotiator_config_t::own_id at init time). Diagnostic use only.
uint32_t rte_dual_negotiator_get_peer_id (const rte_dual_negotiator_t *negotiator)
 Returns the expected peer's tie-break identifier (rte_dual_negotiator_config_t::peer_id at init time). Diagnostic use only.
const char * rte_dual_state_to_string (rte_dual_state_t state)
 Returns a short, static, human-readable string for a rte_dual_state_t. Intended for diagnostics/logging only (e.g. as the Info= field of a rte_log_write_event() call); never on a safety-decision path.
const char * rte_dual_channel_status_to_string (rte_dual_channel_status_t status)
 Returns a short, static, human-readable string for a rte_dual_channel_status_t. Same diagnostics-only contract as rte_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

◆ RTE_DUAL_CHANNEL_MAX_LINKS

#define RTE_DUAL_CHANNEL_MAX_LINKS   4U

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

Definition at line 66 of file rte_dual_channel.h.

◆ RTE_DUAL_CHANNEL_MAX_PAYLOAD

#define RTE_DUAL_CHANNEL_MAX_PAYLOAD   (RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD - 4U)

Max application payload bytes usable via rte_dual_channel_send()/_receive() - Layer-1's own RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD minus this layer's 4-byte rte_dual_frame_header_t.

Definition at line 72 of file rte_dual_channel.h.

◆ RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD

#define RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD   248U

Max payload bytes usable via rte_dual_msgchannel_send()/ _receive() - bounded by rte_vital_message_t's own fixed 248-byte payload field (rte_checksum.h).

Definition at line 58 of file rte_dual_msgchannel.h.

Typedef Documentation

◆ rte_dual_channel_status_callback_t

typedef void(*) rte_dual_channel_status_callback_t(rte_dual_channel_status_t new_status, rte_dual_channel_status_t old_status, void *user_ctx)

Optional callback invoked whenever rte_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 rte_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 rte_dual_channel_config_t.

Definition at line 85 of file rte_dual_channel.h.

◆ rte_dual_negotiator_state_change_callback_t

typedef void(*) rte_dual_negotiator_state_change_callback_t(rte_dual_state_t new_own_state, rte_dual_state_t old_own_state, rte_dual_state_t new_peer_state, rte_dual_state_t old_peer_state, void *user_ctx)

Optional callback invoked whenever either the own or the peer rte_dual_state_t changes, from inside rte_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 rte_dual_negotiator_config_t.

Definition at line 71 of file rte_dual_negotiator.h.

Enumeration Type Documentation

◆ rte_dual_frame_kind_t

Which kind of Layer-2 frame a given Layer-1 payload holds. Stored as the first byte of every rte_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
RTE_DUAL_FRAME_KIND_DATA 

Application payload (rte_dual_channel_send()/_receive()); expects a RTE_DUAL_FRAME_KIND_ACK frame back.

RTE_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).

RTE_DUAL_FRAME_KIND_STATE 

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

RTE_DUAL_FRAME_KIND_HEARTBEAT 

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

Definition at line 41 of file rte_dual_frames.h.

◆ rte_dual_state_t

State of one instance in a dual (two-redundant-instance) relationship, as decided by rte_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 rte_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 rte_dual_state_frame_t::state): do not renumber existing entries, only append.

Enumerator
RTE_DUAL_STATE_IDLE 

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

RTE_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.

RTE_DUAL_STATE_ONLINE 

This instance is the active one.

RTE_DUAL_STATE_HOTSTANDBY 

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

RTE_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 rte_dual_types.h.

◆ rte_dual_channel_status_t

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

Enumerator
RTE_DUAL_CHANNEL_STATUS_DOWN 

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

RTE_DUAL_CHANNEL_STATUS_DEGRADED 

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

RTE_DUAL_CHANNEL_STATUS_FULL 

Every configured redundant link is up.

Definition at line 65 of file rte_dual_types.h.

Function Documentation

◆ rte_dual_channel_init()

rte_status_t rte_dual_channel_init ( rte_dual_channel_t * channel,
const rte_dual_channel_config_t * config )

Initializes a rte_dual_channel_t: initializes every configured redundant link (rte_dual_msgchannel_init()) and starts rte_dual_channel_get_status() at RTE_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, RTE_DUAL_CHANNEL_MAX_LINKS]; every config->links[0..link_count-1] must not be NULL.
Returns
RTE_STATUS_OK; RTE_STATUS_INVALID_PARAM otherwise.

Definition at line 183 of file rte_dual_channel.c.

◆ rte_dual_channel_send()

rte_status_t rte_dual_channel_send ( rte_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 rte_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 <= RTE_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
RTE_STATUS_OK if at least one link ACKed; RTE_STATUS_TIMEOUT if none did (data was still transmitted-attempted on every link; this reports delivery confirmation, not transmission attempt); RTE_STATUS_HARDWARE_FAULT instead of RTE_STATUS_TIMEOUT if no link ACKed and at least one link reported a hard fault (REQ-DUAL-CHANNEL-008 below); RTE_STATUS_INVALID_PARAM for a bad argument.

REQ-DUAL-CHANNEL-007: The per-link ACK wait keeps polling within config->ack_timeout_ms even when rte_timer_now() shows no progress between polls, bounded by a fixed cap (RTE_DUAL_CHANNEL_STALL_POLL_LIMIT) on consecutive no-progress polls. REQ-DUAL-CHANNEL-008: If no link ACKed and at least one link's own send/receive reported RTE_STATUS_HARDWARE_FAULT, that status is returned instead of RTE_STATUS_TIMEOUT; RTE_STATUS_DATA_CORRUPTION on an otherwise healthy link still folds into RTE_STATUS_TIMEOUT.

Definition at line 353 of file rte_dual_channel.c.

◆ rte_dual_channel_receive()

rte_status_t rte_dual_channel_receive ( rte_dual_channel_t * channel,
uint8_t * out_payload,
uint8_t max_size,
rte_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 rte_dual_channel_send()'s own doc) if none was already staged from a previous rte_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
RTE_STATUS_OK; RTE_STATUS_TIMEOUT if nothing arrived in time; RTE_STATUS_HARDWARE_FAULT instead of RTE_STATUS_TIMEOUT if no link produced a usable frame and at least one link's receive reported a hard fault (REQ-DUAL-CHANNEL-008 below; a link's RTE_STATUS_DATA_CORRUPTION, or any other receive status that is not a hard fault, folds into RTE_STATUS_TIMEOUT); RTE_STATUS_INVALID_PARAM if a staged frame is larger than max_size (not silently truncated) or for a bad argument.

REQ-DUAL-CHANNEL-008: If no link produced a usable frame and at least one link's own receive reported RTE_STATUS_HARDWARE_FAULT, that status is returned instead of RTE_STATUS_TIMEOUT; RTE_STATUS_DATA_CORRUPTION on an otherwise healthy link still folds into RTE_STATUS_TIMEOUT.

Definition at line 426 of file rte_dual_channel.c.

◆ rte_dual_channel_send_heartbeat()

rte_status_t rte_dual_channel_send_heartbeat ( rte_dual_channel_t * channel,
uint32_t * out_ack_count )

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
RTE_STATUS_OK if at least one link acknowledged; RTE_STATUS_HARDWARE_FAULT if no link acknowledged and at least one link reported a hard fault (on its send or while waiting for its ACK); RTE_STATUS_TIMEOUT if no link acknowledged within timeout and none reported a hard fault (also when no link is configured); RTE_STATUS_INVALID_PARAM if channel is NULL.

Definition at line 491 of file rte_dual_channel.c.

◆ rte_dual_channel_send_state_frame()

rte_status_t rte_dual_channel_send_state_frame ( rte_dual_channel_t * channel,
rte_dual_state_t state,
bool channel_degraded,
uint64_t timestamp_ms )

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

Parameters
channelInitialized channel. Must not be NULL.
stateThis instance's own rte_dual_state_t to advertise.
channel_degradedThis instance's own current rte_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 rte_timer_now() itself: a rte_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 rte_dual_state_frame_t's own doc.
Returns
RTE_STATUS_OK if the frame was transmitted on at least one link; RTE_STATUS_TIMEOUT if every link's send failed/timed out; RTE_STATUS_INVALID_PARAM if channel is NULL.

Definition at line 550 of file rte_dual_channel.c.

◆ rte_dual_channel_receive_state_frame()

rte_status_t rte_dual_channel_receive_state_frame ( rte_dual_channel_t * channel,
rte_duration_ms_t timeout_ms,
rte_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 rte_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
RTE_STATUS_OK; RTE_STATUS_TIMEOUT if none arrived in time; RTE_STATUS_INVALID_PARAM for a bad argument.

Definition at line 584 of file rte_dual_channel.c.

◆ rte_dual_channel_get_status()

rte_dual_channel_status_t rte_dual_channel_get_status ( const rte_dual_channel_t * channel)

Returns the aggregate connection status across every configured redundant link, as of the most recent rte_dual_channel_send() call (ADR-020 section 2). RTE_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 618 of file rte_dual_channel.c.

◆ rte_dual_channel_is_link_up()

bool rte_dual_channel_is_link_up ( const rte_dual_channel_t * channel,
uint32_t link_index )

Returns whether one specific configured link is currently considered up, as of the most recent rte_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 rte_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 627 of file rte_dual_channel.c.

◆ rte_dual_channel_set_resync_on_sequence_error()

rte_status_t rte_dual_channel_set_resync_on_sequence_error ( rte_dual_channel_t * channel,
bool enable )

Enables sequence resynchronisation on every link of channel (see rte_dual_msgchannel_set_resync_on_sequence_error()).

Definition at line 636 of file rte_dual_channel.c.

◆ rte_dual_msgchannel_set_resync_on_sequence_error()

rte_status_t rte_dual_msgchannel_set_resync_on_sequence_error ( rte_dual_msgchannel_t * channel,
bool enable )

Turns sequence resynchronisation on or off (default off after init).

When on, an inbound frame with a valid CRC and the right sender_id but the wrong sequence number is still rejected (never delivered), but the expected sequence is set to that frame's number plus one, so the next frame from the same peer is accepted. For a link whose peer can restart on its own (the inter-site negotiation link): each end restarts at sequence 0, the surviving end keeps expecting the old number, and without this both ends reject each other for good. CRC and sender checks stay in force.

Returns
RTE_STATUS_OK; RTE_STATUS_INVALID_PARAM for a NULL channel.

Definition at line 162 of file rte_dual_msgchannel.c.

◆ rte_dual_msgchannel_init()

rte_status_t rte_dual_msgchannel_init ( rte_dual_msgchannel_t * channel,
const rte_dual_msgchannel_config_t * config )

Initializes a rte_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
RTE_STATUS_OK; RTE_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 rte_dual_msgchannel.c.

◆ rte_dual_msgchannel_send()

rte_status_t rte_dual_msgchannel_send ( rte_dual_msgchannel_t * channel,
const uint8_t * payload,
uint8_t payload_size,
rte_duration_ms_t timeout_ms,
uint32_t * out_sequence )

Wraps payload in a rte_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 <= RTE_DUAL_MSGCHANNEL_MAX_PAYLOAD.
timeout_msMaximum time to wait for the underlying rte_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 rte_dual_channel.h). May be NULL.
Returns
RTE_STATUS_OK (channel->next_sequence has been incremented); RTE_STATUS_INVALID_PARAM; whatever rte_netlink_send() or rte_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 38 of file rte_dual_msgchannel.c.

◆ rte_dual_msgchannel_receive()

rte_status_t rte_dual_msgchannel_receive ( rte_dual_msgchannel_t * channel,
uint8_t * out_payload,
uint8_t payload_max_size,
rte_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 (rte_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 rte_dual_channel.h). May be NULL.
Returns
RTE_STATUS_OK (channel->expected_sequence has been advanced to this frame's sequence_number + 1); RTE_STATUS_INVALID_PARAM; RTE_STATUS_TIMEOUT if no frame arrives in time; RTE_STATUS_DATA_CORRUPTION if the frame's CRC-64 or sequence continuity check fails (channel->expected_sequence is left unchanged - see rte_dual_msgchannel_reset_sequence() for how to recover after a link (re)establishment rather than staying permanently out of sync); RTE_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 81 of file rte_dual_msgchannel.c.

◆ rte_dual_msgchannel_reset_sequence()

rte_status_t rte_dual_msgchannel_reset_sequence ( rte_dual_msgchannel_t * channel)

Resets both sequence counters to 0. Intended to be called by the caller (typically rte_dual_channel_t) exactly once, immediately after config->link has been freshly (re)established with the peer - the same idea as a mechanism RBC_GP once had and has since removed (2026-10-04): a cycle-counter reset after a peer-link reconnect. 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
RTE_STATUS_OK; RTE_STATUS_INVALID_PARAM if channel is NULL.

Definition at line 149 of file rte_dual_msgchannel.c.

◆ rte_dual_negotiator_init()

rte_status_t rte_dual_negotiator_init ( rte_dual_negotiator_t * negotiator,
const rte_dual_negotiator_config_t * config )

Initializes a rte_dual_negotiator_t. Captures this instance's own startup timestamp once (rte_timer_now(), degrading to 0 if no timer OSAdapter is registered - same best-effort posture as rte_log_write_event()'s Timestamp field) for use on every beacon this negotiator ever sends - see rte_dual_state_frame_t's own doc on why this must stay fixed - UNLESS config->resume_own_state/resume_own_startup_timestamp_ms opt into resuming a prior identity instead (see their own doc). peer_state always starts at RTE_DUAL_STATE_IDLE; own_state does too unless a resume was requested.

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

Definition at line 127 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_execute()

rte_status_t rte_dual_negotiator_execute ( rte_dual_negotiator_t * negotiator,
rte_duration_ms_t receive_timeout_ms )

Drives one round of state negotiation: refreshes own_channel_degraded from the attached channel's current rte_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 rte_dual_state_t:

  • Never yet heard from the peer: both states are RTE_DUAL_STATE_IDLE.
  • Heard from the peer before but not within config->peer_lost_timeout_ms: peer_state becomes RTE_DUAL_STATE_UNKNOWN; own_state becomes RTE_DUAL_STATE_UNKNOWN too UNLESS it was already RTE_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 rte_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 rte_dual_channel_receive_state_frame()); 0 = do not block, only process what is already pending.
Returns
RTE_STATUS_OK; RTE_STATUS_INVALID_PARAM if negotiator is NULL.

Definition at line 162 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_own_state()

rte_dual_state_t rte_dual_negotiator_get_own_state ( const rte_dual_negotiator_t * negotiator)

Returns this instance's own current rte_dual_state_t.

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

Definition at line 197 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_peer_state()

rte_dual_state_t rte_dual_negotiator_get_peer_state ( const rte_dual_negotiator_t * negotiator)

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

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

Definition at line 206 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_own_startup_timestamp_ms()

uint64_t rte_dual_negotiator_get_own_startup_timestamp_ms ( const rte_dual_negotiator_t * negotiator)

Returns this instance's own startup timestamp (captured at rte_dual_negotiator_init() time, or resumed from config - see rte_dual_negotiator_config_t::resume_own_startup_timestamp_ms). Diagnostic use (e.g. logging alongside the peer's own, below) - never itself consulted by this module's own tie-break logic after init.

Parameters
negotiatorNegotiator to query. May be NULL (returns 0).
Returns
The own startup timestamp, in milliseconds.

Definition at line 215 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_peer_startup_timestamp_ms()

uint64_t rte_dual_negotiator_get_peer_startup_timestamp_ms ( const rte_dual_negotiator_t * negotiator)

Returns this instance's last-known view of the peer's own startup timestamp (0/unset until the first valid STATE frame arrives). Diagnostic use only, same posture as rte_dual_negotiator_get_own_startup_timestamp_ms() above.

Parameters
negotiatorNegotiator to query. May be NULL (returns 0).
Returns
The peer's own last-reported startup timestamp, in milliseconds.

Definition at line 224 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_own_id()

uint32_t rte_dual_negotiator_get_own_id ( const rte_dual_negotiator_t * negotiator)

Returns this instance's own tie-break identifier (rte_dual_negotiator_config_t::own_id at init time). Diagnostic use only.

Parameters
negotiatorNegotiator to query. May be NULL (returns 0).
Returns
The own tie-break identifier.

Definition at line 233 of file rte_dual_negotiator.c.

◆ rte_dual_negotiator_get_peer_id()

uint32_t rte_dual_negotiator_get_peer_id ( const rte_dual_negotiator_t * negotiator)

Returns the expected peer's tie-break identifier (rte_dual_negotiator_config_t::peer_id at init time). Diagnostic use only.

Parameters
negotiatorNegotiator to query. May be NULL (returns 0).
Returns
The expected peer's tie-break identifier.

Definition at line 242 of file rte_dual_negotiator.c.

◆ rte_dual_state_to_string()

const char * rte_dual_state_to_string ( rte_dual_state_t state)

Returns a short, static, human-readable string for a rte_dual_state_t. Intended for diagnostics/logging only (e.g. as the Info= field of a rte_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 rte_dual_types.c.

◆ rte_dual_channel_status_to_string()

const char * rte_dual_channel_status_to_string ( rte_dual_channel_status_t status)

Returns a short, static, human-readable string for a rte_dual_channel_status_t. Same diagnostics-only contract as rte_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 rte_dual_types.c.