package main import ( "encoding/binary" "flag" "fmt" "os" "path/filepath" "time" "github.com/warthog618/go-gpiocdev" "golang.org/x/sys/unix" ) const ( i2cDev = "/dev/i2c-1" gpioChip = "gpiochip0" gpioPLDPin = 6 maxAddr = 0x36 regVoltage = 0x02 regCapacity = 0x04 i2cSlave = 0x0703 // I2C_SLAVE maxRetries = 3 ) func readI2CWord(fd int, reg byte) (uint16, error) { if _, err := unix.Write(fd, []byte{reg}); err != nil { return 0, fmt.Errorf("write reg: %w", err) } buf := make([]byte, 2) n, err := unix.Read(fd, buf) if err != nil { return 0, fmt.Errorf("read: %w", err) } if n != 2 { return 0, fmt.Errorf("short read: %d bytes", n) } return binary.BigEndian.Uint16(buf), nil } func readI2CWordRetry(fd int, reg byte, validate func(uint16) bool) (uint16, error) { var lastErr error for i := 0; i < maxRetries; i++ { v, err := readI2CWord(fd, reg) if err == nil && validate(v) { return v, nil } if err != nil { lastErr = err } else { lastErr = fmt.Errorf("read out of range: 0x%04x", v) } time.Sleep(20 * time.Millisecond) } return 0, lastErr } func readBattery() (voltage float64, percent float64, err error) { fd, err := unix.Open(i2cDev, unix.O_RDWR, 0) if err != nil { return 0, 0, fmt.Errorf("open %s: %w", i2cDev, err) } defer unix.Close(fd) if err := unix.IoctlSetInt(fd, i2cSlave, maxAddr); err != nil { return 0, 0, fmt.Errorf("ioctl I2C_SLAVE: %w", err) } rawV, err := readI2CWordRetry(fd, regVoltage, func(v uint16) bool { f := float64(v) * 1.25 / 1000 / 16 return f > 2.0 && f < 5.0 }) if err != nil { return 0, 0, fmt.Errorf("voltage: %w", err) } voltage = float64(rawV) * 1.25 / 1000 / 16 rawC, err := readI2CWordRetry(fd, regCapacity, func(v uint16) bool { return float64(v)/256 <= 110 }) if err != nil { return 0, 0, fmt.Errorf("capacity: %w", err) } percent = float64(rawC) / 256 return voltage, percent, nil } func readACPower() (int, error) { line, err := gpiocdev.RequestLine(gpioChip, gpioPLDPin, gpiocdev.AsInput, gpiocdev.WithConsumer("x1208-exporter")) if err != nil { return 0, fmt.Errorf("request gpio: %w", err) } defer line.Close() v, err := line.Value() if err != nil { return 0, fmt.Errorf("read gpio: %w", err) } return v, nil } func writeAtomic(path string, content string) error { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o755); err != nil { return err } tmp, err := os.CreateTemp(dir, ".x1208.*.tmp") if err != nil { return err } tmpPath := tmp.Name() defer os.Remove(tmpPath) if _, err := tmp.WriteString(content); err != nil { tmp.Close() return err } if err := tmp.Chmod(0o644); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmpPath, path) } func main() { out := flag.String("out", "/var/lib/node-exporter/textfiles/x1208.prom", "output prom file") flag.Parse() var ( ac int voltage float64 percent float64 acErr, batErr error ) ac, acErr = readACPower() voltage, percent, batErr = readBattery() if acErr != nil && batErr != nil { fmt.Fprintf(os.Stderr, "all reads failed: ac=%v bat=%v\n", acErr, batErr) os.Exit(1) } now := time.Now().Unix() var content string content += "# HELP rpi_ups_last_update_seconds Unix timestamp of last successful exporter run.\n" content += "# TYPE rpi_ups_last_update_seconds gauge\n" content += fmt.Sprintf("rpi_ups_last_update_seconds %d\n", now) if acErr == nil { content += "# HELP rpi_ups_ac_power AC adapter present (1=plugged in, 0=on battery).\n" content += "# TYPE rpi_ups_ac_power gauge\n" content += fmt.Sprintf("rpi_ups_ac_power %d\n", ac) } else { fmt.Fprintf(os.Stderr, "ac read failed: %v\n", acErr) } if batErr == nil { content += "# HELP rpi_ups_voltage_volts Battery cell voltage from MAX17040 fuel gauge.\n" content += "# TYPE rpi_ups_voltage_volts gauge\n" content += fmt.Sprintf("rpi_ups_voltage_volts %.3f\n", voltage) content += "# HELP rpi_ups_battery_percent Battery state of charge from MAX17040 fuel gauge.\n" content += "# TYPE rpi_ups_battery_percent gauge\n" content += fmt.Sprintf("rpi_ups_battery_percent %.2f\n", percent) } else { fmt.Fprintf(os.Stderr, "battery read failed: %v\n", batErr) } if err := writeAtomic(*out, content); err != nil { fmt.Fprintf(os.Stderr, "write %s: %v\n", *out, err) os.Exit(1) } if acErr != nil || batErr != nil { os.Exit(1) } }