hdmistat/internal/statcollector/collector.go
sneak 402c0797d5 Initial implementation of hdmistat - Linux framebuffer system stats display
Features:
- Beautiful system statistics display using IBM Plex Mono font
- Direct framebuffer rendering without X11/Wayland
- Multiple screens with automatic carousel rotation
- Real-time system monitoring (CPU, memory, disk, network, processes)
- Systemd service integration with install command
- Clean architecture using uber/fx dependency injection

Architecture:
- Cobra CLI with daemon, install, status, and info commands
- Modular design with separate packages for display, rendering, and stats
- Font embedding for zero runtime dependencies
- Layout API for clean text rendering
- Support for multiple screen types (overview, top CPU, top memory)

Technical details:
- Uses gopsutil for cross-platform system stats collection
- Direct Linux framebuffer access via memory mapping
- Anti-aliased text rendering with freetype
- Configurable screen rotation and update intervals
- Structured logging with slog
- Comprehensive test coverage and linting setup

This initial version provides a solid foundation for displaying rich
system information on resource-constrained devices like Raspberry Pis.
2025-07-23 12:55:42 +02:00

225 lines
5.0 KiB
Go

package statcollector
import (
"log/slog"
"os"
"strings"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
psnet "github.com/shirou/gopsutil/v3/net"
"github.com/shirou/gopsutil/v3/process"
)
// SystemInfo represents overall system information
type SystemInfo struct {
Hostname string
Uptime time.Duration
MemoryTotal uint64
MemoryUsed uint64
MemoryFree uint64
CPUPercent []float64
Temperature map[string]float64
DiskUsage []DiskInfo
Network []NetworkInfo
Processes []ProcessInfo
CollectedAt time.Time
}
// DiskInfo represents disk usage information
type DiskInfo struct {
Path string
Total uint64
Used uint64
Free uint64
UsedPercent float64
}
// NetworkInfo represents network interface information
type NetworkInfo struct {
Name string
IPAddresses []string
LinkSpeed uint64
BytesSent uint64
BytesRecv uint64
PacketsSent uint64
PacketsRecv uint64
}
// ProcessInfo represents process information
type ProcessInfo struct {
PID int32
Name string
CPUPercent float64
MemoryRSS uint64
MemoryVMS uint64
Username string
}
// Collector interface for collecting system information
type Collector interface {
Collect() (*SystemInfo, error)
}
// SystemCollector implements Collector
type SystemCollector struct {
logger *slog.Logger
lastNetStats map[string]psnet.IOCountersStat
lastCollectTime time.Time
}
// NewSystemCollector creates a new system collector
func NewSystemCollector(logger *slog.Logger) *SystemCollector {
return &SystemCollector{
logger: logger,
lastNetStats: make(map[string]psnet.IOCountersStat),
}
}
// Collect gathers system information
func (c *SystemCollector) Collect() (*SystemInfo, error) {
info := &SystemInfo{
CollectedAt: time.Now(),
Temperature: make(map[string]float64),
}
// Hostname
hostname, err := os.Hostname()
if err != nil {
c.logger.Warn("getting hostname", "error", err)
info.Hostname = "unknown"
} else {
info.Hostname = hostname
}
// Uptime
uptimeSecs, err := host.Uptime()
if err != nil {
c.logger.Warn("getting uptime", "error", err)
} else {
info.Uptime = time.Duration(uptimeSecs) * time.Second
}
// Memory
vmStat, err := mem.VirtualMemory()
if err != nil {
c.logger.Warn("getting memory stats", "error", err)
} else {
info.MemoryTotal = vmStat.Total
info.MemoryUsed = vmStat.Used
info.MemoryFree = vmStat.Available
}
// CPU
cpuPercent, err := cpu.Percent(time.Second, true)
if err != nil {
c.logger.Warn("getting cpu percent", "error", err)
} else {
info.CPUPercent = cpuPercent
}
// Temperature
temps, err := host.SensorsTemperatures()
if err != nil {
c.logger.Warn("getting temperatures", "error", err)
} else {
for _, temp := range temps {
if temp.Temperature > 0 {
info.Temperature[temp.SensorKey] = temp.Temperature
}
}
}
// Disk usage
partitions, err := disk.Partitions(false)
if err != nil {
c.logger.Warn("getting partitions", "error", err)
} else {
for _, partition := range partitions {
if strings.HasPrefix(partition.Mountpoint, "/dev") ||
strings.HasPrefix(partition.Mountpoint, "/sys") ||
strings.HasPrefix(partition.Mountpoint, "/proc") {
continue
}
usage, err := disk.Usage(partition.Mountpoint)
if err != nil {
continue
}
info.DiskUsage = append(info.DiskUsage, DiskInfo{
Path: partition.Mountpoint,
Total: usage.Total,
Used: usage.Used,
Free: usage.Free,
UsedPercent: usage.UsedPercent,
})
}
}
// Network
interfaces, err := psnet.Interfaces()
if err != nil {
c.logger.Warn("getting network interfaces", "error", err)
} else {
ioCounters, _ := psnet.IOCounters(true)
ioMap := make(map[string]psnet.IOCountersStat)
for _, counter := range ioCounters {
ioMap[counter.Name] = counter
}
for _, iface := range interfaces {
if iface.Name == "lo" || strings.HasPrefix(iface.Name, "docker") {
continue
}
netInfo := NetworkInfo{
Name: iface.Name,
}
// Get IP addresses
for _, addr := range iface.Addrs {
netInfo.IPAddresses = append(netInfo.IPAddresses, addr.Addr)
}
// Get stats
if stats, ok := ioMap[iface.Name]; ok {
netInfo.BytesSent = stats.BytesSent
netInfo.BytesRecv = stats.BytesRecv
netInfo.PacketsSent = stats.PacketsSent
netInfo.PacketsRecv = stats.PacketsRecv
}
info.Network = append(info.Network, netInfo)
}
}
// Processes
processes, err := process.Processes()
if err != nil {
c.logger.Warn("getting processes", "error", err)
} else {
for _, p := range processes {
name, _ := p.Name()
cpuPercent, _ := p.CPUPercent()
memInfo, _ := p.MemoryInfo()
username, _ := p.Username()
info.Processes = append(info.Processes, ProcessInfo{
PID: p.Pid,
Name: name,
CPUPercent: cpuPercent,
MemoryRSS: memInfo.RSS,
MemoryVMS: memInfo.VMS,
Username: username,
})
}
}
c.lastCollectTime = time.Now()
return info, nil
}