drivers/relays: hardened + 100% MC/DC (Tier-1, first driver)

Bring relays.c into warden-sdk/drivers/ behind a relay_io seam (ADR-0005): the
sysfs plumbing is an injectable backend, the gpio root is $WARDEN_GPIO_ROOT-
overridable. Host harness (drivers/relays/test/) reaches 100% MC/DC (40/40
conditions, 100% lines) by combining fake-io unit tests (logic branches incl.
export->node-appears) with real-sysfs integration tests over a scratch tree
(backend fopen/stat branches). CI `mcdc` job enforces 100% across drivers/*/test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
BFE Engineering
2026-08-25 14:01:29 -06:00
co-authored by Claude Opus 4.8
parent d4acc9f3b3
commit 1752dea7b6
7 changed files with 516 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
/**
* The two onboard relays (86-Panel bottom board).
*
* RELAY1 <- GPIO1_A0 (sysfs gpio 32)
* RELAY2 <- GPIO1_A1 (sysfs gpio 33)
*
* gpiochip1 base is 32; A0..A7 are offsets 0..7. Nothing in the DT claims these
* (there is no relay driver), so we drive them through /sys/class/gpio directly.
* An off-by-one here would toggle the neighbouring RS485 pair (GPIO1_B0/B1)
* under a running Modbus master, so the numbers come from the schematic, not
* inference. State is never assumed at startup: a relay may be holding a
* contactor closed, and deciding it should be off because we just booted is not
* this module's call.
*
* Hardened for warden-sdk: the sysfs plumbing sits behind a `relay_io` seam so
* the decision logic runs and is measured to 100% MC/DC on the host, and the
* gpio root is `$WARDEN_GPIO_ROOT`-overridable so the real backend can run
* against a scratch tree in an integration test.
*/
#ifndef WARDEN_RELAYS_H
#define WARDEN_RELAYS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define WARDEN_RELAY_COUNT 2
/** Display name for @p idx, e.g. "Relay 1"; "-" if out of range. */
const char * warden_relay_name(uint32_t idx);
/** Current output state, false if it cannot be read. */
bool warden_relay_get(uint32_t idx);
/** Drive the output. Exports and sets the direction on first use. */
void warden_relay_set(uint32_t idx, bool on);
/** True if the GPIO is exported and usable — the page says so if it is not. */
bool warden_relay_available(uint32_t idx);
/* --- Hardware-abstraction seam ---------------------------------------------
* The four filesystem primitives the relay logic needs. Production binds the
* sysfs backend (the default); a unit test binds an in-memory fake that can
* model export -> node-appears, which a passive scratch tree cannot. */
struct relay_io {
bool (*exists)(const char *path); /* stat(path)==0 */
void (*write)(const char *path, const char *value); /* best-effort */
bool (*read_line)(const char *path, char *buf, size_t n); /* a line was read */
bool (*read_int)(const char *path, int *out); /* one int parsed */
};
/** Swap the io backend (test hook). Pass NULL to restore the sysfs default. */
void warden_relay__set_io(const struct relay_io *io);
#endif /* WARDEN_RELAYS_H */