Files
mesh-host/internal/profile/power.go
T
jochen 41b3392789
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
Count the same meters the power module's sampler reads (hq ADR 0255)
An APU's integrated graphics and Intel's second (MMIO) view of a package made a machine
report a power meter the sampler would not read, so its bar could only say not
measured.
2026-10-08 14:44:55 +02:00

136 lines
5.0 KiB
Go

package profile
import (
"context"
"os"
"path/filepath"
"sort"
"strings"
)
// What this machine has to say about its own power (novox/hq ADR 0255): whether it has a battery,
// and whether anything on it can measure the power it draws.
//
// **Hardware, read from the kernel, as `seat` is.** A module that shows a battery, or the power a
// machine draws, is on every machine — the power module holds node-power everywhere — and what it
// shows depends on what is there. The controller composes such a piece only where the machine
// reported the capability, so a desktop never gets a battery block that would sit empty, and the
// bar never asks at run time what the mesh could have said once.
// Named power capabilities.
const (
// CapBattery is a battery that powers this machine: a power supply of type Battery that is not
// a device's (a mouse's or a headset's battery has scope Device).
CapBattery = "battery"
// CapPowerMeter is something here that measures the power the machine, or part of it, draws:
// the machine's battery, the processor package's energy counter (powercap, Intel's and AMD's
// RAPL), or a discrete graphics device that reports its own power — the same meters the power
// module's sampler reads, so a machine never gets a block that can only say "not measured".
CapPowerMeter = "power-meter"
)
// Where the kernel reports supplies and energy counters. Variables so a test can point them at a
// directory it made, rather than at whatever this machine happens to have.
var (
PowerSupplySource = "/sys/class/power_supply"
PowercapSource = "/sys/class/powercap"
NvidiaSource = "/proc/driver/nvidia/gpus"
)
// systemBatteries is every battery that powers this machine, by name.
func systemBatteries() []string {
entries, err := os.ReadDir(PowerSupplySource)
if err != nil {
return nil
}
var out []string
for _, e := range entries {
dir := filepath.Join(PowerSupplySource, e.Name())
if readTrimmed(filepath.Join(dir, "type")) != "Battery" {
continue
}
if readTrimmed(filepath.Join(dir, "scope")) == "Device" {
continue
}
// A bay with no battery in it still has a directory.
if readTrimmed(filepath.Join(dir, "present")) == "0" {
continue
}
out = append(out, e.Name())
}
sort.Strings(out)
return out
}
func readTrimmed(path string) string {
b, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}
type battery struct{}
func (battery) Name() string { return CapBattery }
func (battery) Detect(context.Context) Verdict {
const how = "/sys/class/power_supply/*/type — a Battery whose scope is not Device"
if found := systemBatteries(); len(found) > 0 {
return Verdict{Name: CapBattery, Present: true, Detail: strings.Join(found, ", "), How: how}
}
return Verdict{Name: CapBattery, Present: false, Detail: "no battery powers this machine", How: how}
}
type powerMeter struct{}
func (powerMeter) Name() string { return CapPowerMeter }
// Detect names every meter it found. Only that one exists: whether its counter can be read is the
// reader's question — powercap's energy counter is root's to read, and the power module samples it
// as root and publishes only a rate.
func (powerMeter) Detect(context.Context) Verdict {
const how = "a system battery; /sys/class/powercap/*/name starting package, each name once; a discrete " +
"graphics device's (mem_info_vram_vendor) hwmon power1_average or power1_input; /proc/driver/nvidia/gpus"
var found []string
for _, b := range systemBatteries() {
found = append(found, "battery "+b)
}
// Each package once by its name: Intel shows some twice, as intel-rapl:N and intel-rapl-mmio:N.
if entries, err := os.ReadDir(PowercapSource); err == nil {
seen := map[string]bool{}
for _, e := range entries {
dir := filepath.Join(PowercapSource, e.Name())
name := readTrimmed(filepath.Join(dir, "name"))
if !strings.HasPrefix(name, "package") || seen[name] {
continue
}
if _, err := os.Stat(filepath.Join(dir, "energy_uj")); err == nil {
seen[name] = true
found = append(found, "processor "+name)
}
}
}
if cards, err := filepath.Glob(filepath.Join(SeatSource, "card*", "device", "hwmon", "hwmon*")); err == nil {
for _, hw := range cards {
// Integrated graphics report the package the processor's counter already holds.
if _, err := os.Stat(filepath.Join(filepath.Dir(filepath.Dir(hw)), "mem_info_vram_vendor")); err != nil {
continue
}
for _, f := range []string{"power1_average", "power1_input"} {
if _, err := os.Stat(filepath.Join(hw, f)); err == nil {
found = append(found, "graphics "+strings.Split(strings.TrimPrefix(hw, SeatSource+"/"), "/")[0])
break
}
}
}
}
if gpus, err := os.ReadDir(NvidiaSource); err == nil && len(gpus) > 0 {
found = append(found, "graphics nvidia")
}
if len(found) == 0 {
return Verdict{Name: CapPowerMeter, Present: false, Detail: "nothing here measures the power drawn", How: how}
}
return Verdict{Name: CapPowerMeter, Present: true, Detail: strings.Join(found, ", "), How: how}
}