Safe API Framework
Layered API framework for safety-related applications (ERTMS RBC reference targeting CENELEC EN 50128 SIL 4)
Loading...
Searching...
No Matches
sapi_watchdog.c
Go to the documentation of this file.
1
21
26
27#include <stdbool.h>
28#include <string.h>
29
30#ifndef SAPI_WATCHDOG_MAX_COUNT
34#define SAPI_WATCHDOG_MAX_COUNT 8U
35#endif
36
60
64static uint8_t g_manager_initialized = 0U;
65
66/* ============================================================================
67 * Internal helpers
68 * ========================================================================== */
69
81static bool is_valid_handle(sapi_watchdog_t watchdog)
82{
83 const sapi_watchdog_s *slot = (const sapi_watchdog_s *)watchdog;
84
85 return (slot != NULL) && (slot >= &g_watchdog_pool[0]) && (slot < &g_watchdog_pool[SAPI_WATCHDOG_MAX_COUNT])
86 && (slot->in_use != 0U);
87}
88
95{
96 bool valid;
97
98 switch (action)
99 {
105 valid = true;
106 break;
107 default:
108 valid = false;
109 break;
110 }
111 return valid;
112}
113
114/* ============================================================================
115 * API: Watchdog Manager
116 * ========================================================================== */
117
119{
120 if (g_manager_initialized == 0U)
121 {
122 (void)memset(g_watchdog_pool, 0, sizeof(g_watchdog_pool));
124 sapi_log_write(SAPI_LOG_LEVEL_INFO, "watchdog", "manager initialized");
125 }
126 return SAPI_STATUS_OK;
127}
128
130{
131 size_t i;
132
133 for (i = 0U; i < (size_t)SAPI_WATCHDOG_MAX_COUNT; i++)
134 {
135 g_watchdog_pool[i].active = 0U;
136 }
138 sapi_log_write(SAPI_LOG_LEVEL_INFO, "watchdog", "manager shut down");
139 return SAPI_STATUS_OK;
140}
141
142/* ============================================================================
143 * API: Core Watchdog Operations
144 * ========================================================================== */
145
146sapi_status_t sapi_watchdog_create(sapi_watchdog_t *handle_out, const sapi_watchdog_config_t *config)
147{
148 size_t i;
149
150 if ((handle_out == NULL) || (config == NULL) || (config->timeout_ms == 0U) || !is_valid_action(config->action))
151 {
153 }
155 && (config->custom_action == NULL))
156 {
157 /* FAILOVER requires a real handler just like CUSTOM does (see
158 * sapi_watchdog_timeout_handler()'s own doc) - a FAILOVER watchdog
159 * with no handler wired up can only ever log, which is what
160 * ACTION_LOG is already for; requiring custom_action here catches
161 * that misconfiguration at create() time instead of silently
162 * degrading to a log line the first time it actually fires. */
164 }
165 if (g_manager_initialized == 0U)
166 {
168 }
169 {
170 /* REQ-LIFECYCLE-001 (ADR-026): a watchdog is a setup-only resource - refuse once the
171 * application's setup phase has been locked. */
173
174 if (lifecycle_status != SAPI_STATUS_OK)
175 {
176 return lifecycle_status;
177 }
178 }
179
180 for (i = 0U; i < (size_t)SAPI_WATCHDOG_MAX_COUNT; i++)
181 {
182 if (g_watchdog_pool[i].in_use == 0U)
183 {
184 (void)memset(&g_watchdog_pool[i], 0, sizeof(g_watchdog_pool[i]));
185 g_watchdog_pool[i].in_use = 1U;
186 g_watchdog_pool[i].config = *config;
187 *handle_out = (sapi_watchdog_t)&g_watchdog_pool[i];
188 sapi_log_write(SAPI_LOG_LEVEL_INFO, config->name, "watchdog created");
189 return SAPI_STATUS_OK;
190 }
191 }
193}
194
195sapi_status_t sapi_watchdog_start(sapi_watchdog_t watchdog)
196{
197 sapi_watchdog_s *slot = (sapi_watchdog_s *)watchdog;
198 sapi_timestamp_ms_t now_ms = 0U;
199
200 if (!is_valid_handle(watchdog))
201 {
203 }
204 (void)sapi_timer_now(&now_ms);
205 slot->active = 1U;
206 slot->fired = 0U;
207 slot->deadline_ms = now_ms + slot->config.timeout_ms;
208 slot->last_kick_ms = now_ms;
209 return SAPI_STATUS_OK;
210}
211
212sapi_status_t sapi_watchdog_stop(sapi_watchdog_t watchdog)
213{
214 sapi_watchdog_s *slot = (sapi_watchdog_s *)watchdog;
215
216 if (!is_valid_handle(watchdog))
217 {
219 }
220 slot->active = 0U;
221 return SAPI_STATUS_OK;
222}
223
224sapi_status_t sapi_watchdog_kick(sapi_watchdog_t watchdog)
225{
226 sapi_watchdog_s *slot = (sapi_watchdog_s *)watchdog;
227 sapi_timestamp_ms_t now_ms = 0U;
228
229 if (!is_valid_handle(watchdog))
230 {
232 }
233 if ((slot->active == 0U) || (slot->fired != 0U))
234 {
235 /* Not running, or already fired and awaiting an explicit
236 * start() before it can be kicked again - this codebase's real
237 * status enum has no generic SAPI_STATUS_ERROR (see
238 * sapi_status.h); SAPI_STATUS_INTERNAL_ERROR is the closest
239 * "kick while not in a kickable state" signal available. */
241 }
242 (void)sapi_timer_now(&now_ms);
243 slot->deadline_ms = now_ms + slot->config.timeout_ms;
244 slot->last_kick_ms = now_ms;
245 slot->kicks++;
246 return SAPI_STATUS_OK;
247}
248
250{
251 const sapi_watchdog_s *slot = (const sapi_watchdog_s *)watchdog;
252 sapi_timestamp_ms_t now_ms = 0U;
253
254 if (!is_valid_handle(watchdog) || (status_out == NULL))
255 {
257 }
258 (void)sapi_timer_now(&now_ms);
259 status_out->active = slot->active;
260 status_out->kicks = slot->kicks;
261 status_out->fires = slot->fires;
262 status_out->recoveries = slot->recoveries;
263 status_out->time_since_last_kick =
264 (sapi_duration_ms_t)((now_ms >= slot->last_kick_ms) ? (now_ms - slot->last_kick_ms) : 0U);
265 status_out->time_until_fire = (sapi_duration_ms_t)((slot->deadline_ms > now_ms) ? (slot->deadline_ms - now_ms) : 0U);
266 return SAPI_STATUS_OK;
267}
268
269sapi_status_t sapi_watchdog_destroy(sapi_watchdog_t watchdog)
270{
271 sapi_watchdog_s *slot = (sapi_watchdog_s *)watchdog;
272
273 if (!is_valid_handle(watchdog))
274 {
276 }
277 slot->active = 0U;
278 slot->in_use = 0U;
279 return SAPI_STATUS_OK;
280}
281
282/* ============================================================================
283 * Internal: Timeout Dispatch
284 * ========================================================================== */
285
286void sapi_watchdog_timeout_handler(uint32_t watchdog_id)
287{
288 sapi_watchdog_s *slot;
289
290 if (watchdog_id >= (uint32_t)SAPI_WATCHDOG_MAX_COUNT)
291 {
292 return;
293 }
294 slot = &g_watchdog_pool[watchdog_id];
295 if ((slot->in_use == 0U) || (slot->active == 0U) || (slot->fired != 0U))
296 {
297 return;
298 }
299
300 slot->fired = 1U;
301 slot->fires++;
302
303 switch (slot->config.action)
304 {
306 sapi_log_write(SAPI_LOG_LEVEL_ERROR, slot->config.name, "watchdog timeout");
307 break;
309 slot->recoveries++;
310 sapi_log_write(SAPI_LOG_LEVEL_ERROR, slot->config.name, "watchdog timeout - entering SAFE state");
312 (int32_t)__LINE__, slot->config.name);
313 break; /* Not statically unreachable: sapi_safestate_enter() has no
314 * [[noreturn]]/_Noreturn attribute, so the compiler cannot
315 * prove this dead - kept for switch-statement completeness. */
317 slot->recoveries++;
318 sapi_log_write(SAPI_LOG_LEVEL_ERROR, slot->config.name, "watchdog timeout - requesting reboot");
320 (int32_t)__LINE__, slot->config.name);
321 break; /* Same non-return note as SAFESTATE above. */
323 slot->recoveries++;
324 sapi_log_write(SAPI_LOG_LEVEL_ERROR, slot->config.name, "watchdog timeout - failover requested");
325 /* sapi_watchdog_create() requires config.custom_action != NULL
326 * for this action (see its own doc) - dispatched exactly like
327 * ACTION_CUSTOM, just under a name that documents *why* the
328 * integrator registered this watchdog (loss of a redundant
329 * peer/channel) rather than *how* it reacts, which is
330 * identical to CUSTOM's own mechanism. This used to be a dead
331 * stub ("no generic failover primitive in this framework;
332 * caller must poll sapi_watchdog_get_status()") - replaced
333 * once a real integrator (safeAPIRBC2oo2's dual-channel A/B
334 * link) needed exactly this reaction and found nothing to
335 * call. */
336 if (slot->config.custom_action != NULL)
337 {
338 slot->config.custom_action(slot->config.context);
339 }
340 break;
342 slot->recoveries++;
343 if (slot->config.custom_action != NULL)
344 {
345 slot->config.custom_action(slot->config.context);
346 }
347 break;
348 default: /* GCOVR_EXCL_LINE - see rationale below */
349 /* Defense-in-depth, not a reachable API path:
350 * sapi_watchdog_create() rejects any config.action that fails
351 * is_valid_action() before a slot is ever populated, so a live
352 * slot's action is always one of the 5 named cases above.
353 * There is no public way to mutate an already-created slot's
354 * action, so this default cannot be reached by any caller -
355 * kept for switch-statement completeness against a future
356 * sapi_watchdog_action_t value, not because it is exercised
357 * today. See docs/COVERAGE_REPORT.md for the accepted-
358 * exception rationale (this is one of the project's two
359 * documented gaps against the 100% branch coverage target). */
360 sapi_log_write(SAPI_LOG_LEVEL_ERROR, slot->config.name, "watchdog timeout - unrecognized action"); /* GCOVR_EXCL_LINE */
361 break; /* GCOVR_EXCL_LINE */
362 }
363}
364
365/* ============================================================================
366 * Timer Integration
367 * ========================================================================== */
368
370{
371 /* Polling design, not an ISR/hardware-timer callback: this framework's
372 * only portable time source is sapi_timer_now() (a plain "read the
373 * clock" query, not a way to register a recurring OS-level interrupt
374 * across every target this framework claims to support - POSIX,
375 * QNX, bare-metal SysTick, etc.). Giving this module its own
376 * platform-specific interrupt setup would duplicate what
377 * safeapi::timer already exists to abstract, and would break the
378 * "backends are integrator-supplied" philosophy (ADR-005) for a
379 * module whose own header was never given a backend vtable. Instead:
380 * whatever already runs periodically in the integrating application
381 * (its own sapi_timer periodic callback, or just its own main loop)
382 * is expected to call this function regularly - each call is an O(N)
383 * scan (N = SAPI_WATCHDOG_MAX_COUNT, a small fixed pool) comparing
384 * each active watchdog's deadline against the current time. */
385 sapi_timestamp_ms_t now_ms = 0U;
386 uint32_t i;
387
388 if (g_manager_initialized == 0U)
389 {
390 return;
391 }
392 (void)sapi_timer_now(&now_ms);
393 for (i = 0U; i < (uint32_t)SAPI_WATCHDOG_MAX_COUNT; i++)
394 {
395 const sapi_watchdog_s *slot = &g_watchdog_pool[i];
396
397 if ((slot->in_use != 0U) && (slot->active != 0U) && (slot->fired == 0U) && (now_ms >= slot->deadline_ms))
398 {
400 }
401 }
402}
sapi_status_t sapi_lifecycle_check_setup_allowed(void)
Convenience check for a setup-only constructor: call this as one of the first checks in any function ...
void sapi_log_write(sapi_log_level_t level, const char *tag, const char *message)
Emits one log message. Non-blocking; never fails the caller's control flow even if the message is dro...
Definition sapi_log.c:108
@ SAPI_LOG_LEVEL_ERROR
Definition sapi_log.h:38
@ SAPI_LOG_LEVEL_INFO
Definition sapi_log.h:36
#define SAPI_SAFESTATE_REASON_UNSPECIFIED
void sapi_safestate_enter(sapi_safestate_level_t level, sapi_safestate_reason_t reason, const char *file, int32_t line, const char *message)
Enters a safe-state level: invokes the registered handler (if any), then, for SAPI_SAFESTATE_LEVEL_SA...
@ SAPI_SAFESTATE_LEVEL_REBOOT
@ SAPI_SAFESTATE_LEVEL_SAFE
sapi_status_t
Common result/status codes.
Definition sapi_status.h:27
@ SAPI_STATUS_INTERNAL_ERROR
Definition sapi_status.h:38
@ SAPI_STATUS_INVALID_PARAM
Definition sapi_status.h:29
@ SAPI_STATUS_RESOURCE_EXHAUSTED
Definition sapi_status.h:33
@ SAPI_STATUS_NOT_INITIALIZED
Definition sapi_status.h:30
@ 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
sapi_status_t sapi_watchdog_stop(sapi_watchdog_t watchdog)
Stop watchdog timer.
sapi_status_t sapi_watchdog_manager_initialize(void)
Initialize watchdog manager (call once at startup).
sapi_status_t sapi_watchdog_get_status(sapi_watchdog_t watchdog, sapi_watchdog_status_t *status_out)
Get watchdog status.
sapi_status_t sapi_watchdog_destroy(sapi_watchdog_t watchdog)
Destroy watchdog.
void sapi_watchdog_timeout_handler(uint32_t watchdog_id)
Watchdog timeout handler (INTERNAL - called by framework).
sapi_watchdog_action_t
Recovery action when watchdog fires.
sapi_status_t sapi_watchdog_create(sapi_watchdog_t *handle_out, const sapi_watchdog_config_t *config)
Create a watchdog.
sapi_status_t sapi_watchdog_manager_shutdown(void)
Shutdown watchdog manager (call once at shutdown).
sapi_status_t sapi_watchdog_kick(sapi_watchdog_t watchdog)
Kick (pet) watchdog - prove liveness.
sapi_status_t sapi_watchdog_start(sapi_watchdog_t watchdog)
Start watchdog timer.
void sapi_watchdog_timer_tick(void)
Poll all active watchdogs for expiry (call periodically).
@ SAPI_WATCHDOG_ACTION_LOG
@ SAPI_WATCHDOG_ACTION_REBOOT
@ SAPI_WATCHDOG_ACTION_SAFESTATE
@ SAPI_WATCHDOG_ACTION_FAILOVER
@ SAPI_WATCHDOG_ACTION_CUSTOM
Process-wide application setup-phase lock (ADR-026).
OS Abstraction Layer - Logging/diagnostics service.
Safe-state transitions and checked assertions (ADR-004).
OS Abstraction Layer - Timer service.
static sapi_watchdog_s g_watchdog_pool[SAPI_WATCHDOG_MAX_COUNT]
Fixed-size static pool backing every sapi_watchdog_t handle.
static bool is_valid_handle(sapi_watchdog_t watchdog)
Defensive check that handle actually points at one of this module's own live pool slots,...
#define SAPI_WATCHDOG_MAX_COUNT
static uint8_t g_manager_initialized
1 once sapi_watchdog_manager_initialize() has been called.
static bool is_valid_action(sapi_watchdog_action_t action)
Checks whether action is one of the defined sapi_watchdog_action_t enumerators.
Watchdog mechanism for detecting system/task hang conditions.
Watchdog configuration.
sapi_watchdog_action_t action
void(*) custom_action(void *ctx)
sapi_duration_ms_t timeout_ms
Watchdog handle (opaque).
uint32_t recoveries
sapi_timestamp_ms_t last_kick_ms
sapi_watchdog_config_t config
sapi_timestamp_ms_t deadline_ms
Watchdog health/status information.
sapi_duration_ms_t time_since_last_kick
sapi_duration_ms_t time_until_fire