making lots of progress!
This commit is contained in:
35
cmd/fbsimplestat/README.md
Normal file
35
cmd/fbsimplestat/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# fbhello
|
||||
|
||||
A simple "Hello World" framebuffer application demonstrating the hdmistat carousel and layout APIs.
|
||||
|
||||
## Features
|
||||
|
||||
- Displays "Hello World" centered on the screen
|
||||
- Shows current time updating at 1 FPS
|
||||
- Shows uptime counter
|
||||
- Decorative border around the display
|
||||
- Falls back to terminal display if framebuffer is unavailable
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run with framebuffer (requires appropriate permissions)
|
||||
sudo ./fbhello
|
||||
|
||||
# Run with terminal display (if framebuffer fails)
|
||||
./fbhello
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
The application demonstrates:
|
||||
|
||||
1. Creating a custom screen that implements `FrameGenerator`
|
||||
2. Using the layout API to draw text and borders
|
||||
3. Setting up a carousel (though with only one screen)
|
||||
4. Proper signal handling for clean shutdown
|
||||
5. Fallback to terminal display when framebuffer is unavailable
|
||||
|
||||
## Exit
|
||||
|
||||
Press Ctrl+C to exit the application.
|
||||
BIN
cmd/fbsimplestat/fbhello
Executable file
BIN
cmd/fbsimplestat/fbhello
Executable file
Binary file not shown.
BIN
cmd/fbsimplestat/fbsimplestat
Executable file
BIN
cmd/fbsimplestat/fbsimplestat
Executable file
Binary file not shown.
384
cmd/fbsimplestat/main.go
Normal file
384
cmd/fbsimplestat/main.go
Normal file
@@ -0,0 +1,384 @@
|
||||
//nolint:mnd
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||
"git.eeqj.de/sneak/hdmistat/internal/layout"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultFontSize is the font size used throughout the application
|
||||
DefaultFontSize = 24
|
||||
)
|
||||
|
||||
// SimpleStatScreen implements the FrameGenerator interface
|
||||
type SimpleStatScreen struct {
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
// NewSimpleStatScreen creates a new simple stat screen
|
||||
func NewSimpleStatScreen() *SimpleStatScreen {
|
||||
return &SimpleStatScreen{}
|
||||
}
|
||||
|
||||
// Init initializes the screen with the display dimensions
|
||||
func (s *SimpleStatScreen) Init(width, height int) error {
|
||||
s.width = width
|
||||
s.height = height
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSystemUptime returns the system uptime
|
||||
func getSystemUptime() (time.Duration, error) {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 1 {
|
||||
return 0, fmt.Errorf("invalid /proc/uptime format")
|
||||
}
|
||||
|
||||
seconds, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return time.Duration(seconds * float64(time.Second)), nil
|
||||
}
|
||||
|
||||
// GenerateFrame generates a frame with system stats
|
||||
func (s *SimpleStatScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||
// Create a draw context that works directly on the provided grid
|
||||
draw := layout.NewDraw(grid)
|
||||
|
||||
// Clear the screen with a dark background
|
||||
draw.Clear()
|
||||
|
||||
// Get hostname
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
// Title - moved down one line
|
||||
draw.Color(layout.Color("cyan")).Bold()
|
||||
draw.TextCenter(0, 3, "%s: info", hostname)
|
||||
draw.Plain()
|
||||
|
||||
// Uptime
|
||||
uptime, err := getSystemUptime()
|
||||
if err != nil {
|
||||
uptime = 0
|
||||
}
|
||||
draw.Color(layout.Color("gray60"))
|
||||
draw.TextCenter(0, 4, "Uptime: %s", uptime.Round(time.Second).String())
|
||||
|
||||
// Starting Y position for stats (with extra blank line after uptime)
|
||||
statY := 7
|
||||
|
||||
// Calculate alignment positions
|
||||
labelX := 10
|
||||
valueX := 30 // For right-aligned values before the bar
|
||||
barX := 40 // Start of progress bars
|
||||
barWidth := 50 // Extended bar width
|
||||
|
||||
// CPU Usage
|
||||
cpuPercent, _ := cpu.Percent(time.Millisecond*100, false)
|
||||
cpuUsage := 0.0
|
||||
if len(cpuPercent) > 0 {
|
||||
cpuUsage = cpuPercent[0]
|
||||
}
|
||||
draw.Color(layout.Color("white"))
|
||||
draw.Text(labelX, statY, "CPU Usage:")
|
||||
draw.Text(valueX-6, statY, "%6.1f%%", cpuUsage)
|
||||
barGrid := draw.Grid(barX, statY, barWidth+1, 1)
|
||||
barGrid.Bar(0, 0, barWidth, cpuUsage, getCPUColor(cpuUsage))
|
||||
// Add CPU details below
|
||||
draw.Color(layout.Color("gray60"))
|
||||
cpuInfo := getCPUInfo()
|
||||
draw.Text(labelX+2, statY+1, "%s", cpuInfo)
|
||||
|
||||
// Memory Usage
|
||||
statY += 4 // Add extra blank line (3 + 1 for the CPU detail line)
|
||||
vmStat, _ := mem.VirtualMemory()
|
||||
memUsage := vmStat.UsedPercent
|
||||
draw.Color(layout.Color("white"))
|
||||
draw.Text(labelX, statY, "Memory:")
|
||||
draw.Text(valueX-6, statY, "%6.1f%%", memUsage)
|
||||
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||
barGrid.Bar(0, 0, barWidth, memUsage, getMemoryColor(memUsage))
|
||||
// Add usage details below
|
||||
draw.Color(layout.Color("gray60"))
|
||||
draw.Text(
|
||||
labelX+2,
|
||||
statY+1,
|
||||
"%s / %s",
|
||||
layout.Bytes(vmStat.Used),
|
||||
layout.Bytes(vmStat.Total),
|
||||
)
|
||||
|
||||
// System Temperature (only if available)
|
||||
temp := getSystemTemperature()
|
||||
if temp > 0 {
|
||||
statY += 4 // Add blank line (3 + 1 for the memory detail line)
|
||||
tempPercent := (temp / 100.0) * 100.0 // Assume 100°C as max
|
||||
if tempPercent > 100 {
|
||||
tempPercent = 100
|
||||
}
|
||||
draw.Color(layout.Color("white"))
|
||||
draw.Text(labelX, statY, "Temperature:")
|
||||
draw.Text(valueX-8, statY, "%6.1f°C", temp)
|
||||
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||
barGrid.Bar(0, 0, barWidth, tempPercent, getTempColor(temp))
|
||||
}
|
||||
|
||||
// Filesystem Usage
|
||||
if temp > 0 {
|
||||
statY += 3 // Add blank line after temperature
|
||||
} else {
|
||||
statY += 4 // Add blank line after memory (3 + 1 for the memory detail line)
|
||||
}
|
||||
fsPath, fsUsage, fsUsed, fsTotal := getLargestFilesystemUsageWithDetails()
|
||||
draw.Color(layout.Color("white"))
|
||||
draw.Text(labelX, statY, "Filesystem:")
|
||||
draw.Text(valueX-6, statY, "%6.1f%%", fsUsage)
|
||||
barGrid = draw.Grid(barX, statY, barWidth+1, 1)
|
||||
barGrid.Bar(0, 0, barWidth, fsUsage, getFSColor(fsUsage))
|
||||
// Add filesystem details below
|
||||
draw.Color(layout.Color("gray60"))
|
||||
draw.Text(
|
||||
labelX+2,
|
||||
statY+1,
|
||||
"%s: %s / %s",
|
||||
fsPath,
|
||||
layout.Bytes(fsUsed),
|
||||
layout.Bytes(fsTotal),
|
||||
)
|
||||
|
||||
// Add a decorative border
|
||||
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
|
||||
borderGrid.Border(layout.Color("gray30"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FramesPerSecond returns the desired frame rate (1 FPS for stat updates)
|
||||
func (s *SimpleStatScreen) FramesPerSecond() float64 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
// getCPUColor returns a color based on CPU usage
|
||||
func getCPUColor(percent float64) color.Color {
|
||||
if percent > 80 {
|
||||
return layout.Color("red")
|
||||
} else if percent > 50 {
|
||||
return layout.Color("yellow")
|
||||
}
|
||||
return layout.Color("green")
|
||||
}
|
||||
|
||||
// getMemoryColor returns a color based on memory usage
|
||||
func getMemoryColor(percent float64) color.Color {
|
||||
if percent > 90 {
|
||||
return layout.Color("red")
|
||||
} else if percent > 70 {
|
||||
return layout.Color("yellow")
|
||||
}
|
||||
return layout.Color("green")
|
||||
}
|
||||
|
||||
// getTempColor returns a color based on temperature
|
||||
func getTempColor(temp float64) color.Color {
|
||||
if temp > 80 {
|
||||
return layout.Color("red")
|
||||
} else if temp > 60 {
|
||||
return layout.Color("yellow")
|
||||
}
|
||||
return layout.Color("green")
|
||||
}
|
||||
|
||||
// getFSColor returns a color based on filesystem usage
|
||||
func getFSColor(percent float64) color.Color {
|
||||
if percent > 90 {
|
||||
return layout.Color("red")
|
||||
} else if percent > 80 {
|
||||
return layout.Color("yellow")
|
||||
}
|
||||
return layout.Color("green")
|
||||
}
|
||||
|
||||
// getCPUInfo returns CPU details like cores, threads, and clock speed
|
||||
func getCPUInfo() string {
|
||||
// Get CPU info
|
||||
cpuInfos, err := cpu.Info()
|
||||
if err != nil || len(cpuInfos) == 0 {
|
||||
return "Unknown CPU"
|
||||
}
|
||||
|
||||
// Get logical (threads) and physical core counts
|
||||
logical, _ := cpu.Counts(true)
|
||||
physical, _ := cpu.Counts(false)
|
||||
|
||||
// Get frequency info
|
||||
freq := cpuInfos[0].Mhz
|
||||
freqStr := ""
|
||||
if freq > 0 {
|
||||
if freq >= 1000 {
|
||||
freqStr = fmt.Sprintf("%.2f GHz", freq/1000.0)
|
||||
} else {
|
||||
freqStr = fmt.Sprintf("%.0f MHz", freq)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%d cores, %d threads @ %s",
|
||||
physical,
|
||||
logical,
|
||||
freqStr,
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d cores, %d threads", physical, logical)
|
||||
}
|
||||
|
||||
// getSystemTemperature returns the system temperature in Celsius
|
||||
func getSystemTemperature() float64 {
|
||||
// Try to get temperature from host sensors
|
||||
temps, err := host.SensorsTemperatures()
|
||||
if err != nil || len(temps) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Find the highest temperature
|
||||
maxTemp := 0.0
|
||||
for _, temp := range temps {
|
||||
if temp.Temperature > maxTemp {
|
||||
maxTemp = temp.Temperature
|
||||
}
|
||||
}
|
||||
|
||||
return maxTemp
|
||||
}
|
||||
|
||||
// getLargestFilesystemUsageWithDetails returns the path, usage percentage, used bytes, and total bytes
|
||||
func getLargestFilesystemUsageWithDetails() (string, float64, uint64, uint64) {
|
||||
partitions, err := disk.Partitions(false)
|
||||
if err != nil {
|
||||
return "/", 0.0, 0, 0
|
||||
}
|
||||
|
||||
var largestPath string
|
||||
var largestSize uint64
|
||||
var largestUsage float64
|
||||
var largestUsed uint64
|
||||
|
||||
for _, partition := range partitions {
|
||||
// Skip special filesystems
|
||||
if strings.HasPrefix(partition.Mountpoint, "/proc") ||
|
||||
strings.HasPrefix(partition.Mountpoint, "/sys") ||
|
||||
strings.HasPrefix(partition.Mountpoint, "/dev") ||
|
||||
strings.HasPrefix(partition.Mountpoint, "/run") ||
|
||||
partition.Fstype == "tmpfs" ||
|
||||
partition.Fstype == "devtmpfs" {
|
||||
continue
|
||||
}
|
||||
|
||||
usage, err := disk.Usage(partition.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the filesystem with the largest total size
|
||||
if usage.Total > largestSize {
|
||||
largestSize = usage.Total
|
||||
largestUsage = usage.UsedPercent
|
||||
largestUsed = usage.Used
|
||||
largestPath = partition.Mountpoint
|
||||
}
|
||||
}
|
||||
|
||||
if largestPath == "" {
|
||||
return "/", 0.0, 0, 0
|
||||
}
|
||||
|
||||
return largestPath, largestUsage, largestUsed, largestSize
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Set up signal handling
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Println("Received shutdown signal")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Create framebuffer display
|
||||
display, err := fbdraw.NewFBDisplayAuto()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open framebuffer: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := display.Close(); err != nil {
|
||||
log.Printf("Failed to close display: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create carousel with no rotation (single screen)
|
||||
carousel := fbdraw.NewCarousel(display, 0) // 0 means no rotation
|
||||
|
||||
// Set font size
|
||||
if err := carousel.SetFontSize(DefaultFontSize); err != nil {
|
||||
log.Fatalf("Failed to set font size: %v", err)
|
||||
}
|
||||
|
||||
// Add our simple stat screen with header
|
||||
statScreen := NewSimpleStatScreen()
|
||||
wrappedScreen := fbdraw.NewHeaderWrapper(statScreen)
|
||||
|
||||
if err := carousel.AddScreen("Simple Stats", wrappedScreen); err != nil {
|
||||
log.Fatalf("Failed to add screen: %v", err)
|
||||
}
|
||||
|
||||
// Run the carousel
|
||||
log.Println("Starting fbsimplestat...")
|
||||
log.Println("Press Ctrl+C to exit")
|
||||
|
||||
// Run carousel in a goroutine
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- carousel.Run()
|
||||
}()
|
||||
|
||||
// Wait for either context cancellation or carousel to finish
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Stopping carousel...")
|
||||
carousel.Stop()
|
||||
<-done // Wait for carousel to finish
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Printf("Carousel error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("fbsimplestat exited cleanly")
|
||||
}
|
||||
Reference in New Issue
Block a user