182 lines
4.4 KiB
Go
182 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|