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) } } } }