rewrite as a daemon with shutdown on low battery
the oneshot+timer produced 8367 journal lines a day of systemd start/stop noise. now Type=notify with an internal ticker, logging transitions only. hardware sits behind sensor.Sensor; internal/monitor is pure, so the shutdown state machine is tested without a pi. four guards, each tested, because a false poweroff of a box with no physical access is worse than a missed one: - a failed read resets the arming window rather than pausing it - voltage must actually fall across the window, else it is a stuck sensor. this is the only defence against the ac line reading 0 while mains is connected, so it is load-bearing - a settle period stops a restart loop acting on early readings - flapping ac cannot accumulate verified on hardware 2026-08-09: after poweroff on battery the x1208 starts the pi again when mains returns, with no button press. that was the risk that could have made this feature unsafe. a failed tick rewrites the previous sample with sensor_healthy 0 and the failure counter, so last_update still freezes for the staleness alert while the failure stays visible. module path moved to git.mial.net. debian units and the compose overlay deleted; deployment lives in the infra repo.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// Package pld reads the X1208 power-loss-detect line.
|
||||
//
|
||||
// The HAT drives GPIO 6 high while mains power is present and low while the
|
||||
// Pi runs on cells.
|
||||
package pld
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/warthog618/go-gpiocdev"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultChip is addressed by name, not by /dev path, so this does not
|
||||
// depend on any distribution-specific symlink.
|
||||
DefaultChip = "gpiochip0"
|
||||
// DefaultLine is the power-loss-detect line on the X1208.
|
||||
DefaultLine = 6
|
||||
|
||||
consumer = "x1208-exporter"
|
||||
)
|
||||
|
||||
// Line holds the GPIO request open across reads. The kernel grants the request
|
||||
// exclusively, so reacquiring it per read risks EBUSY against our own
|
||||
// not-yet-released handle.
|
||||
type Line struct {
|
||||
chip string
|
||||
offset int
|
||||
line *gpiocdev.Line
|
||||
}
|
||||
|
||||
// New returns a Line that has not yet been requested. The first Read requests it.
|
||||
func New(chip string, offset int) *Line {
|
||||
if chip == "" {
|
||||
chip = DefaultChip
|
||||
}
|
||||
return &Line{chip: chip, offset: offset}
|
||||
}
|
||||
|
||||
func (l *Line) ensureOpen() error {
|
||||
if l.line != nil {
|
||||
return nil
|
||||
}
|
||||
line, err := gpiocdev.RequestLine(l.chip, l.offset,
|
||||
gpiocdev.AsInput,
|
||||
gpiocdev.WithConsumer(consumer))
|
||||
if err != nil {
|
||||
return fmt.Errorf("request %s line %d: %w", l.chip, l.offset, err)
|
||||
}
|
||||
l.line = line
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the line. It is safe to call more than once.
|
||||
func (l *Line) Close() error {
|
||||
if l.line == nil {
|
||||
return nil
|
||||
}
|
||||
line := l.line
|
||||
l.line = nil
|
||||
return line.Close()
|
||||
}
|
||||
|
||||
// Read reports whether mains power is present.
|
||||
//
|
||||
// On error the line is released so the next Read requests a fresh one.
|
||||
//
|
||||
// A read that succeeds is not proof that the value is meaningful: a floating
|
||||
// or misconfigured line returns 0 with no error. Callers that act on "no AC"
|
||||
// must corroborate it, not trust this alone.
|
||||
func (l *Line) Read() (acPresent bool, err error) {
|
||||
if err := l.ensureOpen(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = l.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
v, err := l.line.Value()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("read %s line %d: %w", l.chip, l.offset, err)
|
||||
}
|
||||
return v != 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user