RTE RBC 2oo2 Reference Application 0.1.0 build 0006685
SIL 4 2oo2 Dual-Site Fault-Tolerant ERTMS Radio Block Centre Reference Architecture
Loading...
Searching...
No Matches
ab_gp_channel.c
Go to the documentation of this file.
1
8
9#include "ab_gp_channel.h"
10
11#include <stdio.h>
12#include <stdlib.h>
13#include <string.h>
14
15#include "rte/redundancy/checkpoint/rte_checkpoint.h"
16#include "rte/redundancy/channel_service/rte_channel_service.h"
17#include "rte/redundancy/channel_service/rte_channel_service_flow_osadapter.h"
18#include "rte/posix_osadapter/rte_posix_osadapter_channel_service.h"
19#include "rte/protocol_adapter_dds/rte_protocol_adapter_dds.h"
20#include "rte_osadapter/flow/rte_osadapter_flow.h"
21#include "rte/oal/log/rte_log.h"
22#include "rte/oal/memory/rte_mem_util.h"
23#include "rte/utils/safestate/rte_safestate.h"
24
29#include "ab_gp_channel_dual.h"
32#include "ab_gp_channel_types.h"
33#include "ab_gp_channel_util.h"
35#include "ab_gp_km114_relay.h"
37#include "../main/ab_gp_ga_hooks.h" /* ab_gp_build_ctc_alarm() - see that file's own doc */
40
42
43/* Shared by both config-driven resolution paths below (previously two verbatim-duplicated
44 * copies of this exact if/else-if chain, one per path - a real complexity and drift source in
45 * its own right). Only touches config->host; every other field is the caller's responsibility.
46 * Precedence, unchanged from before this extraction: an explicit env var override, else the
47 * config-supplied host, else a well-known "same host as this build's own default docker/host
48 * name" fallback to loopback. */
49static void apply_host_env_override(const char *name, const ab_gp_channel_context_t *ctx, rte_netlink_config_t *config)
50{
51 const char *env_host;
52
53 if ((strcmp(name, "ab-peer") == 0) || (strcmp(name, "ab-crosscompare") == 0))
54 {
55 env_host = getenv(RTE_EXAMPLE_A_HOST_ENV);
56 if ((env_host != NULL) && (env_host[0] != '\0'))
57 {
58 config->host = env_host;
59 }
60 else if ((config->host == NULL) || (config->host[0] == '\0') ||
61 (strcmp(config->host, "a-west") == 0) || (strcmp(config->host, "a-east") == 0))
62 {
63 config->host = "127.0.0.1";
64 }
65 }
66 else if (strcmp(name, "ab-negotiate") == 0)
67 {
68 const char *env_var = ctx->is_role_a ? RTE_EXAMPLE_REMOTE_A_HOST_ENV
70 env_host = getenv(env_var);
71 if ((env_host != NULL) && (env_host[0] != '\0'))
72 {
73 config->host = env_host;
74 }
75 else if ((config->host == NULL) || (config->host[0] == '\0') ||
76 (strcmp(config->host, "a-west") == 0) || (strcmp(config->host, "b-west") == 0))
77 {
78 config->host = "127.0.0.1";
79 }
80 }
81 else
82 {
83 env_host = getenv(RTE_EXAMPLE_C_HOST_ENV);
84 if ((env_host != NULL) && (env_host[0] != '\0'))
85 {
86 config->host = env_host;
87 }
88 else if ((config->host == NULL) || (config->host[0] == '\0') ||
89 (strcmp(config->host, "c-west") == 0) || (strcmp(config->host, "c-east") == 0))
90 {
91 config->host = "127.0.0.1";
92 }
93 }
94}
95
96/* Highest-priority resolution path: the runtime redundancy config (JSON, ctx->redundancy).
97 * Returns false (config left untouched) when name isn't in that config, or when ctx itself is
98 * NULL - unchanged from before this extraction, this is the ONLY one of the three paths that
99 * tolerates a NULL ctx; the other two dereference it unconditionally, exactly as the original,
100 * un-extracted function did (not "fixed" as a side effect of this refactor). */
101static bool try_resolve_from_redundancy_config(const ab_gp_channel_context_t *ctx, const char *name,
102 rte_netlink_config_t *config)
103{
104 const rte_channel_def_t *found_ch = NULL;
105
106 if (ctx != NULL)
107 {
108 uint8_t ch_i;
109 for (ch_i = 0U; ch_i < ctx->redundancy.channel_count; ch_i++)
110 {
111 if (strcmp(ctx->redundancy.channels[ch_i].name, name) == 0)
112 {
113 found_ch = &ctx->redundancy.channels[ch_i];
114 break;
115 }
116 }
117 }
118 if (found_ch == NULL)
119 {
120 return false;
121 }
122
123 rte_mem_set(config, 0, sizeof(*config));
124 config->role = (found_ch->role == RTE_CHANNEL_ROLE_CONNECT) ? RTE_NETLINK_ROLE_CONNECT : RTE_NETLINK_ROLE_LISTEN;
125 config->host = (config->role == RTE_NETLINK_ROLE_CONNECT && found_ch->host[0] != '\0') ? found_ch->host : NULL;
126 if (config->role == RTE_NETLINK_ROLE_CONNECT)
127 {
128 apply_host_env_override(name, ctx, config);
129 }
130 config->port = found_ch->port;
131 config->message_size = (found_ch->message_size > 0U) ? found_ch->message_size : RBC_ENVELOPE_WIRE_SIZE;
132 if ((strcmp(name, "ab-peer") == 0) || (strcmp(name, "ab-crosscompare") == 0))
133 {
134 /* The A/B frame size is a compile-time wire fact of this build, not a deployment setting. */
135 config->message_size = PEER_WIRE_FRAME_SIZE;
136 }
137 config->connect_timeout_ms = (found_ch->connect_timeout_ms > 0U) ? found_ch->connect_timeout_ms
139 return true;
140}
141
142/* Second-priority resolution path: the legacy etc/config/<role>_<site>.conf file. Dereferences
143 * ctx->config unconditionally - unchanged from before this extraction (see the NULL-handling
144 * note on try_resolve_from_redundancy_config above). */
145static bool try_resolve_from_legacy_config(const ab_gp_channel_context_t *ctx, const char *name,
146 rte_netlink_config_t *config)
147{
148 if (ab_gp_config_find_channel(&ctx->config, name, config) != RTE_STATUS_OK)
149 {
150 return false;
151 }
152 if (config->role == RTE_NETLINK_ROLE_CONNECT)
153 {
154 apply_host_env_override(name, ctx, config);
155 }
156 if ((strcmp(name, "ab-peer") == 0) || (strcmp(name, "ab-crosscompare") == 0))
157 {
158 config->message_size = PEER_WIRE_FRAME_SIZE;
159 }
160 else if (config->message_size == 0U)
161 {
162 config->message_size = RBC_ENVELOPE_WIRE_SIZE;
163 }
164 return true;
165}
166
167/* ADR-036: one CONNECT link per {kind} to C - train_id demux is done against the envelope, not
168 * the link. Matches resolve_channel_hardcoded_fallback()'s own "ab-relay-*" name set. */
169static bool relay_kind_from_name(const char *name, rte_example_relay_kind_t *out_kind)
170{
171 if (strcmp(name, "ab-relay-train") == 0)
172 {
174 return true;
175 }
176 if (strcmp(name, "ab-relay-il") == 0)
177 {
178 *out_kind = RTE_EXAMPLE_RELAY_KIND_IL;
179 return true;
180 }
181 if (strcmp(name, "ab-relay-ctc") == 0)
182 {
183 *out_kind = RTE_EXAMPLE_RELAY_KIND_CTC;
184 return true;
185 }
186 if (strcmp(name, "ab-relay-util") == 0)
187 {
188 *out_kind = RTE_EXAMPLE_RELAY_KIND_UTIL;
189 return true;
190 }
191 return false;
192}
193
194/* ADR-038: the UTIL link carries rbc_util_frame_t (256 B), not an rbc_envelope_t. Train is
195 * widened to RBC_TRAIN_ENVELOPE_WIRE_SIZE (common_config.h) to carry RBC_MSG_ERTMS_ENVELOPE
196 * frames alongside its existing flat-scheme kinds - see that constant's own doc. */
198{
200 {
201 return (uint32_t)RBC_UTIL_FRAME_SIZE;
202 }
204 {
205 return (uint32_t)RBC_TRAIN_ENVELOPE_WIRE_SIZE;
206 }
207 return (uint32_t)RBC_ENVELOPE_WIRE_SIZE;
208}
209
210/* Last-resort resolution path: common_config.h's own hardcoded port/host formulas
211 * (site_config_*), used when neither config-driven path above found this channel name.
212 * Dereferences ctx unconditionally - see the NULL-handling note on
213 * try_resolve_from_redundancy_config above. */
214static rte_status_t resolve_channel_hardcoded_fallback(const ab_gp_channel_context_t *ctx, const char *name,
215 rte_netlink_config_t *config)
216{
218
219 rte_mem_set(config, 0, sizeof(*config));
220 config->connect_timeout_ms = ctx->config.timers.reconnect_attempt_timeout_ms;
221
222 if (strcmp(name, "ab-peer") == 0)
223 {
224 config->role = ctx->is_role_a ? RTE_NETLINK_ROLE_LISTEN : RTE_NETLINK_ROLE_CONNECT;
225 config->host = ctx->is_role_a ? NULL : site_config_host(RTE_EXAMPLE_A_HOST_ENV);
226 config->port = site_config_ab_port(ctx->site);
227 config->message_size = PEER_WIRE_FRAME_SIZE;
228 return RTE_STATUS_OK;
229 }
230 if (strcmp(name, "ab-crosscompare") == 0)
231 {
232 config->role = ctx->is_role_a ? RTE_NETLINK_ROLE_LISTEN : RTE_NETLINK_ROLE_CONNECT;
233 config->host = ctx->is_role_a ? NULL : site_config_host(RTE_EXAMPLE_A_HOST_ENV);
234 config->port = site_config_ab_crosscompare_port(ctx->site);
235 config->message_size = PEER_WIRE_FRAME_SIZE;
236 return RTE_STATUS_OK;
237 }
238 if (relay_kind_from_name(name, &rk))
239 {
240 config->role = RTE_NETLINK_ROLE_CONNECT;
242 config->port = ctx->is_role_a ? site_config_c_port_for_a(ctx->site, rk)
243 : site_config_c_port_for_b(ctx->site, rk);
244 config->message_size = message_size_for_relay_kind(rk);
245 return RTE_STATUS_OK;
246 }
247 return RTE_STATUS_INVALID_PARAM;
248}
249
250/* TODO.md "Single source for GP ports and message sizes" (was ISSUES.md "etc/config/
251 * {a,b}_{west,east}.conf hardcode ports independently of common_config.h", 2026-09-18):
252 * ab-peer/ab-crosscompare used to be the two channel names with BOTH a config-file literal
253 * port (JSON redundancy config, then the legacy .conf, in that priority order above) AND a
254 * compiled common_config.h formula (resolve_channel_hardcoded_fallback()) able to disagree
255 * silently - unlike every other channel name, which is deliberately resolved from
256 * common_config.h every run for exactly this reason (see that function's own doc). A prior
257 * pass (2026-09-23) added detection (a loud CONFIG_DRIFT warning) without changing which port
258 * won, confirming the drift is real against the shipped default config
259 * (etc/config/a_west.json's own ab-crosscompare literal, 15016, vs the formula, 15017).
260 *
261 * This now goes the rest of the way: the compiled formula is the single source of truth for
262 * these two channels' PORT, same as every other channel already gets - a config file's own
263 * port for ab-peer/ab-crosscompare is overridden, not merely flagged, closing the actual
264 * footgun (changing common_config.h and rebuilding now always takes effect). The config file's
265 * HOST for these two channels is untouched - that genuinely is deployment-specific (a real
266 * site's A/B may be on different machines) and already has its own env-var override
267 * (apply_host_env_override()); only the port number was ever the two-independent-sources-of-
268 * truth problem. Still logs when an override actually happened, so a config file's leftover
269 * literal port is visible, not silently ignored either. */
270static void enforce_hardcoded_port(const ab_gp_channel_context_t *ctx, const char *name,
271 rte_netlink_config_t *config)
272{
273 uint16_t formula_port;
274
275 if (strcmp(name, "ab-peer") == 0)
276 {
277 formula_port = site_config_ab_port(ctx->site);
278 }
279 else if (strcmp(name, "ab-crosscompare") == 0)
280 {
281 formula_port = site_config_ab_crosscompare_port(ctx->site);
282 }
283 else
284 {
285 return;
286 }
287
288 if (formula_port != config->port)
289 {
290 char info[96];
291
292 (void)snprintf(info, sizeof(info), "channel=%s config_port=%u formula_port=%u", name,
293 (unsigned int)config->port, (unsigned int)formula_port);
294 rte_log_write_event(RTE_LOG_LEVEL_WARNING, site_config_name(ctx->site), 0U, ctx->role_tag, "-",
295 "CONFIG_DRIFT", "config-file port ignored - common_config.h formula wins", info);
296 config->port = formula_port;
297 }
298}
299
300static rte_status_t resolve_channel(const char *name, rte_netlink_config_t *config, void *context)
301{
302 const ab_gp_channel_context_t *ctx = (const ab_gp_channel_context_t *)context;
303
304 if (try_resolve_from_redundancy_config(ctx, name, config))
305 {
306 if (ctx != NULL)
307 {
308 enforce_hardcoded_port(ctx, name, config);
309 }
310 return RTE_STATUS_OK;
311 }
312 if (try_resolve_from_legacy_config(ctx, name, config))
313 {
314 if (ctx != NULL)
315 {
316 enforce_hardcoded_port(ctx, name, config);
317 }
318 return RTE_STATUS_OK;
319 }
320 return resolve_channel_hardcoded_fallback(ctx, name, config);
321}
322
339static rte_status_t resolve_channel_flow(const char *name, rte_netlink_config_t *config, void *context)
340{
341 rte_status_t status = resolve_channel(name, config, context);
342 if (status == RTE_STATUS_OK)
343 {
344 (void)rte_protocol_adapter_dds_add_route(name, config->host, config->port);
345 }
346 return status;
347}
348
357static bool ab_gp_supports_redundancy(rte_redundancy_topology_t topology, uint32_t replica_count, void *context)
358{
360 bool supported = (replica_count == 2U) &&
361 ((topology == RTE_REDUNDANCY_TOPOLOGY_2OO2) ||
362 (topology == RTE_REDUNDANCY_TOPOLOGY_2OO2_REDUNDANT));
363
364 if (!supported)
365 {
367 "[%s] redundancy config names topology=%s replicas=%u - not yet implemented "
368 "(only 2oo2/2oo2_redundant with exactly 2 replicas run today)\n",
369 ctx->role_tag, rte_redundancy_config_topology_to_string(topology), (unsigned int)replica_count);
370 }
371 return supported;
372}
373
396static rte_status_t channel_service_setup_with_retry(rte_channel_service_t *storage, const char *name)
397{
398 rte_timestamp_ms_t waited_ms = 0U;
399 const unsigned int retry_delay_ms = 100U;
400 rte_status_t status;
401
402 RTE_EXAMPLE_LOG("[%s] GP: opening channel '%s' to RTE\n", g_ab_ctx.role_tag, name);
403 for (;;)
404 {
405 status = rte_channel_service_setup(storage, name);
406 if (status == RTE_STATUS_OK)
407 {
408 RTE_EXAMPLE_LOG("[%s] GP: channel '%s' to RTE opened successfully\n", g_ab_ctx.role_tag, name);
409 return RTE_STATUS_OK;
410 }
411 if (waited_ms >= RTE_EXAMPLE_CONNECT_TIMEOUT_MS)
412 {
413 return status;
414 }
415 ab_gp_channel_sleep_ms(retry_delay_ms);
416 waited_ms += retry_delay_ms;
417 }
418}
419
420/* Runs on the OSAdapter's timer thread (task context - see ab_gp_cycle_ticks.h for that assumption). The tick is
421 * counted under the tick count's mutex (ISS-051). A failed post (the lock not acquired within
422 * AB_GP_CYCLE_TICKS_LOCK_TIMEOUT_MS, or an OSAdapter fault) drops this tick and is deliberately not logged: a log from
423 * the timer thread would enter the JRU forwarder / UTIL send path. A single dropped tick is covered by the
424 * cross-compare cycle skew tolerance; a persistent failure leaves the count at 0 and is caught by the no-tick watchdog
425 * timeout of ab_gp_channel_wait_cycle_tick(). */
426static void cycle_tick_callback(rte_timer_handle_t handle, void *user_ctx)
427{
429 (void)handle;
431}
432
433static void ab_gp_channel_safestate_handler(rte_safestate_level_t level, rte_safestate_reason_t reason,
434 const char *file, int32_t line, const char *message)
435{
436 (void)level;
437 RTE_EXAMPLE_LOG("[%s] SAFE-state handler: reason=%u at %s:%d%s%s\n", g_ab_ctx.role_tag,
438 (unsigned int)reason, (file != NULL) ? file : "?", (int)line,
439 (message != NULL) ? " - " : "", (message != NULL) ? message : "");
440 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(g_ab_ctx.site), g_ab_ctx.cyc_cycle, g_ab_ctx.role_tag,
441 "-", "SAFE_HALT", (message != NULL) ? message : "entering SAFE state", NULL);
442 /* Before any shutdown() below (CTC relay socket still open either
443 * way here - see ab_gp_channel_raise_ctc_alarm()'s own doc on why
444 * ordering matters): a dispatcher watching CTC needs to see every
445 * safestate-driven fault this channel reacts to, not just this
446 * process's own local log. */
448 (message != NULL) ? message : "entering SAFE state");
449
450 if (reason == RTE_SAFESTATE_REASON_CHECKPOINT_TIMEOUT)
451 {
453 "[%s] checkpoint timeout treated as peer-link-down (REQ-RBC, ADR-029 2.6) - rebooting, not halting\n",
455 g_ab_ctx.own_faulted = true;
457 RTE_SAFESTATE(RTE_SAFESTATE_LEVEL_REBOOT, RTE_EXAMPLE_REASON_AB_PEER_LINK_DOWN);
458 }
459}
460
465static void load_legacy_config(ab_gp_channel_context_t *ctx, rte_example_site_t site, bool is_role_a)
466{
467 const char *env_cfg = getenv("RTE_CONFIG_FILE");
468 const char *site_str = (site == RTE_EXAMPLE_SITE_WEST) ? "west" : "east";
469 const char *role_str = is_role_a ? "a" : "b";
470
471 if (env_cfg != NULL)
472 {
473 (void)ab_gp_config_load_file(env_cfg, &ctx->config);
474 }
475 else
476 {
477 char default_cfg_path[128];
478 (void)snprintf(default_cfg_path, sizeof(default_cfg_path), "etc/config/%s_%s.conf", role_str, site_str);
479 if (ab_gp_config_load_file(default_cfg_path, &ctx->config) != RTE_STATUS_OK)
480 {
481 (void)snprintf(default_cfg_path, sizeof(default_cfg_path), "/etc/sapi/%s_%s.conf", role_str, site_str);
482 (void)ab_gp_config_load_file(default_cfg_path, &ctx->config);
483 }
484 }
485}
486
501static rte_status_t load_redundancy_config(ab_gp_channel_context_t *ctx, rte_example_site_t site, bool is_role_a)
502{
503 const char *env_redundancy_cfg = getenv("RTE_REDUNDANCY_CONFIG_FILE");
504 const char *site_str = (site == RTE_EXAMPLE_SITE_WEST) ? "west" : "east";
505 const char *role_str = is_role_a ? "a" : "b";
506 char json_cfg_path[128];
507 const char *redundancy_cfg_path = env_redundancy_cfg;
508 rte_status_t redundancy_status;
509
510 if (redundancy_cfg_path == NULL)
511 {
512 (void)snprintf(json_cfg_path, sizeof(json_cfg_path), "etc/config/%s_%s.json", role_str, site_str);
513 redundancy_cfg_path = json_cfg_path;
514 }
515
516 redundancy_status = rte_redundancy_config_load(redundancy_cfg_path, &ctx->redundancy);
517 if ((redundancy_status != RTE_STATUS_OK) && (env_redundancy_cfg == NULL))
518 {
519 redundancy_status = rte_redundancy_config_load("etc/config/redundancy.json", &ctx->redundancy);
520 }
521
522 if (redundancy_status == RTE_STATUS_NOT_SUPPORTED)
523 {
524 /* ab_gp_supports_redundancy() already logged which
525 * topology/replica_count it rejected and why. */
526 return RTE_STATUS_NOT_SUPPORTED;
527 }
528 if (redundancy_status != RTE_STATUS_OK)
529 {
530 rte_mem_set(&ctx->redundancy, 0, sizeof(ctx->redundancy));
531 ctx->redundancy.topology = RTE_REDUNDANCY_TOPOLOGY_2OO2;
532 ctx->redundancy.replica_count = 2U;
533 ctx->redundancy.quorum_size = 2U;
534 ctx->redundancy.standby_mode = RTE_STANDBY_MODE_COLD;
535 }
536
537 RTE_EXAMPLE_LOG("[%s] redundancy topology=%s replicas=%u quorum=%u standby_mode=%s\n", ctx->role_tag,
538 rte_redundancy_config_topology_to_string(ctx->redundancy.topology),
539 (unsigned int)ctx->redundancy.replica_count, (unsigned int)ctx->redundancy.quorum_size,
540 rte_redundancy_config_standby_mode_to_string(ctx->redundancy.standby_mode));
541 return RTE_STATUS_OK;
542}
543
548{
549 uint8_t kind_idx;
550 rte_status_t status;
551
552 for (kind_idx = 0U; kind_idx < (uint8_t)RTE_EXAMPLE_RELAY_KIND_COUNT; kind_idx++)
553 {
555 const char *name;
556
558 {
559 name = "ab-relay-train";
560 }
561 else if (kind == RTE_EXAMPLE_RELAY_KIND_IL)
562 {
563 name = "ab-relay-il";
564 }
565 else if (kind == RTE_EXAMPLE_RELAY_KIND_UTIL)
566 {
567 name = "ab-relay-util";
568 }
569 else
570 {
571 name = "ab-relay-ctc";
572 }
573 status = channel_service_setup_with_retry(&ctx->com.relay_channel[kind_idx], name);
574 if (status != RTE_STATUS_OK)
575 {
576 RTE_EXAMPLE_LOG("[%s] relay channel setup failed for %s: %s\n", ctx->role_tag, name, rte_status_to_string(status));
577 return status;
578 }
579 }
580 return RTE_STATUS_OK;
581}
582
583rte_status_t ab_gp_channel_open_link(bool is_role_a, rte_example_site_t site, ab_gp_channel_handle_t *out_handle)
584{
586 rte_status_t status;
587
588 if (out_handle == NULL)
589 {
590 return RTE_STATUS_INVALID_PARAM;
591 }
592 *out_handle = NULL;
593
594 rte_mem_set(ctx, 0, sizeof(*ctx));
595 ctx->is_role_a = is_role_a;
596 ctx->site = site;
597 (void)snprintf(ctx->role_tag_buf, sizeof(ctx->role_tag_buf), "%s/%s", is_role_a ? "A" : "B",
598 site_config_name(site));
599 ctx->role_tag = ctx->role_tag_buf;
600 ctx->stop_requested = false;
601 /* ctx->startup_timestamp_ms stays 0 here: A and B agree it over the peer link in
602 * ab_gp_channel_negotiate_init() (ADR-040 step 5a), once the link below is open. */
603
604 (void)ab_gp_config_init_defaults(site, is_role_a, &ctx->config);
605 load_legacy_config(ctx, site, is_role_a);
606
607 (void)rte_safestate_register_handler(RTE_SAFESTATE_LEVEL_SAFE, ab_gp_channel_safestate_handler);
608
609 /* RCA/OCORA Phase 8 (see docs/rca/ and TODO.md): see load_redundancy_config()'s own doc. */
610 (void)rte_redundancy_config_register_capability(ab_gp_supports_redundancy, ctx);
611 if (load_redundancy_config(ctx, site, is_role_a) != RTE_STATUS_OK)
612 {
613 return RTE_STATUS_NOT_SUPPORTED;
614 }
615
616 /* RCA/OCORA Phase 4: full replacement of GP's A/B and site-negotiation
617 * transport with rte_flow (routed through today's DDS-stub OSAdapter,
618 * safeAPIFlowBackendDDS - see docs/rca/ and TODO.md). Was
619 * rte_posix_osadapter_channel_service_register_resolver(resolve_channel, ctx)
620 * (still used unchanged by role C, see gateway_c_common.c - out of
621 * scope for this phase, see TODO.md's own scoping note). */
622 status = rte_osadapter_flow_register(rte_protocol_adapter_dds_get_vtable());
623 if (status != RTE_STATUS_OK)
624 {
625 RTE_EXAMPLE_LOG("[%s] flow osadapter registration failed: %s\n", ctx->role_tag,
626 rte_status_to_string(status));
627 return status;
628 }
629 status = rte_channel_service_flow_osadapter_register_resolver(resolve_channel_flow, ctx);
630 if (status != RTE_STATUS_OK)
631 {
632 RTE_EXAMPLE_LOG("[%s] resolver registration failed: %s\n", ctx->role_tag, rte_status_to_string(status));
633 return status;
634 }
635
636 status = channel_service_setup_with_retry(&ctx->com.peer_channel, "ab-peer");
637 if (status != RTE_STATUS_OK)
638 {
639 RTE_EXAMPLE_LOG("[%s] peer channel setup failed: %s\n", ctx->role_tag, rte_status_to_string(status));
640 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag,
641 ctx->is_role_a ? "B" : "A", "LINK", "peer channel setup failed", NULL);
642 return status;
643 }
644
645 status = channel_service_setup_with_retry(&ctx->com.crosscompare_channel, "ab-crosscompare");
646 if (status != RTE_STATUS_OK)
647 {
648 RTE_EXAMPLE_LOG("[%s] crosscompare channel setup failed: %s\n", ctx->role_tag, rte_status_to_string(status));
649 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag,
650 ctx->is_role_a ? "B" : "A", "LINK", "crosscompare channel setup failed", NULL);
651 return status;
652 }
653
654 status = open_relay_channels(ctx);
655 if (status != RTE_STATUS_OK)
656 {
657 return status;
658 }
659
660 *out_handle = (ab_gp_channel_handle_t)ctx;
661 return RTE_STATUS_OK;
662}
663
665{
666 if (handle != NULL)
667 {
668 (void)ab_gp_channel_shutdown((void *)handle);
669 }
670}
671
673{
675 rte_status_t status;
676
677 if (ctx == NULL)
678 {
679 return RTE_STATUS_INVALID_PARAM;
680 }
681
682 {
683 rte_channel_config_t checkpoint_vc_cfg;
684 rte_voter_config_t checkpoint_voter_cfg;
685
686 ctx->checkpoint_channel_handle = ctx;
687 /* Named for lookup (rte_voter_get_channel_by_name()) and logging -
688 * "ChannelAtoB" from A's own perspective, "ChannelBtoA" from B's. */
689 (void)snprintf(ctx->checkpoint_channel_name_buf, sizeof(ctx->checkpoint_channel_name_buf),
690 "Channel%sto%s", ctx->is_role_a ? "A" : "B", ctx->is_role_a ? "B" : "A");
691 rte_mem_set(&checkpoint_vc_cfg, 0, sizeof(checkpoint_vc_cfg));
692 checkpoint_vc_cfg.channel_handle = ctx->checkpoint_channel_handle;
693 checkpoint_vc_cfg.send = ab_gp_channel_checkpoint_backend_send;
694 checkpoint_vc_cfg.recv = ab_gp_channel_checkpoint_backend_recv;
695 checkpoint_vc_cfg.name = ctx->checkpoint_channel_name_buf;
696
697 rte_mem_set(&checkpoint_voter_cfg, 0, sizeof(checkpoint_voter_cfg));
698 checkpoint_voter_cfg.voting_strategy = RTE_VOTING_NMR;
699 checkpoint_voter_cfg.quorum_size = 1U;
700 checkpoint_voter_cfg.channel_timeout_ms = ctx->config.timers.checkpoint_max_delay_ms;
701
702 status = rte_voter_init(&ctx->checkpoint_voter, &checkpoint_voter_cfg);
703 if (status == RTE_STATUS_OK)
704 {
705 status = rte_channel_init(&ctx->checkpoint_vc, &checkpoint_vc_cfg);
706 }
707 if (status == RTE_STATUS_OK)
708 {
709 status = rte_voter_register_channel(&ctx->checkpoint_voter, &ctx->checkpoint_vc);
710 }
711 if (status != RTE_STATUS_OK)
712 {
713 RTE_EXAMPLE_LOG("[%s] checkpoint channel/voter init failed (%s) - built-in checkpoint disabled this run\n",
714 ctx->role_tag, rte_status_to_string(status));
715 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag,
716 ctx->is_role_a ? "B" : "A", "CHECKPOINT",
717 "channel/voter init failed - built-in checkpoint disabled this run", NULL);
718 }
719 else
720 {
721 rte_timestamp_ms_t init_now_ms = 0U;
722
723 ctx->checkpoint_cfg.voter = &ctx->checkpoint_voter;
724 ctx->checkpoint_cfg.max_delay_ms = (rte_duration_ms_t)ctx->config.timers.checkpoint_relaxed_max_delay_ms;
725 ctx->checkpoint_cfg.expected_node_count = 1U;
726 ctx->checkpoint_cfg.watchdog = NULL; /* wired to ctx->watchdog in
727 * ab_gp_channel_watchdog_setup()
728 * once that watchdog exists -
729 * see its own comment */
730 (void)rte_timer_now(&init_now_ms);
731 ctx->checkpoint_relaxed_until_ms = init_now_ms + (rte_timestamp_ms_t)ctx->config.timers.checkpoint_relax_duration_ms;
732 }
733 }
734 return status;
735}
736
738{
740
741 if (ctx == NULL)
742 {
743 return RTE_STATUS_INVALID_PARAM;
744 }
745
746 ctx->single_mode = false;
747 ctx->was_online = false;
748 ctx->last_peer_sample_cycle = 0U;
749 return RTE_STATUS_OK;
750}
751
752const rte_appmanager_checkpoint_config_t *ab_gp_channel_checkpoint_config(ab_gp_channel_handle_t handle)
753{
755
756 return (ctx != NULL) ? &ctx->checkpoint_cfg : NULL;
757}
758
760{
762
763 if (ctx == NULL)
764 {
765 return RTE_STATUS_INVALID_PARAM;
766 }
768}
769
770rte_status_t ab_gp_channel_init(void *context)
771{
773 uint32_t discarded_ticks = 0U;
774 rte_status_t status;
775
776 /* Ticks posted between the timer start and here do not count: the first cycle starts from an empty count. */
777 status = ab_gp_cycle_ticks_take(&ctx->cycle_ticks, &discarded_ticks);
778 if (status != RTE_STATUS_OK)
779 {
780 return status;
781 }
782 ctx->cycle = 1U;
783 RTE_EXAMPLE_LOG("[%s] cyclic executive armed (period=%ums, starting cycle=%u)\n", ctx->role_tag,
784 (unsigned int)ctx->config.timers.cycle_period_ms, ctx->cycle);
785 return RTE_STATUS_OK;
786}
787
789{
790 const ab_gp_channel_context_t *ctx = (const ab_gp_channel_context_t *)handle;
791 rte_status_t status;
792
793 status = ab_gp_train_init();
794 if (status != RTE_STATUS_OK)
795 {
796 RTE_EXAMPLE_LOG("[%s] session codec registration (train) -> %s\n", ctx->role_tag,
797 rte_status_to_string(status));
798 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "GP",
799 "train session codec registration failed", NULL);
800 }
801 return status;
802}
803
805{
807
809 /* Bare call now - GA calls back into GP's own functions (declared in
810 * ga_interface.h) by ordinary symbol resolution against the real
811 * librte_gp, not through a handed-off function-pointer struct.
812 * See ga_interface.h's own header doc for the dummy-then-real
813 * shared-library mechanism this replaced. */
814 return ab_ga_ma_adaptation_init(ctx->sessions, &ctx->db, ctx->site, ctx->role_tag);
815}
816
817
819{
820 (void)handle;
821}
822
824{
826 rte_status_t status;
827
828 status = ab_gp_channel_negotiate_init(ctx);
829 if (status != RTE_STATUS_OK)
830 {
831 RTE_EXAMPLE_LOG("[%s] inter-site negotiation init failed (%s)\n", ctx->role_tag, rte_status_to_string(status));
832 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-",
833 "NEGOTIATION", "init failed", NULL);
834 }
835 return status;
836}
837
839{
841 rte_watchdog_config_t wd_cfg;
842 rte_status_t status;
843
844 rte_mem_set(&wd_cfg, 0, sizeof(wd_cfg));
845 wd_cfg.type = RTE_WATCHDOG_CHANNEL;
846 wd_cfg.name = ctx->role_tag;
847 wd_cfg.timeout_ms = (rte_duration_ms_t)ctx->config.timers.watchdog_timeout_ms;
848 wd_cfg.action = RTE_WATCHDOG_ACTION_REBOOT;
849 status = rte_watchdog_create(&ctx->watchdog, &wd_cfg);
850 if (status == RTE_STATUS_OK)
851 {
852 (void)rte_watchdog_start(ctx->watchdog);
853
854 /* Wire this same watchdog into the checkpoint config too (set up
855 * earlier, in ab_gp_channel_checkpoint_setup() - this watchdog
856 * did not exist yet at that point, hence the ordering here
857 * instead of there). rte_channel_checkpoint() now kicks
858 * config->watchdog once per internal retry round, not only on
859 * success (see rte_checkpoint.h's own doc) - without this,
860 * ctx->watchdog (kicked only once per full cycle, in
861 * post_execute.c, AFTER a checkpoint call returns) fires and
862 * reboots this process out from under a checkpoint call that is
863 * still legitimately retrying within the relaxed startup budget
864 * (RTE_EXAMPLE_AB_CHECKPOINT_RELAXED_MAX_DELAY_MS, far longer
865 * than RTE_EXAMPLE_AB_WATCHDOG_TIMEOUT_MS) - confirmed live
866 * via Docker: B/WEST logged "watchdog timeout - requesting
867 * reboot" mid-retry, well before its own checkpoint call could
868 * either succeed or genuinely exhaust its budget. */
869 ctx->checkpoint_cfg.watchdog = ctx->watchdog;
870 }
871 else
872 {
873 RTE_EXAMPLE_LOG("[%s] watchdog_create failed (%s) - continuing without it\n", ctx->role_tag,
874 rte_status_to_string(status));
875 ctx->watchdog = NULL;
876 }
877 return status;
878}
879
881{
883 rte_timer_config_t timer_cfg;
884 rte_status_t status;
885
886 /* The tick count's mutex exists before the timer that posts to it (a mutex is a setup-phase resource). The status
887 * text is printed by the caller's fatal path (main.c step 12). */
888 status = ab_gp_cycle_ticks_setup(&ctx->cycle_ticks);
889 if (status != RTE_STATUS_OK)
890 {
891 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "TIMER",
892 "cycle tick mutex_create failed", NULL);
893 return status;
894 }
895
896 rte_mem_set(&timer_cfg, 0, sizeof(timer_cfg));
897 timer_cfg.mode = RTE_TIMER_MODE_PERIODIC;
898 timer_cfg.period_ms = (rte_duration_ms_t)ctx->config.timers.cycle_period_ms;
899 timer_cfg.callback = cycle_tick_callback;
900 timer_cfg.user_ctx = ctx;
901 status = rte_timer_create(&ctx->cycle_timer_storage, &timer_cfg, &ctx->cycle_timer_handle);
902 if (status != RTE_STATUS_OK)
903 {
904 RTE_EXAMPLE_LOG("[%s] cycle timer_create failed (%s)\n", ctx->role_tag, rte_status_to_string(status));
905 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "TIMER",
906 "cycle timer_create failed", NULL);
907 return status;
908 }
909 status = rte_timer_start(ctx->cycle_timer_handle);
910 if (status != RTE_STATUS_OK)
911 {
912 RTE_EXAMPLE_LOG("[%s] cycle timer_start failed (%s)\n", ctx->role_tag, rte_status_to_string(status));
913 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "TIMER",
914 "cycle timer_start failed", NULL);
915 }
916 return status;
917}
918
920{
922 unsigned int waited_ms;
923 uint32_t pending = 0U;
924 rte_status_t status;
925
926 if (ctx == NULL)
927 {
928 return RTE_STATUS_INVALID_PARAM;
929 }
930
931 /* One take per 10 ms pass: the pending ticks are read and cleared in one critical section (ISS-051), so the value
932 * that leaves the loop is the only read of the count. */
933 waited_ms = 0U;
934 status = ab_gp_cycle_ticks_take(&ctx->cycle_ticks, &pending);
935 while ((status == RTE_STATUS_OK) && (pending == 0U))
936 {
938 waited_ms += 10U;
939 /* Fast internal cycle: keep the negotiation link serviced (warm/hot standby acks) between RBC cycles. */
941 if (waited_ms >= ctx->config.timers.watchdog_timeout_ms)
942 {
943 return RTE_STATUS_TIMEOUT;
944 }
945 status = ab_gp_cycle_ticks_take(&ctx->cycle_ticks, &pending);
946 }
947 if (status != RTE_STATUS_OK)
948 {
949 return status;
950 }
951 {
952 /* The tick callback counts every timer expiry; after a stall (this process was starved, or a cycle overran)
953 * several ticks are pending. Consuming them all as ONE cycle made this channel's cycle number lag behind its
954 * sibling's after every stall (found live: ~700 cycles apart after 40 minutes, the A/B cross-compare then
955 * skipped every cycle). The missed ticks are counted, so the number keeps following real time. Bounded, so a
956 * long suspension cannot make the number jump by more than 20 cycles at once. */
957 const uint32_t missed = (pending > 1U) ? (pending - 1U) : 0U;
958
959 ctx->cycle += (missed > 20U) ? 20U : missed;
960 }
961
962 if (ctx->checkpoint_relaxed_until_ms != 0U)
963 {
964 rte_timestamp_ms_t now_ms = 0U;
965
966 (void)rte_timer_now(&now_ms);
967 if (now_ms >= ctx->checkpoint_relaxed_until_ms)
968 {
969 ctx->checkpoint_cfg.max_delay_ms = (rte_duration_ms_t)ctx->config.timers.checkpoint_max_delay_ms;
971 }
972 }
973
974 {
975 /* Stall detector: the rule is ab_gp_stall_lost_contact() (pure, unit-tested); both times are the full 64-bit
976 * monotonic time (ISS-052). */
977 rte_timestamp_ms_t wall_now = 0U;
978 rte_timestamp_ms_t stalled_ms = 0U;
979
980 (void)rte_timer_now(&wall_now);
981 if (ab_gp_stall_lost_contact(ctx->last_cycle_wall_ms, wall_now, ctx->is_online, &stalled_ms))
982 {
983 rte_site_role_note_lost_contact(&ctx->site_role);
984 RTE_EXAMPLE_LOG("[%s] stalled for %llu ms while ONLINE - the counterpart may have taken over, yielding if it is ONLINE\n",
985 ctx->role_tag, (unsigned long long)stalled_ms);
986 }
987 ctx->last_cycle_wall_ms = wall_now;
988 }
989
990 ctx->cyc_cycle = ctx->cycle;
991 ctx->cycle++;
992
993 return RTE_STATUS_OK;
994}
995
996/* ISS-020: every UTIL send goes through here (per-cycle status publish, JRU log forwarder; both on the cycle thread).
997 * Before the link's first successful open, each send is an open attempt that blocks until the status service answers
998 * the handshake or the timeout runs out, so: at most one attempt per RTE_EXAMPLE_UTIL_REOPEN_BACKOFF_MS, each bounded
999 * to RTE_EXAMPLE_UTIL_OPEN_ATTEMPT_TIMEOUT_MS; a refused send returns RTE_STATUS_TIMEOUT and the frame is dropped
1000 * (best-effort, as the callers already document). Once open, the gate always allows and the send is the one it
1001 * always was. */
1002static rte_status_t send_util_gated(ab_gp_channel_context_t *ctx, const uint8_t *bytes, size_t size)
1003{
1004 ab_gp_util_link_gate_t *gate = &ctx->com.util_gate;
1005 rte_timestamp_ms_t now_ms = 0U;
1006 rte_status_t status;
1007
1008 (void)rte_timer_now(&now_ms);
1009 if (!ab_gp_util_link_gate_may_attempt(gate, now_ms))
1010 {
1011 status = RTE_STATUS_TIMEOUT;
1012 }
1013 else if (ab_gp_util_link_gate_is_open(gate))
1014 {
1015 status = rte_channel_service_send(&ctx->com.relay_channel[RTE_EXAMPLE_RELAY_KIND_UTIL], bytes, size,
1017 }
1018 else
1019 {
1020 ab_gp_util_link_gate_on_attempt(gate, now_ms);
1021 status = rte_channel_service_send(&ctx->com.relay_channel[RTE_EXAMPLE_RELAY_KIND_UTIL], bytes, size,
1023 (void)rte_timer_now(&now_ms);
1024 ab_gp_util_link_gate_on_result(gate, status, now_ms);
1025 }
1026 return status;
1027}
1028
1030{
1032 /* Sized for the widest relay kind (Train, RBC_TRAIN_ENVELOPE_WIRE_SIZE)
1033 * and shared across IL/CTC too - a genuinely-96-byte IL/CTC datagram
1034 * read into this bigger buffer just leaves its own tail untouched,
1035 * harmless since those domains' own decode never reads past their
1036 * own known 96-byte shape. */
1037 uint8_t env_bytes[RBC_TRAIN_ENVELOPE_WIRE_SIZE];
1038 uint32_t drained_count = 0U;
1039 uint8_t kind_idx;
1040
1041 if (ctx == NULL)
1042 {
1043 return 0U;
1044 }
1045
1046 /* ADR-036: one link per {kind}; the callback's index is always 0 -
1047 * the receiving domain code routes by the envelope's own train_id.
1048 * ADR-038: the UTIL link carries rbc_util_frame_t, not rbc_envelope_t. A/B mostly only
1049 * PUBLISH on it (post_execute.c) - but Gateway now also relays RBC_UTIL_TOPIC_KM114_FILE
1050 * chunks down this same link (Platform_Protocol_KM114/CLAUDE.md's "Consumption design"),
1051 * so it must be drained too - handled separately below, not through @p cb (util frames are
1052 * not rbc_envelope_t and would misparse). */
1053 for (kind_idx = 0U; kind_idx < (uint8_t)RTE_EXAMPLE_RELAY_KIND_COUNT; kind_idx++)
1054 {
1056
1057 if (kind == RTE_EXAMPLE_RELAY_KIND_UTIL)
1058 {
1059 uint8_t util_bytes[RBC_UTIL_FRAME_SIZE];
1060 rbc_util_frame_t util_frame;
1061
1062 /* ISS-020: no read before the link's first successful open. A read on the unopened link triggers the
1063 * lazy open, and its timeout 0 means "the channel's connect_timeout_ms" (1000 ms for ab-relay-util), so
1064 * this poll blocked the cycle thread ~1.3 s every cycle while the status service was absent (measured
1065 * 1298 ms: the 1000 ms open plus a nested 300 ms open from the JRU forwarder republishing the open's
1066 * own INFO line). The open is driven by the gated UTIL sends (ab_gp_channel_send_relay()); the Gateway
1067 * cannot send on this link before it has heard from A/B anyway, so nothing is lost by not reading. */
1069 {
1070 continue;
1071 }
1072 while (rte_channel_service_read(&ctx->com.relay_channel[kind_idx], util_bytes,
1073 sizeof(util_bytes), 0U) == RTE_STATUS_OK)
1074 {
1075 if (rbc_util_frame_decode(util_bytes, &util_frame) &&
1076 (util_frame.msg_type == RBC_UTIL_MSG_PUBLISH) &&
1077 (util_frame.topic_id == (uint16_t)RBC_UTIL_TOPIC_KM114_FILE))
1078 {
1079 ab_gp_km114_relay_on_frame(&util_frame, ctx->role_tag, ctx->site, (ab_gp_channel_handle_t)ctx);
1080 }
1081 else if (rbc_util_frame_decode(util_bytes, &util_frame) &&
1082 (util_frame.msg_type == RBC_UTIL_MSG_PUBLISH) &&
1083 (util_frame.topic_id == (uint16_t)RBC_UTIL_TOPIC_TAKEOVER_CONFIRM))
1084 {
1086 }
1087 /* Any other util topic/msg_type arriving here has no receiver today - dropped,
1088 * same as before this Gateway->A/B direction existed. */
1089 }
1090 continue;
1091 }
1092
1093 /* Zeroed before every read (not just once) so a genuinely-smaller
1094 * frame (e.g. a 96-byte flat-scheme kind, or IL/CTC's own always-96
1095 * traffic) leaves this buffer's own tail deterministic zero rather
1096 * than the previous iteration's bytes - callers key off each
1097 * kind's own known shape, but must never see stale data past it. */
1098 size_t read_size = (kind == RTE_EXAMPLE_RELAY_KIND_TRAIN) ? RBC_TRAIN_ENVELOPE_WIRE_SIZE
1100 rte_mem_set(env_bytes, 0, sizeof(env_bytes));
1101 while (rte_channel_service_read(&ctx->com.relay_channel[kind_idx],
1102 env_bytes, read_size, 0U) == RTE_STATUS_OK)
1103 {
1104 drained_count++;
1105 if (cb != NULL)
1106 {
1107 cb(user_ctx, kind, 0U, env_bytes);
1108 }
1109 rte_mem_set(env_bytes, 0, sizeof(env_bytes));
1110 }
1111 }
1112
1113 return drained_count;
1114}
1115
1117{
1119 uint8_t in_bytes[PEER_WIRE_FRAME_SIZE];
1120
1121 if (ctx == NULL)
1122 {
1123 return;
1124 }
1125
1126 while (rte_channel_service_read(&ctx->com.peer_channel, in_bytes, sizeof(in_bytes), 0U) == RTE_STATUS_OK)
1127 {
1128 if (cb != NULL)
1129 {
1130 cb(user_ctx, in_bytes[0], &in_bytes[1], sizeof(in_bytes) - 1U);
1131 }
1132 }
1133}
1134
1136{
1138 uint8_t in_bytes[PEER_WIRE_FRAME_SIZE];
1139
1140 if (ctx == NULL)
1141 {
1142 return;
1143 }
1144
1145 while (rte_channel_service_read(&ctx->com.crosscompare_channel, in_bytes, sizeof(in_bytes), 0U) == RTE_STATUS_OK)
1146 {
1147 if (cb != NULL)
1148 {
1149 cb(user_ctx, in_bytes[0], &in_bytes[1], sizeof(in_bytes) - 1U);
1150 }
1151 }
1152}
1153
1154rte_status_t ab_gp_channel_send_crosscompare(ab_gp_channel_handle_t handle, uint8_t kind, const uint8_t *payload, size_t size)
1155{
1157 uint8_t out_bytes[PEER_WIRE_FRAME_SIZE];
1158
1159 if ((ctx == NULL) || (payload == NULL) || (size > (sizeof(out_bytes) - 1U)))
1160 {
1161 return RTE_STATUS_INVALID_PARAM;
1162 }
1163
1164 rte_mem_set(out_bytes, 0, sizeof(out_bytes));
1165 out_bytes[0] = kind;
1166 rte_mem_copy(&out_bytes[1], payload, size);
1167 return rte_channel_service_send(&ctx->com.crosscompare_channel, out_bytes, sizeof(out_bytes),
1169}
1170
1172 uint8_t index, const uint8_t *bytes, size_t size)
1173{
1175
1176 (void)index; /* ADR-036: one link per kind - envelope carries train_id */
1177
1178 if (ctx == NULL || bytes == NULL || size == 0U || kind >= RTE_EXAMPLE_RELAY_KIND_COUNT)
1179 {
1180 return RTE_STATUS_INVALID_PARAM;
1181 }
1182
1183 /* Historical note: the Train relay channel's own transport
1184 * (gateway_c_train_handler.c, C-side) used to forward every
1185 * A/B->TrainSim datagram at a FIXED RBC_TRAIN_ENVELOPE_WIRE_SIZE
1186 * width, because rte_channel_service_read() had no "actual bytes
1187 * received" out-parameter for it to forward only what really
1188 * arrived. That gap is closed (rte_channel_service_read_ex(),
1189 * Platform_RTE) and gateway_c_train_handler.c now uses it both
1190 * directions, so a genuinely shorter datagram on this channel would
1191 * reach the far end at its real length today, not silently regrown to
1192 * full width. The zero-pad below is kept anyway, as a belt-and-
1193 * suspenders match for TrainRBCSim's own train_sim.py (_pad(), same
1194 * reasoning) and, more importantly, for RBC_Test_CommServer's TCP hop
1195 * further downstream, which still requires fixed rbc.message_size=512
1196 * framing (TCP has no datagram boundaries of its own to preserve) -
1197 * removing this padding is only safe once that hop is also made
1198 * length-aware, which it is not yet (see docs/architecture/
1199 * SAFE_COMM_INTEGRATION.md). Zero-pad every Train-kind send up to the
1200 * channel's own full width here, at this one choke point, so "every
1201 * datagram on this channel is exactly RBC_TRAIN_ENVELOPE_WIRE_SIZE
1202 * bytes" still holds for BOTH directions. */
1203 if ((kind == RTE_EXAMPLE_RELAY_KIND_TRAIN) && (size < (size_t)RBC_TRAIN_ENVELOPE_WIRE_SIZE))
1204 {
1205 uint8_t padded[RBC_TRAIN_ENVELOPE_WIRE_SIZE];
1206
1207 rte_mem_set(padded, 0, sizeof(padded));
1208 rte_mem_copy(padded, bytes, size);
1209 return rte_channel_service_send(&ctx->com.relay_channel[kind], padded, sizeof(padded),
1211 }
1212
1213 if (kind == RTE_EXAMPLE_RELAY_KIND_UTIL)
1214 {
1215 return send_util_gated(ctx, bytes, size);
1216 }
1217
1218 return rte_channel_service_send(&ctx->com.relay_channel[kind], bytes, size, RTE_EXAMPLE_LINK_TIMEOUT_MS);
1219}
1220
1221rte_status_t ab_gp_channel_send_peer(ab_gp_channel_handle_t handle, const uint8_t *bytes, size_t size)
1222{
1224
1225 if (ctx == NULL || bytes == NULL || size == 0U)
1226 {
1227 return RTE_STATUS_INVALID_PARAM;
1228 }
1229
1230 return rte_channel_service_send(&ctx->com.peer_channel, bytes, size, RTE_EXAMPLE_LINK_TIMEOUT_MS);
1231}
1232
1234{
1236
1237 return (ctx == NULL) ? NULL : &ctx->saf_train;
1238}
1239
1241{
1243 const char *value = getenv("RTE_SAFECOMM_TRAIN_RBC_ID");
1244 char *end = NULL;
1245 unsigned long id;
1246 char info[32];
1247
1248 if (ctx == NULL)
1249 {
1250 return RTE_STATUS_INVALID_PARAM;
1251 }
1252 if ((value == NULL) || (value[0] == '\0'))
1253 {
1254 ab_gp_saf_train_init(&ctx->saf_train, false, ctx->is_role_a, 0U);
1255 return RTE_STATUS_OK;
1256 }
1257 id = strtoul(value, &end, 0);
1258 if ((end == value) || (*end != '\0') || (id == 0UL) || (id > 0xFFFFFFUL))
1259 {
1260 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "SAF",
1261 "RTE_SAFECOMM_TRAIN_RBC_ID is not a valid ETCS identity - refusing to start", NULL);
1262 return RTE_STATUS_INVALID_PARAM;
1263 }
1264 ab_gp_saf_train_init(&ctx->saf_train, true, ctx->is_role_a, (uint32_t)id);
1265 {
1266 const char *keys_path = getenv("RTE_SAFECOMM_TRAIN_KEYS_FILE");
1267 uint32_t loaded = 0U;
1268 char kinfo[32];
1269
1270 if ((keys_path != NULL) && (keys_path[0] != '\0'))
1271 {
1272 if (!ab_gp_saf_keys_load_file(keys_path, &loaded))
1273 {
1274 rte_log_write_event(RTE_LOG_LEVEL_ERROR, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "SAF",
1275 "RTE_SAFECOMM_TRAIN_KEYS_FILE unreadable or malformed - refusing to start",
1276 NULL);
1277 return RTE_STATUS_INVALID_PARAM;
1278 }
1279 (void)snprintf(kinfo, sizeof(kinfo), "keys=%u", (unsigned int)loaded);
1280 rte_log_write_event(RTE_LOG_LEVEL_INFO, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "SAF",
1281 "train KMACs loaded from RTE_SAFECOMM_TRAIN_KEYS_FILE", kinfo);
1282 }
1283 }
1284 (void)snprintf(info, sizeof(info), "rbc_etcs_id=%lu", id);
1285 rte_log_write_event(RTE_LOG_LEVEL_INFO, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "SAF",
1286 "train link protected (Subset-037 SaF terminated in this channel)", info);
1287 return RTE_STATUS_OK;
1288}
1289
1291 const char *log_message)
1292{
1294 rte_log_level_t log_level;
1295 uint8_t bytes[RBC_ENVELOPE_WIRE_SIZE];
1296 rte_timestamp_ms_t now_ms = 0U;
1297 rte_status_t status;
1298
1299 if (ctx == NULL)
1300 {
1301 return;
1302 }
1303
1304 switch (severity)
1305 {
1307 log_level = RTE_LOG_LEVEL_WARNING;
1308 break;
1310 log_level = RTE_LOG_LEVEL_ERROR;
1311 break;
1313 default:
1314 log_level = RTE_LOG_LEVEL_INFO;
1315 break;
1316 }
1317 /* Guaranteed side of this function's contract - see its own header
1318 * doc: this local log line happens regardless of whether the CTC
1319 * send below succeeds. */
1320 rte_log_write_event(log_level, site_config_name(ctx->site), ctx->cyc_cycle, ctx->role_tag, "CTC", "ALARM",
1321 (log_message != NULL) ? log_message : "-", NULL);
1322
1323 (void)rte_timer_now(&now_ms);
1324 ab_gp_build_ctc_alarm(bytes, severity, code, ctx->cyc_cycle, (uint32_t)now_ms);
1325 /* Direct ab_gp_channel_send_relay(), not ab_gp_channel_stage_relay():
1326 * every current caller is reacting to an already-decided fault right
1327 * before shutdown/reboot, so there is no next checkpoint round to
1328 * gate this on - see this function's own header doc on the ordering
1329 * requirement (must run before ab_gp_channel_shutdown()). */
1331 if (status != RTE_STATUS_OK)
1332 {
1333 /* Best-effort, per this function's own doc: a failed CTC send
1334 * must never block or fail the caller's own (already-decided)
1335 * fault reaction - the local log line above is the guaranteed
1336 * part of this contract, this is only a diagnostic best-effort
1337 * addition on top of it. */
1338 rte_log_write_event(RTE_LOG_LEVEL_WARNING, site_config_name(ctx->site), ctx->cyc_cycle, ctx->role_tag,
1339 "CTC", "ALARM_SEND_FAILED", rte_status_to_string(status), NULL);
1340 }
1341}
1342
1343rte_status_t ab_gp_channel_shutdown(void *context)
1344{
1346 uint8_t kind_idx;
1347
1348 if (ctx == NULL)
1349 {
1350 return RTE_STATUS_INVALID_PARAM;
1351 }
1352
1353 ctx->stop_requested = true;
1354
1355 (void)rte_voter_destroy(&ctx->checkpoint_voter);
1356 (void)rte_channel_destroy(&ctx->checkpoint_vc);
1357 ctx->checkpoint_cfg.voter = NULL;
1358
1359 if (ctx->cycle_timer_handle != NULL)
1360 {
1361 (void)rte_timer_stop(ctx->cycle_timer_handle);
1362 (void)rte_timer_destroy(ctx->cycle_timer_handle);
1363 ctx->cycle_timer_handle = NULL;
1364 }
1365 /* After the timer is destroyed (its thread joined), so nothing posts any more; also when the timer was never
1366 * created but the mutex was. Not set up (a shutdown before the cycle timer setup) is refused by the call and
1367 * ignored here. */
1369 if (ctx->watchdog != NULL)
1370 {
1371 (void)rte_watchdog_stop(ctx->watchdog);
1372 (void)rte_watchdog_destroy(ctx->watchdog);
1373 ctx->watchdog = NULL;
1374 }
1375
1376 (void)rte_channel_service_close(&ctx->com.peer_channel);
1377 (void)rte_channel_service_close(&ctx->com.crosscompare_channel);
1378
1379 for (kind_idx = 0U; kind_idx < (uint8_t)RTE_EXAMPLE_RELAY_KIND_COUNT; kind_idx++)
1380 {
1381 (void)rte_channel_service_close(&ctx->com.relay_channel[kind_idx]);
1382 }
1383
1385 RTE_EXAMPLE_LOG("[%s] done\n", ctx->role_tag);
1386 rte_log_write_event(RTE_LOG_LEVEL_INFO, site_config_name(ctx->site), 0U, ctx->role_tag, "-", "LIFECYCLE",
1387 "channel done", NULL);
1388 return RTE_STATUS_OK;
1389}
rte_status_t ab_gp_channel_saf_train_setup(ab_gp_channel_handle_t handle)
Reads RTE_SAFECOMM_TRAIN_RBC_ID and sets up the train-link SaF termination: unset leaves it off (plai...
static uint32_t message_size_for_relay_kind(rte_example_relay_kind_t rk)
rte_status_t ab_gp_channel_open_link(bool is_role_a, rte_example_site_t site, ab_gp_channel_handle_t *out_handle)
Opens this process's own A<->B peer channel and C relay channels.
static bool try_resolve_from_redundancy_config(const ab_gp_channel_context_t *ctx, const char *name, rte_netlink_config_t *config)
void ab_gp_channel_relay_queues_setup(ab_gp_channel_handle_t handle)
Initializes relay queues (no-op in single-threaded channel_service).
rte_status_t ab_gp_channel_cycle_timer_setup(ab_gp_channel_handle_t handle)
Creates and starts periodic cycle timer.
ab_gp_saf_train_t * ab_gp_channel_saf_train(ab_gp_channel_handle_t handle)
This channel's train-link SaF termination (ab_gp_saf_train.h), or NULL for a NULL handle.
static ab_gp_channel_context_t g_ab_ctx
static void cycle_tick_callback(rte_timer_handle_t handle, void *user_ctx)
static bool ab_gp_supports_redundancy(rte_redundancy_topology_t topology, uint32_t replica_count, void *context)
void ab_gp_channel_close_link(ab_gp_channel_handle_t handle)
Full teardown of a channel opened by ab_gp_channel_open_link().
void ab_gp_channel_poll_peer(ab_gp_channel_handle_t handle, ab_gp_com_peer_frame_cb cb, void *user_ctx)
Polls the A<->B peer channel non-blockingly and invokes cb for every received frame.
rte_status_t ab_gp_channel_send_peer(ab_gp_channel_handle_t handle, const uint8_t *bytes, size_t size)
Sends a frame over the A<->B peer channel.
rte_status_t ab_gp_channel_ga_setup(ab_gp_channel_handle_t handle)
GA registration for ma_logic_adaptation / il / ctc.
static void apply_host_env_override(const char *name, const ab_gp_channel_context_t *ctx, rte_netlink_config_t *config)
static void load_legacy_config(ab_gp_channel_context_t *ctx, rte_example_site_t site, bool is_role_a)
Load the legacy .conf config into ctx->config: RTE_CONFIG_FILE override, or the default etc/config/<r...
rte_status_t ab_gp_channel_negotiate_setup(ab_gp_channel_handle_t handle)
Opens inter-site negotiation link and runs startup tie-break.
static rte_status_t resolve_channel_hardcoded_fallback(const ab_gp_channel_context_t *ctx, const char *name, rte_netlink_config_t *config)
static rte_status_t channel_service_setup_with_retry(rte_channel_service_t *storage, const char *name)
static rte_status_t send_util_gated(ab_gp_channel_context_t *ctx, const uint8_t *bytes, size_t size)
rte_status_t ab_gp_channel_send_relay(ab_gp_channel_handle_t handle, rte_example_relay_kind_t kind, uint8_t index, const uint8_t *bytes, size_t size)
Sends an envelope over a specific C relay channel.
const rte_appmanager_checkpoint_config_t * ab_gp_channel_checkpoint_config(ab_gp_channel_handle_t handle)
Accessor for checkpoint config.
uint32_t ab_gp_channel_poll_relays(ab_gp_channel_handle_t handle, ab_gp_com_envelope_cb cb, void *user_ctx)
Polls all C relay channels non-blockingly and invokes cb for every received envelope.
rte_status_t ab_gp_channel_shutdown(void *context)
rte_appmanager_operations_t::shutdown hook.
static rte_status_t load_redundancy_config(ab_gp_channel_context_t *ctx, rte_example_site_t site, bool is_role_a)
Load the app-level redundancy topology config into ctx->redundancy (RCA/OCORA Phase 8).
static rte_status_t open_relay_channels(ab_gp_channel_context_t *ctx)
Open the relay channel for each rte_example_relay_kind_t.
rte_status_t ab_gp_channel_crosscompare_setup(ab_gp_channel_handle_t handle)
Creates cross-comparator on an already-open channel.
static bool relay_kind_from_name(const char *name, rte_example_relay_kind_t *out_kind)
void ab_gp_channel_raise_ctc_alarm(ab_gp_channel_handle_t handle, rbc_alarm_severity_t severity, uint32_t code, const char *log_message)
Raises a severity-leveled operational alarm: logs it locally (rte_log_write_event(),...
rte_status_t ab_gp_channel_dual_setup(ab_gp_channel_handle_t handle)
Wires dual-transfer liveness watchdog.
rte_status_t ab_gp_channel_init(void *context)
rte_appmanager_operations_t::init hook.
rte_status_t ab_gp_channel_checkpoint_setup(ab_gp_channel_handle_t handle)
Wires AppManager checkpoint mechanism onto peer channel.
static void ab_gp_channel_safestate_handler(rte_safestate_level_t level, rte_safestate_reason_t reason, const char *file, int32_t line, const char *message)
rte_status_t ab_gp_channel_train_codec_setup(ab_gp_channel_handle_t handle)
GP-internal registration for train codec.
static bool try_resolve_from_legacy_config(const ab_gp_channel_context_t *ctx, const char *name, rte_netlink_config_t *config)
rte_status_t ab_gp_channel_watchdog_setup(ab_gp_channel_handle_t handle)
Arms general health watchdog.
static rte_status_t resolve_channel_flow(const char *name, rte_netlink_config_t *config, void *context)
rte_status_t ab_gp_channel_send_crosscompare(ab_gp_channel_handle_t handle, uint8_t kind, const uint8_t *payload, size_t size)
Sends a frame over the dedicated A<->B cross-compare channel.
rte_status_t ab_gp_channel_wait_cycle_tick(ab_gp_channel_handle_t handle)
Waits for the periodic cycle timer tick, advances cycle, and refreshes watchdog/checkpoint state.
static void enforce_hardcoded_port(const ab_gp_channel_context_t *ctx, const char *name, rte_netlink_config_t *config)
void ab_gp_channel_poll_crosscompare(ab_gp_channel_handle_t handle, ab_gp_com_peer_frame_cb cb, void *user_ctx)
Polls the dedicated A<->B cross-compare channel non-blockingly and invokes cb for every received fram...
static rte_status_t resolve_channel(const char *name, rte_netlink_config_t *config, void *context)
struct ab_gp_channel_context_s * ab_gp_channel_handle_t
Opaque handle over this component's own static per-process context.
rte_status_t ab_gp_channel_checkpoint_backend_send(void *channel, const void *data, size_t data_size)
rte_channel_send_fn for the checkpoint transport: wraps the given rte_vital_message_t in a PEER_MSG_K...
rte_status_t ab_gp_channel_checkpoint_backend_recv(void *channel, void *data, size_t data_size, uint32_t timeout_ms)
rte_channel_recv_fn for the checkpoint transport: waits (bounded by timeout_ms) for a PEER_MSG_KIND_C...
rte_channel_t OSAdapter for the built-in AppManager checkpoint (ADR-019 addendum, rewired onto rte_vo...
rte_status_t ab_gp_channel_crosscompare_init(ab_gp_channel_context_t *ctx)
Creates this channel's rte_cross_comparator_t. Called once from ab_gp_channel_crosscompare_setup() (A...
The actual A-vs-B cross-compare decision: does this cycle's own M24/M15 answer match what the peer in...
Everything related to this channel's own "dual transfer" state: the single-mode degradation reaction ...
void ab_gp_channel_negotiate_on_takeover_confirm(ab_gp_channel_context_t *ctx, const char *payload)
Records a dispatcher confirmation that the other site is not ONLINE (from Gateway,...
void ab_gp_channel_negotiate_service_link(ab_gp_channel_context_t *ctx)
Fast internal-cycle service of the negotiation link (warm/hot standby only).
rte_status_t ab_gp_channel_negotiate_init(ab_gp_channel_context_t *ctx)
Opens this channel's own inter-site negotiation link (WEST LISTENs, EAST CONNECTs),...
void ab_gp_channel_negotiate_shutdown(ab_gp_channel_context_t *ctx)
This channel's own West<->East inter-site negotiation link - see ab_gp_channel.c's file header for ho...
Generic, data-blind wire framing for the A<->B peer link's AB_SAMPLE frame (CRC-64-protected,...
Modular type definitions for AB channel components (Com, Train, DB, Redundancy).
#define PEER_WIRE_FRAME_SIZE
void(*) ab_gp_com_peer_frame_cb(void *user_ctx, uint8_t kind, const uint8_t *payload, size_t size)
void ab_gp_channel_sleep_ms(unsigned int ms)
void(*) ab_gp_com_envelope_cb(void *user_ctx, rte_example_relay_kind_t kind, uint8_t index, const uint8_t *bytes)
Declares ab_gp_channel_sleep_ms() - see ab_gp_channel_types.h's own doc on why this tiny helper gets ...
rte_status_t ab_gp_config_load_file(const char *filepath, ab_gp_config_t *out_cfg)
Parses an INI-style configuration file into the static ab_gp_config_t structure.
rte_status_t ab_gp_config_init_defaults(rte_example_site_t site, bool is_role_a, ab_gp_config_t *out_cfg)
Initializes default configuration in memory based on site and role.
rte_status_t ab_gp_config_find_channel(const ab_gp_config_t *cfg, const char *channel_name, rte_netlink_config_t *out_netlink_cfg)
Resolves netlink configuration for a named channel from the parsed config.
rte_status_t ab_gp_cycle_ticks_take(ab_gp_cycle_ticks_t *ticks, uint32_t *out_pending)
Returns every pending tick and clears the count, as one critical section (cycle thread side).
rte_status_t ab_gp_cycle_ticks_teardown(ab_gp_cycle_ticks_t *ticks)
Destroys the mutex. Call only when no other task can post or take any more (the posting timer is dest...
rte_status_t ab_gp_cycle_ticks_setup(ab_gp_cycle_ticks_t *ticks)
Creates the mutex and clears the count. Setup phase only (rte_mutex_create() is a setup-only resource...
rte_status_t ab_gp_cycle_ticks_post(ab_gp_cycle_ticks_t *ticks)
Adds one tick (timer callback side). The count saturates at UINT32_MAX.
void ab_gp_ertms_engine_init(ab_gp_db_t *db)
Initializes the ERTMS procedure engine and all train contexts.
Unified ERTMS Subset-026 Chapter 5 Operational Procedure Orchestrator.
void ab_gp_build_ctc_alarm(uint8_t out[RBC_ENVELOPE_WIRE_SIZE], rbc_alarm_severity_t severity, uint32_t code, uint32_t cycle, uint32_t timestamp_ms)
Builds and wire-encodes an outbound CTC_ALARM envelope - GP's own now (moved from GA,...
GP's own dispatch side of the "GP calls into GA" surface - see ga_interface.h's own "functions GP cal...
void ab_gp_km114_relay_on_frame(const rbc_util_frame_t *frame, const char *role_tag, rte_example_site_t site, ab_gp_channel_handle_t handle)
Feeds one decoded RBC_UTIL_TOPIC_KM114_FILE publish frame into the reassembler.
A/B-side receiver for Gateway-relayed Subset-114 off-line KMC file bytes (RBC_UTIL_TOPIC_KM114_FILE,...
bool ab_gp_saf_keys_load_file(const char *path, uint32_t *out_count)
Loads KMACs from a text file: one "<etcs_id> <48 hex chars>" per line (decimal or 0x identity),...
void ab_gp_saf_train_init(ab_gp_saf_train_t *s, bool enabled, bool is_role_a, uint32_t rbc_etcs_id)
Resets s. enabled false makes every other call a no-op / rejection.
bool ab_gp_stall_lost_contact(rte_timestamp_ms_t last_ms, rte_timestamp_ms_t now_ms, bool is_online, rte_timestamp_ms_t *stalled_ms)
Whether a channel that wakes up at a cycle tick has stalled long enough to count as having lost conta...
GP-specific standby and site-link constants, the session-adoption rule and the stall rule.
rte_status_t ab_gp_train_init(void)
Registers this module's own ab_gp_train_session_t codec as a payload collector with com/ (AB/GP/com/a...
A/B's own train lifecycle handling against db/'s runtime table.
#define RTE_EXAMPLE_REASON_AB_PEER_LINK_DOWN
#define RTE_EXAMPLE_C_HOST_ENV
#define RTE_EXAMPLE_UTIL_OPEN_ATTEMPT_TIMEOUT_MS
#define RTE_EXAMPLE_LOG(...)
#define RTE_EXAMPLE_A_HOST_ENV
#define RTE_EXAMPLE_REMOTE_B_HOST_ENV
#define RBC_ENVELOPE_WIRE_SIZE
#define RBC_TRAIN_ENVELOPE_WIRE_SIZE
#define RTE_EXAMPLE_REMOTE_A_HOST_ENV
#define RTE_EXAMPLE_LINK_TIMEOUT_MS
#define RTE_EXAMPLE_CONNECT_TIMEOUT_MS
THE single door GA (safeAPIRBC2oo2GA, a separate shared-library project) uses to reach GP - GP export...
rte_status_t ab_ga_ma_adaptation_init(ab_gp_train_session_t *sessions, ab_gp_db_t *db, rte_example_site_t site, const char *role_tag)
GA's own single entry point - registers this channel's route- connection validator (ab_gp_ga_register...
ADR-038: the wire frame for the A/B<->C util side channel (RTE_EXAMPLE_RELAY_KIND_UTIL) and the C-sid...
#define RBC_UTIL_TOPIC_KM114_FILE
Gateway -> A/B: raw Subset-114 off-line KMC file bytes, chunked (see rbc_util_km114_relay....
#define RBC_UTIL_FRAME_SIZE
@ RBC_UTIL_MSG_PUBLISH
static bool rbc_util_frame_decode(const uint8_t in[RBC_UTIL_FRAME_SIZE], rbc_util_frame_t *out)
Decodes a util frame. Returns false only on a NULL arg or a payload_len past the buffer (a corrupt fr...
#define RBC_UTIL_TOPIC_TAKEOVER_CONFIRM
Gateway -> A/B: the dispatcher has confirmed that the OTHER site is not ONLINE, so a STANDBY whose co...
rbc_alarm_severity_t
Severity of an RBC_MSG_CTC_ALARM envelope (msg_type field on the wire) - deliberately a small,...
@ RBC_ALARM_SEVERITY_WARNING
@ RBC_ALARM_SEVERITY_INFO
@ RBC_ALARM_SEVERITY_ERROR
const char * site_config_name(rte_example_site_t site)
Definition site_config.c:52
uint16_t site_config_c_port_for_b(rte_example_site_t site, rte_example_relay_kind_t kind)
Definition site_config.c:76
uint16_t site_config_ab_port(rte_example_site_t site)
Definition site_config.c:57
const char * site_config_host(const char *env_var_name)
Resolves a CONNECT-role link's target host for this process.
uint16_t site_config_c_port_for_a(rte_example_site_t site, rte_example_relay_kind_t kind)
C's listen port for A's dedicated kind relay link, for this site (rte_example_relay_kind_t's own doc)...
Definition site_config.c:67
uint16_t site_config_ab_crosscompare_port(rte_example_site_t site)
Definition site_config.c:62
#define RTE_EXAMPLE_RELAY_KIND_COUNT
Definition site_config.h:50
rte_example_site_t
Definition site_config.h:20
@ RTE_EXAMPLE_SITE_WEST
Definition site_config.h:21
rte_example_relay_kind_t
Which client type a C<->A/C<->B relay link/port belongs to - this project's own dedicated-channel-per...
Definition site_config.h:39
@ RTE_EXAMPLE_RELAY_KIND_UTIL
Definition site_config.h:48
@ RTE_EXAMPLE_RELAY_KIND_CTC
Definition site_config.h:42
@ RTE_EXAMPLE_RELAY_KIND_IL
Definition site_config.h:41
@ RTE_EXAMPLE_RELAY_KIND_TRAIN
Definition site_config.h:40
Central application context owned by composition root (main.c).
rte_appmanager_checkpoint_config_t checkpoint_cfg
ab_gp_train_session_t sessions[RTE_EXAMPLE_MAX_TRAINS]
ab_gp_cycle_ticks_t cycle_ticks
rte_redundancy_config_t redundancy
rte_timestamp_ms_t checkpoint_relaxed_until_ms
rte_timer_handle_t cycle_timer_handle
rte_timestamp_ms_t last_cycle_wall_ms
rte_timer_storage_t cycle_timer_storage
ab_gp_util_link_gate_t util_gate
rte_channel_service_t relay_channel[RTE_EXAMPLE_RELAY_KIND_COUNT]
rte_channel_service_t peer_channel
rte_channel_service_t crosscompare_channel
ab_gp_config_timers_t timers
uint32_t checkpoint_relaxed_max_delay_ms
uint32_t checkpoint_max_delay_ms
uint32_t checkpoint_relax_duration_ms
uint32_t reconnect_attempt_timeout_ms
One channel's train-link SaF termination.
Decoded view of a util frame (payload is a pointer into the caller's buffer, NUL-terminated into payl...
char payload_z[RBC_UTIL_PAYLOAD_MAX+1U]
rbc_util_msg_type_t msg_type