hdmistat/internal/fbdraw/carousel.go
2025-07-24 14:32:50 +02:00

163 lines
3.3 KiB
Go

package fbdraw
import (
"context"
"fmt"
"sync"
"time"
)
// 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
mu sync.Mutex
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// NewCarousel creates a new carousel
func NewCarousel(display FramebufferDisplay, rotationInterval time.Duration) *Carousel {
ctx, cancel := context.WithCancel(context.Background())
return &Carousel{
display: display,
screens: make([]*Screen, 0),
currentIndex: 0,
rotationInterval: rotationInterval,
ctx: ctx,
cancel: cancel,
}
}
// AddScreen adds a new screen to the carousel
func (c *Carousel) AddScreen(name string, generator FrameGenerator) {
c.mu.Lock()
defer c.mu.Unlock()
screen := &Screen{
Name: name,
Generator: generator,
stop: make(chan struct{}),
}
c.screens = append(c.screens, screen)
}
// Run starts the carousel
func (c *Carousel) Run() error {
if len(c.screens) == 0 {
return fmt.Errorf("no screens added to carousel")
}
// Start rotation timer
rotationTicker := time.NewTicker(c.rotationInterval)
defer rotationTicker.Stop()
// Start with first screen
if err := c.activateScreen(0); err != nil {
return err
}
// 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() {
c.cancel()
c.wg.Wait()
}
// 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()
// Get display size
width, height := c.display.Size()
grid := NewCharGrid(width, height)
// 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)
}
}
}
}