ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
safety_scl.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file safety_scl.cpp
6 * @brief IEC 61784-3 black-channel SCL shared primitives - implementation. See safety_scl.h.
7 *
8 * The whole module is one small state machine, and every transition out of RUNNING is one-way. The
9 * elapsed-time test is an unsigned difference (`now - stamp >= limit`), which is what makes the
10 * watchdog wrap-safe: at a millis() rollover the subtraction wraps with it and still yields the true
11 * interval.
12 */
13
15
16#if PC_ENABLE_SAFETY_SCL
17
18namespace
19{
20// Latch the first fault: once fail-safe, later failures do not overwrite the diagnostically
21// interesting one that actually broke the connection.
22void trip(SclConn *c, SclFault why)
23{
24 // Both callers (pc_scl_on_frame, pc_scl_poll) already early-return when state == FAILSAFE, so
25 // trip() is never entered in that state; the guard latches the first fault as belt-and-suspenders.
26 if (c->state != SclState::FAILSAFE) // GCOVR_EXCL_BR_LINE false arm unreachable (see above)
27 {
28 c->state = SclState::FAILSAFE;
29 c->fault = why;
30 }
31}
32
33uint32_t wrap(uint32_t v, uint32_t mod)
34{
35 return mod ? (v % mod) : v;
36}
37} // namespace
38
39void pc_scl_init(SclConn *c, uint32_t first_counter, uint32_t counter_mod, uint32_t watchdog_ms, uint32_t now)
40{
41 if (!c)
42 {
43 return;
44 }
45 c->state = SclState::INIT;
46 c->fault = SclFault::PC_NONE;
47 c->counter_mod = counter_mod;
48 c->expected = wrap(first_counter, counter_mod);
49 c->watchdog_ms = watchdog_ms;
50 c->last_ok_ms = now;
51 c->accepted = 0;
52 c->rejected = 0;
53}
54
55bool pc_scl_on_frame(SclConn *c, bool signature_ok, uint32_t counter, uint32_t now)
56{
57 if (!c)
58 {
59 return false;
60 }
61 if (c->state == SclState::FAILSAFE)
62 {
63 c->rejected++; // refused, but the original fault stands
64 return false;
65 }
66
67 // Corruption is checked first: a frame whose signature failed cannot be trusted to carry a
68 // meaningful counter either, so reporting SIGNATURE is the honest diagnosis.
69 if (!signature_ok)
70 {
71 c->rejected++;
72 trip(c, SclFault::SIGNATURE);
73 return false;
74 }
75 if (wrap(counter, c->counter_mod) != c->expected)
76 {
77 c->rejected++;
78 trip(c, SclFault::COUNTER);
79 return false;
80 }
81
82 c->expected = pc_scl_next_counter(c->expected, c->counter_mod);
83 c->last_ok_ms = now;
84 c->accepted++;
85 c->state = SclState::RUNNING;
86 return true;
87}
88
89bool pc_scl_poll(SclConn *c, uint32_t now)
90{
91 if (!c)
92 {
93 return false;
94 }
95 if (c->state == SclState::FAILSAFE)
96 {
97 return false;
98 }
99 // Only a connection that has actually run can time out; one still in INIT is starting up, not
100 // silent. A zero watchdog disables the check entirely.
101 if (c->state == SclState::RUNNING && c->watchdog_ms && (now - c->last_ok_ms) >= c->watchdog_ms)
102 {
103 trip(c, SclFault::TIMEOUT);
104 return false;
105 }
106 return true;
107}
108
109void pc_scl_reset(SclConn *c, uint32_t first_counter, uint32_t now)
110{
111 if (!c)
112 {
113 return;
114 }
115 c->state = SclState::INIT;
116 c->fault = SclFault::PC_NONE;
117 c->expected = wrap(first_counter, c->counter_mod);
118 c->last_ok_ms = now;
119 // accepted / rejected deliberately preserved: they tally the whole session, not one connection
120 // attempt, so a flapping link is visible rather than reset away.
121}
122
123bool pc_scl_ok(const SclConn *c)
124{
125 return c && c->state != SclState::FAILSAFE;
126}
127
128SclState pc_scl_state(const SclConn *c)
129{
130 return c ? c->state : SclState::FAILSAFE; // a missing connection is not a usable one
131}
132
133SclFault pc_scl_fault(const SclConn *c)
134{
135 return c ? c->fault : SclFault::PC_NONE;
136}
137
138uint32_t pc_scl_next_counter(uint32_t counter, uint32_t counter_mod)
139{
140 uint32_t n = counter + 1u; // wraps naturally at 2^32 when no modulus is set
141 return counter_mod ? (n % counter_mod) : n;
142}
143
144#endif // PC_ENABLE_SAFETY_SCL
IEC 61784-3 black-channel Safety Communication Layer - the shared primitives (PC_ENABLE_SAFETY_SCL).