Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
sapi_dual_channel.c
Go to the documentation of this file.
1
7
8#include <string.h>
9
11
17#define SAPI_DUAL_CHANNEL_STALL_POLL_LIMIT 32U
18
26
27static sapi_dual_channel_status_t dual_channel_compute_status(const sapi_dual_channel_t *channel)
28{
29 uint32_t up_count = 0U;
30 uint32_t i;
31
32 for (i = 0U; i < channel->link_count; i++)
33 {
34 if (channel->link_up[i])
35 {
36 up_count++;
37 }
38 }
39
40 if (up_count == 0U)
41 {
43 }
44 if (up_count == channel->link_count)
45 {
47 }
49}
50
51static void dual_channel_update_status(sapi_dual_channel_t *channel)
52{
53 sapi_dual_channel_status_t new_status = dual_channel_compute_status(channel);
54
55 if (new_status != channel->last_status)
56 {
57 sapi_dual_channel_status_t old_status = channel->last_status;
58
59 channel->last_status = new_status;
60 if (channel->status_callback != NULL)
61 {
62 channel->status_callback(new_status, old_status, channel->status_callback_ctx);
63 }
64 }
65}
66
75 sapi_duration_ms_t timeout_ms, dual_poll_result_t *out_result)
76{
78 uint8_t raw_size = 0U;
79 uint32_t sequence = 0U;
80 sapi_status_t status;
82
83 out_result->matched = false;
84
85 status = sapi_dual_msgchannel_receive(&channel->links[link_index], raw, (uint8_t)sizeof(raw), timeout_ms,
86 &raw_size, &sequence);
87 if (status != SAPI_STATUS_OK)
88 {
89 return status;
90 }
91 if (raw_size < (uint8_t)sizeof(header))
92 {
93 /* Too short to even hold this layer's own header - treat as a
94 * defended-integrity failure, same family as a Layer-1 CRC/
95 * sequence failure, not a silently-ignored malformed frame. */
97 }
98 (void)memcpy(&header, raw, sizeof(header));
99
100 switch ((sapi_dual_frame_kind_t)header.kind)
101 {
103 {
104 uint8_t app_size = (uint8_t)(raw_size - (uint8_t)sizeof(header));
106
107 if (app_size <= SAPI_DUAL_CHANNEL_MAX_PAYLOAD)
108 {
109 (void)memcpy(channel->pending_data, &raw[sizeof(header)], app_size);
110 channel->pending_data_size = app_size;
111 channel->pending_data_valid = true;
112 }
113
114 /* Auto-ACK, best-effort/fire-and-forget: a lost ACK is
115 * observable to the sender as its own timeout on this link
116 * (ADR-020 section 2), not silently hidden here. */
117 ack.header.kind = (uint8_t)SAPI_DUAL_FRAME_KIND_ACK;
118 ack.header.reserved[0] = 0U;
119 ack.header.reserved[1] = 0U;
120 ack.header.reserved[2] = 0U;
121 ack.acked_sequence = sequence;
122 (void)sapi_dual_msgchannel_send(&channel->links[link_index], (const uint8_t *)&ack, (uint8_t)sizeof(ack),
123 channel->ack_timeout_ms, NULL);
124
125 out_result->matched = true;
126 out_result->kind = SAPI_DUAL_FRAME_KIND_DATA;
127 break;
128 }
130 {
132
133 if (raw_size < (uint8_t)sizeof(ack))
134 {
136 }
137 (void)memcpy(&ack, raw, sizeof(ack));
138
139 out_result->matched = true;
140 out_result->kind = SAPI_DUAL_FRAME_KIND_ACK;
141 out_result->ack_sequence = ack.acked_sequence;
142 break;
143 }
145 {
146 if (raw_size < (uint8_t)sizeof(sapi_dual_state_frame_t))
147 {
149 }
150 (void)memcpy(&channel->pending_state, raw, sizeof(channel->pending_state));
151 channel->pending_state_valid = true;
152
153 out_result->matched = true;
154 out_result->kind = SAPI_DUAL_FRAME_KIND_STATE;
155 break;
156 }
158 {
160
161 /* Auto-ACK, best-effort/fire-and-forget: acknowledges heartbeat */
162 ack.header.kind = (uint8_t)SAPI_DUAL_FRAME_KIND_ACK;
163 ack.header.reserved[0] = 0U;
164 ack.header.reserved[1] = 0U;
165 ack.header.reserved[2] = 0U;
166 ack.acked_sequence = sequence;
167 (void)sapi_dual_msgchannel_send(&channel->links[link_index], (const uint8_t *)&ack, (uint8_t)sizeof(ack),
168 channel->ack_timeout_ms, NULL);
169
170 out_result->matched = true;
172 break;
173 }
174 default:
175 /* Unrecognized kind - defensive only (not reachable through
176 * a conforming sender), ignore rather than fail. */
177 break;
178 }
179
180 return SAPI_STATUS_OK;
181}
182
184{
185 uint32_t i;
186
187 if ((channel == NULL) || (config == NULL))
188 {
190 }
191 if ((config->link_count == 0U) || (config->link_count > SAPI_DUAL_CHANNEL_MAX_LINKS))
192 {
194 }
195 for (i = 0U; i < config->link_count; i++)
196 {
197 if (config->links[i] == NULL)
198 {
200 }
201 }
202
203 channel->link_count = config->link_count;
204 for (i = 0U; i < config->link_count; i++)
205 {
207
208 msgcfg.link = config->links[i];
209 msgcfg.sender_id = config->sender_id;
210 msgcfg.expected_peer_id = config->expected_peer_id;
211 (void)sapi_dual_msgchannel_init(&channel->links[i], &msgcfg);
212 channel->link_up[i] = false;
213 }
214 for (i = config->link_count; i < SAPI_DUAL_CHANNEL_MAX_LINKS; i++)
215 {
216 channel->link_up[i] = false;
217 }
218
219 channel->ack_timeout_ms = config->ack_timeout_ms;
220 channel->status_callback = config->status_callback;
221 channel->status_callback_ctx = config->status_callback_ctx;
222 channel->last_status = SAPI_DUAL_CHANNEL_STATUS_DOWN;
223
224 channel->pending_data_valid = false;
225 channel->pending_data_size = 0U;
226 channel->pending_state_valid = false;
227 (void)memset(&channel->pending_state, 0, sizeof(channel->pending_state));
228
229 return SAPI_STATUS_OK;
230}
231
232sapi_status_t sapi_dual_channel_send(sapi_dual_channel_t *channel, const uint8_t *payload, uint8_t payload_size,
233 uint32_t *out_ack_link_count)
234{
235 uint8_t frame[(size_t)SAPI_DUAL_CHANNEL_MAX_PAYLOAD + sizeof(sapi_dual_frame_header_t)];
236 uint8_t frame_size;
237 uint32_t i;
238 uint32_t ack_count = 0U;
239 /* REQ-DUAL-CHANNEL-008: a link whose own send/receive reports
240 * something other than SAPI_STATUS_OK/SAPI_STATUS_TIMEOUT (e.g.
241 * SAPI_STATUS_HARDWARE_FAULT from a closed/reset connection) is
242 * genuinely broken, not just quiet - see this function's own header
243 * for why collapsing that into the same generic TIMEOUT every other
244 * "no ACK yet" case returns hid real transport failures from every
245 * caller. Kept as the FIRST such status seen across all links this
246 * call, not the last - an arbitrary but stable choice among possibly
247 * several simultaneous failures. */
248 bool saw_hard_fault = false;
249 sapi_status_t hard_fault_status = SAPI_STATUS_OK;
250
251 if (channel == NULL)
252 {
254 }
255 if ((payload == NULL) && (payload_size != 0U))
256 {
258 }
259 if (payload_size > SAPI_DUAL_CHANNEL_MAX_PAYLOAD)
260 {
262 }
263
264 {
266
267 header.kind = (uint8_t)SAPI_DUAL_FRAME_KIND_DATA;
268 header.reserved[0] = 0U;
269 header.reserved[1] = 0U;
270 header.reserved[2] = 0U;
271 (void)memcpy(frame, &header, sizeof(header));
272 if (payload_size > 0U)
273 {
274 (void)memcpy(&frame[sizeof(header)], payload, payload_size);
275 }
276 frame_size = (uint8_t)(sizeof(header) + payload_size);
277 }
278
279 for (i = 0U; i < channel->link_count; i++)
280 {
281 uint32_t sent_sequence = 0U;
282 sapi_status_t send_status;
283 bool link_now_up = false;
284
285 send_status = sapi_dual_msgchannel_send(&channel->links[i], frame, frame_size, channel->ack_timeout_ms,
286 &sent_sequence);
287 if ((send_status == SAPI_STATUS_HARDWARE_FAULT) && (!saw_hard_fault))
288 {
289 saw_hard_fault = true;
290 hard_fault_status = send_status;
291 }
292 if (send_status == SAPI_STATUS_OK)
293 {
294 sapi_duration_ms_t remaining = channel->ack_timeout_ms;
295 sapi_timestamp_ms_t start_ms = 0U;
296 /* Bounds how many consecutive polls may complete without
297 * sapi_timer_now() showing any measurable progress since
298 * start_ms, before this loop gives up on this link for this
299 * round - see the "else" branch below for why this can
300 * legitimately happen more than once and must not itself be
301 * unbounded. SAPI_DUAL_CHANNEL_STALL_POLL_LIMIT is a fixed,
302 * generous cap (a real exchange needs at most a handful of
303 * iterations - one per DATA/STATE/foreign-ACK frame handled
304 * as a side effect before this link's own matching ACK
305 * arrives), not a tuned timing value. */
306 uint32_t stall_polls = 0U;
307
308 (void)sapi_timer_now(&start_ms);
309
310 while ((remaining > 0U) && (stall_polls < SAPI_DUAL_CHANNEL_STALL_POLL_LIMIT))
311 {
312 dual_poll_result_t result;
313 sapi_status_t poll_status;
314 sapi_timestamp_ms_t now_ms = 0U;
315
316 poll_status = dual_channel_poll_link_once(channel, i, remaining, &result);
317 if ((poll_status == SAPI_STATUS_OK) && result.matched && (result.kind == SAPI_DUAL_FRAME_KIND_ACK)
318 && (result.ack_sequence == sent_sequence))
319 {
320 link_now_up = true;
321 break;
322 }
323 if (poll_status == SAPI_STATUS_HARDWARE_FAULT)
324 {
325 /* Genuinely broken (not just "no ACK frame arrived
326 * yet this poll"), e.g. HARDWARE_FAULT from the peer
327 * having closed/reset the connection - see this
328 * function's own header. No point continuing to poll
329 * a dead link for the rest of its own ack_timeout_ms
330 * budget. */
331 if (!saw_hard_fault)
332 {
333 saw_hard_fault = true;
334 hard_fault_status = poll_status;
335 }
336 break;
337 }
338
339 /* Recompute the remaining budget from wall-clock elapsed
340 * time, not a fixed per-iteration decrement - a DATA/STATE
341 * frame handled as a side effect above may have consumed
342 * an arbitrary fraction of this link's own timeout
343 * already. */
344 (void)sapi_timer_now(&now_ms);
345 if (now_ms > start_ms)
346 {
347 sapi_timestamp_ms_t elapsed = now_ms - start_ms;
348
349 if (elapsed >= (sapi_timestamp_ms_t)channel->ack_timeout_ms)
350 {
351 remaining = 0U;
352 }
353 else
354 {
355 remaining = channel->ack_timeout_ms - (sapi_duration_ms_t)elapsed;
356 }
357 stall_polls = 0U;
358 }
359 else
360 {
361 /* REQ-DUAL-CHANNEL-007: now_ms == start_ms - no
362 * measurable time has passed on sapi_timer_now()'s own
363 * tick resolution since this round started. This used
364 * to be treated as "no timer
365 * backend available, give up after one attempt"
366 * (REQ-OAL-LOG-001-style "never spin on an
367 * unmeasurable interval"), but a real localhost round
368 * trip (connect, send DATA, receive the peer's own
369 * DATA, auto-ACK it, receive the peer's own ACK)
370 * routinely completes inside a single millisecond
371 * tick, which made that same guard misfire as a false
372 * "no timer" abort after exactly one poll - discovered
373 * live via SITE's migration to sapi_safechannel
374 * (ADR-022), where both sites send their negotiation
375 * beacon at nearly the same instant over a real
376 * loopback TCP link. `remaining` is left unchanged so
377 * a fast exchange like that one gets the extra polls
378 * it needs; stall_polls bounds how many such
379 * no-progress iterations are allowed before this loop
380 * gives up anyway, so a link with a genuinely
381 * non-advancing or absent timer still cannot spin
382 * forever. */
383 stall_polls++;
384 }
385 }
386 }
387
388 channel->link_up[i] = link_now_up;
389 if (link_now_up)
390 {
391 ack_count++;
392 }
393 }
394
395 if (out_ack_link_count != NULL)
396 {
397 *out_ack_link_count = ack_count;
398 }
399
400 dual_channel_update_status(channel);
401
402 if (ack_count > 0U)
403 {
404 return SAPI_STATUS_OK;
405 }
406 return saw_hard_fault ? hard_fault_status : SAPI_STATUS_TIMEOUT;
407}
408
409sapi_status_t sapi_dual_channel_receive(sapi_dual_channel_t *channel, uint8_t *out_payload, uint8_t max_size,
410 sapi_duration_ms_t timeout_ms, uint8_t *out_size)
411{
412 if ((channel == NULL) || (out_payload == NULL) || (out_size == NULL) || (max_size == 0U))
413 {
415 }
416
417 {
418 bool saw_hard_fault = false;
419 sapi_status_t hard_fault_status = SAPI_STATUS_OK;
420
421 if (!channel->pending_data_valid)
422 {
423 uint32_t i;
424 sapi_duration_ms_t per_link_timeout = (channel->link_count > 0U) ? (timeout_ms / channel->link_count) : 0U;
425
426 /* Sweeps every configured link every call, even after an
427 * earlier link in this same sweep already staged a DATA frame -
428 * stopping early here would let a link with a shorter path (or
429 * one that always has traffic) starve every other redundant
430 * link of its own auto-ACK indefinitely (ADR-020 section 2: a
431 * redundant link should degrade to DOWN only from genuinely not
432 * responding, never from this module never getting around to
433 * polling it). Still swept to completion even after a hard
434 * fault on an earlier link, for the same reason. */
435 for (i = 0U; i < channel->link_count; i++)
436 {
437 dual_poll_result_t result;
438 sapi_status_t poll_status;
439
440 poll_status = dual_channel_poll_link_once(channel, i, per_link_timeout, &result);
441 /* REQ-DUAL-CHANNEL-008: see sapi_dual_channel_send()'s own
442 * doc on this same distinction - a link reporting
443 * something other than OK/TIMEOUT (e.g. HARDWARE_FAULT
444 * from a closed/reset connection) is broken, not just
445 * quiet, and that must not be silently collapsed into the
446 * same generic TIMEOUT "nothing staged yet" returns below. */
447 if ((poll_status == SAPI_STATUS_HARDWARE_FAULT) && (!saw_hard_fault))
448 {
449 saw_hard_fault = true;
450 hard_fault_status = poll_status;
451 }
452 }
453 }
454
455 if (!channel->pending_data_valid)
456 {
457 return saw_hard_fault ? hard_fault_status : SAPI_STATUS_TIMEOUT;
458 }
459 }
460 if (channel->pending_data_size > max_size)
461 {
462 /* Caller's buffer is too small for a staged frame - reported,
463 * not silently truncated (safety-relevant data). */
465 }
466
467 (void)memcpy(out_payload, channel->pending_data, channel->pending_data_size);
468 *out_size = channel->pending_data_size;
469 channel->pending_data_valid = false;
470
471 return SAPI_STATUS_OK;
472}
473
475{
477 uint32_t i;
478 uint32_t ack_count = 0U;
479 bool saw_hard_fault = false;
480 sapi_status_t hard_fault_status = SAPI_STATUS_OK;
481 sapi_timestamp_ms_t now_ms = 0U;
482
483 if (channel == NULL)
484 {
486 }
487
488 /* sapi_dual_heartbeat_frame_t has a 4-byte compiler-inserted
489 * alignment gap between `header` (4 bytes) and the 8-byte-aligned
490 * `timestamp_ms` that follows it - unlike every other frame type in
491 * this file, whose fields happen to add up to an already-aligned
492 * offset. Without this memset, that gap is indeterminate stack
493 * content that still gets read (and checksummed/transmitted) by the
494 * sapi_dual_msgchannel_send() call below, since it hashes/sends
495 * `sizeof(frame)` raw bytes, not just the named fields - found via
496 * Valgrind (SAFEAPI_ENABLE_ASAN/Valgrind CTest memcheck target)
497 * flagging a real "use of uninitialised value" inside
498 * sapi_checksum_crc64(), reached from here. */
499 (void)memset(&frame, 0, sizeof(frame));
500 (void)sapi_timer_now(&now_ms);
501 frame.header.kind = (uint8_t)SAPI_DUAL_FRAME_KIND_HEARTBEAT;
502 frame.header.reserved[0] = 0U;
503 frame.header.reserved[1] = 0U;
504 frame.header.reserved[2] = 0U;
505 frame.timestamp_ms = (uint64_t)now_ms;
506
507 for (i = 0U; i < channel->link_count; i++)
508 {
509 uint32_t sent_sequence = 0U;
510 sapi_status_t send_status;
511 bool link_now_up = false;
512
513 send_status = sapi_dual_msgchannel_send(&channel->links[i], (const uint8_t *)&frame,
514 (uint8_t)sizeof(frame), channel->ack_timeout_ms,
515 &sent_sequence);
516 if ((send_status == SAPI_STATUS_HARDWARE_FAULT) && (!saw_hard_fault))
517 {
518 saw_hard_fault = true;
519 hard_fault_status = send_status;
520 }
521 if (send_status == SAPI_STATUS_OK)
522 {
523 sapi_duration_ms_t remaining = channel->ack_timeout_ms;
524 sapi_timestamp_ms_t start_ms = 0U;
525 uint32_t stall_polls = 0U;
526
527 (void)sapi_timer_now(&start_ms);
528
529 while ((remaining > 0U) && (stall_polls < SAPI_DUAL_CHANNEL_STALL_POLL_LIMIT))
530 {
531 dual_poll_result_t result;
532 sapi_status_t poll_status;
533 sapi_timestamp_ms_t poll_now_ms = 0U;
534
535 poll_status = dual_channel_poll_link_once(channel, i, remaining, &result);
536 if ((poll_status == SAPI_STATUS_OK) && result.matched && (result.kind == SAPI_DUAL_FRAME_KIND_ACK)
537 && (result.ack_sequence == sent_sequence))
538 {
539 link_now_up = true;
540 break;
541 }
542 if (poll_status == SAPI_STATUS_HARDWARE_FAULT)
543 {
544 if (!saw_hard_fault)
545 {
546 saw_hard_fault = true;
547 hard_fault_status = poll_status;
548 }
549 break;
550 }
551
552 (void)sapi_timer_now(&poll_now_ms);
553 if (poll_now_ms > start_ms)
554 {
555 sapi_timestamp_ms_t elapsed = poll_now_ms - start_ms;
556 if (elapsed >= (sapi_timestamp_ms_t)channel->ack_timeout_ms)
557 {
558 remaining = 0U;
559 }
560 else
561 {
562 remaining = channel->ack_timeout_ms - (sapi_duration_ms_t)elapsed;
563 }
564 stall_polls = 0U;
565 }
566 else
567 {
568 stall_polls++;
569 }
570 }
571 }
572
573 channel->link_up[i] = link_now_up;
574 if (link_now_up)
575 {
576 ack_count++;
577 }
578 }
579
580 if (out_ack_link_count != NULL)
581 {
582 *out_ack_link_count = ack_count;
583 }
584
585 dual_channel_update_status(channel);
586
587 if (ack_count > 0U)
588 {
589 return SAPI_STATUS_OK;
590 }
591 return saw_hard_fault ? hard_fault_status : SAPI_STATUS_TIMEOUT;
592}
593
594sapi_status_t sapi_dual_channel_send_state_frame(sapi_dual_channel_t *channel, sapi_dual_state_t state,
595 bool channel_degraded, uint64_t timestamp_ms)
596{
598 uint32_t i;
599 uint32_t sent_count = 0U;
600
601 if (channel == NULL)
602 {
604 }
605
606 frame.header.kind = (uint8_t)SAPI_DUAL_FRAME_KIND_STATE;
607 frame.header.reserved[0] = 0U;
608 frame.header.reserved[1] = 0U;
609 frame.header.reserved[2] = 0U;
610 frame.state = (uint8_t)state;
611 frame.channel_degraded = channel_degraded ? 1U : 0U;
612 frame.reserved = 0U;
613 frame.timestamp_ms = timestamp_ms;
614
615 for (i = 0U; i < channel->link_count; i++)
616 {
617 sapi_status_t status = sapi_dual_msgchannel_send(&channel->links[i], (const uint8_t *)&frame,
618 (uint8_t)sizeof(frame), channel->ack_timeout_ms, NULL);
619 if (status == SAPI_STATUS_OK)
620 {
621 sent_count++;
622 }
623 }
624
625 return (sent_count > 0U) ? SAPI_STATUS_OK : SAPI_STATUS_TIMEOUT;
626}
627
629 sapi_dual_state_frame_t *out_frame)
630{
631 if ((channel == NULL) || (out_frame == NULL))
632 {
634 }
635
636 if (!channel->pending_state_valid)
637 {
638 uint32_t i;
639 sapi_duration_ms_t per_link_timeout = (channel->link_count > 0U) ? (timeout_ms / channel->link_count) : 0U;
640
641 /* Sweeps every link every call - see sapi_dual_channel_receive()'s
642 * own comment on why stopping early would starve other links. */
643 for (i = 0U; i < channel->link_count; i++)
644 {
645 dual_poll_result_t result;
646
647 (void)dual_channel_poll_link_once(channel, i, per_link_timeout, &result);
648 }
649 }
650
651 if (!channel->pending_state_valid)
652 {
653 return SAPI_STATUS_TIMEOUT;
654 }
655
656 *out_frame = channel->pending_state;
657 channel->pending_state_valid = false;
658
659 return SAPI_STATUS_OK;
660}
661
663{
664 if (channel == NULL)
665 {
667 }
668 return channel->last_status;
669}
670
671bool sapi_dual_channel_is_link_up(const sapi_dual_channel_t *channel, uint32_t link_index)
672{
673 if ((channel == NULL) || (link_index >= channel->link_count))
674 {
675 return false;
676 }
677 return channel->link_up[link_index];
678}
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 stat...
#define SAPI_DUAL_CHANNEL_MAX_LINKS
Maximum number of redundant links one sapi_dual_channel_t may be configured with. Fixed,...
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 s...
#define SAPI_DUAL_MSGCHANNEL_MAX_PAYLOAD
Max payload bytes usable via sapi_dual_msgchannel_send()/ _receive() - bounded by sapi_vital_message_...
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 w...
sapi_dual_state_t
State of one instance in a dual (two-redundant-instance) relationship, as decided by sapi_dual_negoti...
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_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_status_t
Aggregate connection status of a sapi_dual_channel_t across all of its configured redundant links (AD...
#define SAPI_DUAL_CHANNEL_MAX_PAYLOAD
Max application payload bytes usable via sapi_dual_channel_send()/_receive() - Layer-1's own SAPI_DUA...
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...
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 c...
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_...
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_dua...
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_d...
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-fo...
@ SAPI_DUAL_CHANNEL_STATUS_FULL
@ SAPI_DUAL_CHANNEL_STATUS_DOWN
@ SAPI_DUAL_CHANNEL_STATUS_DEGRADED
@ SAPI_DUAL_FRAME_KIND_STATE
@ SAPI_DUAL_FRAME_KIND_HEARTBEAT
@ SAPI_DUAL_FRAME_KIND_ACK
@ SAPI_DUAL_FRAME_KIND_DATA
sapi_status_t
Common result/status codes.
Definition sapi_status.h:27
@ SAPI_STATUS_HARDWARE_FAULT
Definition sapi_status.h:36
@ SAPI_STATUS_TIMEOUT
Definition sapi_status.h:32
@ SAPI_STATUS_INVALID_PARAM
Definition sapi_status.h:29
@ SAPI_STATUS_DATA_CORRUPTION
Definition sapi_status.h:37
@ SAPI_STATUS_OK
Definition sapi_status.h:28
sapi_status_t sapi_timer_now(sapi_timestamp_ms_t *out_now_ms)
Returns the current monotonic time base used by all timers.
Definition sapi_timer.c:130
uint32_t sapi_duration_ms_t
Definition sapi_types.h:27
uint64_t sapi_timestamp_ms_t
Definition sapi_types.h:30
#define SAPI_DUAL_CHANNEL_STALL_POLL_LIMIT
Cap on consecutive sapi_dual_channel_send() ACK-wait polls that may complete without sapi_timer_now()...
static sapi_status_t dual_channel_poll_link_once(sapi_dual_channel_t *channel, uint32_t link_index, sapi_duration_ms_t timeout_ms, dual_poll_result_t *out_result)
One receive attempt on one link, dispatched by frame kind: DATA is auto-ACKed and staged for sapi_dua...
"DualChannel" layer of ADR-020: wraps 1..N redundant sapi_dual_msgchannel_t links for fault-tolerant,...
OS Abstraction Layer - Timer service.
Result of one dual_channel_poll_link_once() attempt.
sapi_dual_frame_kind_t kind
SAPI_DUAL_FRAME_KIND_ACK payload.
Configuration for sapi_dual_channel_init().
sapi_duration_ms_t ack_timeout_ms
sapi_dual_channel_status_callback_t status_callback
sapi_netlink_handle_t links[SAPI_DUAL_CHANNEL_MAX_LINKS]
One sapi_dual_channel_t instance's state. Caller-owned storage; every field is private - reach it onl...
Common 4-byte header prefixing every Layer-2 frame; kept a fixed 4 bytes (not just 1) so the fields t...
SAPI_DUAL_FRAME_KIND_HEARTBEAT payload - connection maintenance heartbeat.
Configuration for sapi_dual_msgchannel_init().
SAPI_DUAL_FRAME_KIND_STATE payload - sapi_dual_negotiator_t's own periodic beacon.