285 lines
6.1 KiB
Go
285 lines
6.1 KiB
Go
package fbdraw
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/hdmistat/internal/font"
|
|
)
|
|
|
|
// Screen represents a single screen in the carousel
|
|
type Screen struct {
|
|
Name string
|
|
Generator FrameGenerator
|
|
ticker *time.Ticker
|
|
stop chan struct{}
|
|
}
|
|
|
|
// Carousel manages rotating between multiple screens
|
|
type Carousel struct {
|
|
display FramebufferDisplay
|
|
screens []*Screen
|
|
currentIndex int
|
|
rotationInterval time.Duration
|
|
fontSize float64
|
|
gridWidth int
|
|
gridHeight int
|
|
|
|
mu sync.Mutex
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
const DefaultFontSize float64 = 24
|
|
|
|
// NewCarousel creates a new carousel
|
|
func NewCarousel(
|
|
display FramebufferDisplay,
|
|
rotationInterval time.Duration,
|
|
) *Carousel {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
c := &Carousel{
|
|
display: display,
|
|
screens: make([]*Screen, 0),
|
|
currentIndex: 0,
|
|
rotationInterval: rotationInterval,
|
|
fontSize: DefaultFontSize, // Default font size
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
|
|
// Calculate grid dimensions based on font size
|
|
if err := c.calculateGridDimensions(); err != nil {
|
|
// Log error but continue with default dimensions
|
|
log.Printf("Warning: failed to calculate grid dimensions: %v", err)
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// SetFontSize sets the font size and recalculates grid dimensions
|
|
func (c *Carousel) SetFontSize(size float64) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.fontSize = size
|
|
return c.calculateGridDimensions()
|
|
}
|
|
|
|
// calculateGridDimensions calculates grid size based on display and font
|
|
func (c *Carousel) calculateGridDimensions() error {
|
|
// Get pixel dimensions
|
|
pixelWidth, pixelHeight := c.display.PixelSize()
|
|
|
|
// Calculate character dimensions first
|
|
charWidth, charHeight, err := CalculateCharDimensions(
|
|
font.FamilyIBMPlexMono,
|
|
c.fontSize,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("calculating char dimensions: %w", err)
|
|
}
|
|
|
|
// Log the calculated dimensions for debugging
|
|
fmt.Printf(
|
|
"Font size: %.0f, Char dimensions: %dx%d pixels\n",
|
|
c.fontSize,
|
|
charWidth,
|
|
charHeight,
|
|
)
|
|
|
|
// Calculate grid dimensions
|
|
gridWidth, gridHeight, err := CalculateGridSize(
|
|
pixelWidth, pixelHeight,
|
|
font.FamilyIBMPlexMono, c.fontSize,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf(
|
|
"Display: %dx%d pixels, Grid: %dx%d chars\n",
|
|
pixelWidth,
|
|
pixelHeight,
|
|
gridWidth,
|
|
gridHeight,
|
|
)
|
|
|
|
c.gridWidth = gridWidth
|
|
c.gridHeight = gridHeight
|
|
|
|
// Update display if it's an FBDisplay
|
|
if fbDisplay, ok := c.display.(*FBDisplay); ok {
|
|
fbDisplay.charWidth = charWidth
|
|
fbDisplay.charHeight = charHeight
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddScreen adds a new screen to the carousel
|
|
func (c *Carousel) AddScreen(name string, generator FrameGenerator) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Initialize the generator with the calculated grid dimensions
|
|
if err := generator.Init(c.gridWidth, c.gridHeight); err != nil {
|
|
return fmt.Errorf("failed to initialize %s: %w", name, err)
|
|
}
|
|
|
|
screen := &Screen{
|
|
Name: name,
|
|
Generator: generator,
|
|
stop: make(chan struct{}),
|
|
}
|
|
|
|
c.screens = append(c.screens, screen)
|
|
return nil
|
|
}
|
|
|
|
// Run starts the carousel
|
|
func (c *Carousel) Run() error {
|
|
if len(c.screens) == 0 {
|
|
return fmt.Errorf("no screens added to carousel")
|
|
}
|
|
|
|
// Start with first screen
|
|
if err := c.activateScreen(0); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If no rotation, just wait for context cancellation
|
|
if c.rotationInterval <= 0 {
|
|
<-c.ctx.Done()
|
|
return c.ctx.Err()
|
|
}
|
|
|
|
// Start rotation timer
|
|
rotationTicker := time.NewTicker(c.rotationInterval)
|
|
defer rotationTicker.Stop()
|
|
|
|
// Main loop
|
|
for {
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return c.ctx.Err()
|
|
|
|
case <-rotationTicker.C:
|
|
// Move to next screen
|
|
c.mu.Lock()
|
|
nextIndex := (c.currentIndex + 1) % len(c.screens)
|
|
c.mu.Unlock()
|
|
|
|
if err := c.activateScreen(nextIndex); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops the carousel
|
|
func (c *Carousel) Stop() {
|
|
// Cancel the context
|
|
c.cancel()
|
|
|
|
// Stop the current screen
|
|
c.mu.Lock()
|
|
if c.currentIndex >= 0 && c.currentIndex < len(c.screens) {
|
|
screen := c.screens[c.currentIndex]
|
|
if screen.ticker != nil {
|
|
screen.ticker.Stop()
|
|
}
|
|
// Close stop channel if it's still open
|
|
select {
|
|
case <-screen.stop:
|
|
// Already closed
|
|
default:
|
|
close(screen.stop)
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
// Wait for all goroutines
|
|
c.wg.Wait()
|
|
|
|
// Close the display
|
|
_ = c.display.Close()
|
|
}
|
|
|
|
// activateScreen switches to the specified screen
|
|
func (c *Carousel) activateScreen(index int) error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Stop current screen if any
|
|
if c.currentIndex >= 0 && c.currentIndex < len(c.screens) {
|
|
close(c.screens[c.currentIndex].stop)
|
|
if c.screens[c.currentIndex].ticker != nil {
|
|
c.screens[c.currentIndex].ticker.Stop()
|
|
}
|
|
}
|
|
|
|
// Wait for current screen to stop
|
|
c.wg.Wait()
|
|
|
|
// Start new screen
|
|
c.currentIndex = index
|
|
screen := c.screens[index]
|
|
|
|
// Calculate frame interval
|
|
fps := screen.Generator.FramesPerSecond()
|
|
if fps <= 0 {
|
|
fps = 1 // Default to 1 FPS if invalid
|
|
}
|
|
frameInterval := time.Duration(float64(time.Second) / fps)
|
|
|
|
// Create new stop channel and ticker
|
|
screen.stop = make(chan struct{})
|
|
screen.ticker = time.NewTicker(frameInterval)
|
|
|
|
// Start frame generation goroutine
|
|
c.wg.Add(1)
|
|
go c.runScreen(screen)
|
|
|
|
return nil
|
|
}
|
|
|
|
// runScreen runs a single screen's frame generation loop
|
|
func (c *Carousel) runScreen(screen *Screen) {
|
|
defer c.wg.Done()
|
|
|
|
// Create grid with calculated dimensions
|
|
grid := NewCharGrid(c.gridWidth, c.gridHeight)
|
|
grid.FontSize = c.fontSize
|
|
|
|
// Set the character dimensions for proper rendering
|
|
charWidth, charHeight, _ := CalculateCharDimensions(
|
|
font.FamilyIBMPlexMono,
|
|
c.fontSize,
|
|
)
|
|
grid.CharWidth = charWidth
|
|
grid.CharHeight = charHeight
|
|
|
|
// Generate first frame immediately
|
|
if err := screen.Generator.GenerateFrame(grid); err == nil {
|
|
_ = c.display.Write(grid)
|
|
}
|
|
|
|
// Frame generation loop
|
|
for {
|
|
select {
|
|
case <-screen.stop:
|
|
return
|
|
|
|
case <-screen.ticker.C:
|
|
if err := screen.Generator.GenerateFrame(grid); err == nil {
|
|
_ = c.display.Write(grid)
|
|
}
|
|
}
|
|
}
|
|
}
|