Skip to content

rippleon: GFCI fault policy (fault/warn) + contactor force-open latch fix#30

Merged
RAR merged 4 commits into
mainfrom
gfci-fault-policy
May 22, 2026
Merged

rippleon: GFCI fault policy (fault/warn) + contactor force-open latch fix#30
RAR merged 4 commits into
mainfrom
gfci-fault-policy

Conversation

@RAR

@RAR RAR commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Four commits, all bench-validated on the Rippleon ROC001 (2026-05-22):

  • Configurable GFCI fault policyfault (default; UL2231 latch, power-cycle clear) or warn (logs the trip + raises EVT_FAULT_RAISED for HA, but keeps the contactor closed). Persisted in boot_config; exposed as a HA select entity. warn is a bench/diagnostic escape for a known external leakage fault (here: common-mode leakage from an off-grid inverter).
  • ignore dropped after review — silent charge-through has no advantage over logged charge-through; policy is fault/warn only, and any non-WARN byte fails safe to FAULT.
  • Decision logic extracted to a pure core/gfci_policy module (mirrors core/over_temp), making the debounce / WARN-edge-latch / fail-safe rule host-testable — 20 new unit tests (test_gfci_policy + boot_config validator cases).
  • Contactor force-open latch fixevse_transition() armed the PB12 hardware force-open latch on every EVSE_FAULT but its release was dead code (*cur != next tested after *cur = next). Once armed the latch was never released, hardware-latching the main contactor open — "commanded closed" but 0 A drawn. Latent since 2026-05-03; masked by GFCI faults being power-cycle-only-clear, surfaced once WARN removed those power-cycles.

Test plan

  • Host suite 139/139 (ctest)
  • rippleon ARM firmware builds clean; safety_task.c + gfci_policy.c also compile clean for the nexcyber target
  • Bench: GFCI WARN verified (no GFCI fault raised on PE2 trip)
  • Bench: contactor fix verified — EV charges, contactor closes

🤖 Generated with Claude Code

RAR and others added 4 commits May 22, 2026 16:04
Adds a persisted, TLV-settable GFCI fault-handling policy to the
OpenEVCharger MCU firmware and the ESPHome component, exposed in HA
as a select entity ("GFCI Fault Policy").

- fault (default): unchanged UL2231 behaviour — latch FAULT_GFCI,
  force EVSE_FAULT, contactor force-open, power-cycle to clear.
- warn: log + EVT_FAULT_RAISED (visible in HA fault log) but keep the
  contactor closed — charging continues.
- ignore: detector still runs but takes no action.

MCU: new gfci_fault_policy byte in boot_config, carved from reserved[]
so a 0 byte in any pre-existing record reads as the safe FAULT default
(struct stays 32 B, backward compatible). New CMD_SET/GET_GFCI_POLICY +
EVT_GFCI_POLICY TLV, plumbed comms -> persist -> safety. check_gfci()
branches on the policy; WARN/IGNORE are edge-latched to emit once per
PE2-LOW episode. Loud boot log when policy != fault.

ESPHome: OpenevchargerTlvSelect + new select.py platform; "GFCI Fault
Policy" config entity in openevcharger.yaml.

warn/ignore suppress a real UL2231 ground-fault interlock and are
intended for bench/diagnostic use. Policy persists across power cycles.

Verified: native host core build + 119/119 tests pass. ARM firmware
and ESPHome image not yet compile-checked (toolchain swap pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ignore (detector runs, takes no action) only removed logging / HA
visibility versus warn — warn already keeps charging and at minimum
records the trip to the event log + EVT_FAULT_RAISED. There is no
case where silent charge-through beats logged charge-through, so the
option is dropped per review.

Wire enum is now FAULT=0, WARN=1 — contiguous, no renumbering.
check_gfci() drops the IGNORE branch; any policy byte other than WARN
now falls through to the FAULT path, so an unknown or stale value
fails safe to the UL2231 interlock. boot_config validator rejects
values above WARN. GFCI_POLICY_MAX, select.py options, the ESPHome
enum, the boot-log announce, and all comments updated to match.

Host build + 119/119 tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
check_gfci was a static in safety_task.c, which the host test harness
deliberately cannot build (FreeRTOS / SPL / HW headers). Mirror the
core/over_temp.c pattern: the debounce + WARN-edge-latch +
fail-safe-to-FAULT decision moves to a pure static-inline
gfci_policy_step() in core/gfci_policy.h; check_gfci keeps only the I/O
(fault_raise / post_fault_event / evse_transition / printk). Behaviour
is unchanged — a _Static_assert locks GFCI_POL_* against the
proto/commands.h GFCI_POLICY_* wire values.

tests/test_gfci_policy.c — 15 cases: FAULT raises after debounce; WARN
emits once per PE2-LOW episode, re-arms on release, never latches a
fault; unknown/stale policy bytes fail safe to FAULT; sub-debounce
no-ops; the WARN latch never blocks a live policy flip to FAULT.
test_boot_config.c — 5 cases for boot_config_set_gfci_fault_policy
(round-trips, validator rejects values above WARN).

Host suite 119 -> 139 cases, all pass. rippleon ARM firmware builds
clean; safety_task.c + gfci_policy.c also compile clean for nexcyber.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…er a fault

evse_transition() assigns *cur = next and then, in the else-if for
non-FAULT transitions, tested `*cur != next` — always false after that
assignment. relay_force_open_release() was therefore unreachable: once
the PB12 hardware force-open latch is armed on an EVSE_FAULT entry, the
firmware never releases it. Every subsequent relay_main_close() drives
PE12 HIGH into a latched-open contactor — "commanded closed" reads true
but the contactor never pulls in and the EV draws 0 A, then regresses
C->B.

The bug (commit 7fcba09, 2026-05-03) was masked for weeks: GFCI faults
are power-cycle-only-clear, so the mandatory power-cycle reset PB12
every session. It surfaced once the GFCI WARN policy removed those
power-cycles and an ordinary clearable fault armed the latch.

Fix: drop the dead `*cur != next` guard. The early-return at the top of
evse_transition() already guarantees a real state change, so every
non-FAULT transition must release an active force-open latch.

Recovery for an already-bricked unit: power-cycle (PB12 boots LOW).

rippleon ARM firmware builds clean; host suite 139/139.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@RAR RAR merged commit fc11db1 into main May 22, 2026
6 checks passed
@RAR RAR deleted the gfci-fault-policy branch May 22, 2026 20:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant