Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(APP): added new service_runner in main package to better man… #45

Merged
merged 2 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 93 additions & 217 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import (
"log"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
"time"

ble "github.com/richbl/go-ble-sync-cycle/internal/ble"
config "github.com/richbl/go-ble-sync-cycle/internal/configuration"
Expand All @@ -19,160 +17,96 @@ import (
"tinygo.org/x/bluetooth"
)

// appControllers holds the main application controllers
// Application constants
const (
appPrefix = "----- -----"
appName = "BLE Sync Cycle"
appVersion = "0.6.2"
shutdownTimeout = 30 * time.Second
)

// appControllers holds the application component controllers for managing speed, video playback,
// and BLE communication
type appControllers struct {
speedController *speed.SpeedController
videoPlayer *video.PlaybackController
bleController *ble.BLEController
}

// shutdownHandler encapsulates shutdown coordination
type shutdownHandler struct {
done chan struct{}
componentsDown chan struct{}
cleanupOnce sync.Once
wg *sync.WaitGroup
rootCancel context.CancelFunc
resetTerminal func()
}

// componentErr holds the error type and component type used for logging
type componentErr struct {
componentType logger.ComponentType
err error
}

const (
appPrefix = "----- -----"
appName = "BLE Sync Cycle"
appVersion = "0.6.2"
)

func main() {
// Hello computer!
log.Println(appPrefix, "Starting", appName, appVersion)

// Load configuration from TOML
cfg := loadConfig("config.toml")

// Initialize logger package with display level from TOML configuration
logger.Initialize(cfg.App.LogLevel)
// Initialize the shutdown manager and exit handler
sm := NewShutdownManager(shutdownTimeout)
exitHandler := NewExitHandler(sm)

// Initialize shutdown services: signal handling, context cancellation and terminal config
var wg sync.WaitGroup
rootCtx, rootCancel := context.WithCancel(context.Background())
sh := &shutdownHandler{
done: make(chan struct{}),
componentsDown: make(chan struct{}),
wg: &wg,
rootCancel: rootCancel,
resetTerminal: configTerminal(),
}
defer rootCancel()
// Add configureTerminal cleanup function to reset terminal settings on exit
sm.AddCleanupFn(configureTerminal())
sm.Start()

// Set up shutdown handlers
setupSignalHandling(sh)
logger.SetExitHandler(sh.cleanup)
// Initialize the logger with the configured log level and exit handler
logger.Initialize(cfg.App.LogLevel)
logger.SetExitHandler(func() {
sm.initiateShutdown()
exitHandler.HandleExit()
})

// Create component controllers
// Initialize the application controllers
controllers, componentType, err := setupAppControllers(*cfg)
if err != nil {
logger.Fatal(componentType, "failed to create controllers: "+err.Error())
<-sh.done
waveGoodbye()
}

// Start components
if componentType, err := startAppControllers(rootCtx, controllers, sh.wg); err != nil {
logger.Fatal(componentType, err.Error())
<-sh.done
waveGoodbye()
return
}

<-sh.done
}

// loadConfig loads the TOML configuration file
func loadConfig(file string) *config.Config {
cfg, err := config.LoadFile(file)
// Scan for the BLE characteristic and handle context cancellation
bleChar, err := scanForBLECharacteristic(sm.Context(), controllers)
if err != nil {
log.Println(logger.Red + "[FTL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
waveGoodbye()
}

return cfg
}

// cleanup handles graceful shutdown of all components
func (sh *shutdownHandler) cleanup() {
if err != context.Canceled {
logger.Fatal(logger.BLE, "failed to scan for BLE characteristic: "+err.Error())
return
}

sh.cleanupOnce.Do(func() {
// Signal components to shut down and wait for them to finish
sh.rootCancel()
sh.wg.Wait()
close(sh.componentsDown)
exitHandler.HandleExit()
return
}

// Perform final cleanup
sh.resetTerminal()
close(sh.done)
waveGoodbye()
// Create and run the BLE service runner
bleRunner := NewServiceRunner(sm, "BLE")
bleRunner.Run(func(ctx context.Context) error {
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController, bleChar)
})

}

// waveGoodbye outputs a goodbye message and exits the application
func waveGoodbye() {
log.Println(appPrefix, appName, appVersion, "shutdown complete. Goodbye!")
os.Exit(0)
}

// setupSignalHandling configures OS signal handling
func setupSignalHandling(sh *shutdownHandler) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

go func() {
<-sigChan
sh.cleanup()
}()

}

// configTerminal handles terminal char echo to prevent display of break (^C) character
func configTerminal() func() {
// Disable control character echo using stty
rawMode := exec.Command("stty", "-echo")
rawMode.Stdin = os.Stdin

if err := rawMode.Run(); err != nil {
logger.Fatal(logger.APP, "failed to configure terminal: "+err.Error())
waveGoodbye()
}

// Return cleanup function
return func() {
cooked := exec.Command("stty", "echo")
cooked.Stdin = os.Stdin
// Create and run the video service runner
videoRunner := NewServiceRunner(sm, "Video")
videoRunner.Run(func(ctx context.Context) error {
return controllers.videoPlayer.Start(ctx, controllers.speedController)
})

if err := cooked.Run(); err != nil {
logger.Fatal(logger.APP, "failed to restore terminal: "+err.Error())
waveGoodbye()
// Wait for services to complete and check for errors
for _, runner := range []*ServiceRunner{bleRunner, videoRunner} {
if err := runner.Error(); err != nil {
logger.Fatal(logger.APP, "service error: "+err.Error())
return
}

}

// Wait for final shutdown sequences to complete and wave goodbye!
sm.Wait()
waveGoodbye()
}

// setupAppControllers creates and initializes the application controllers
// setupAppControllers creates and initializes all application controllers
func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentType, error) {
// Create speed and video controllers

speedController := speed.NewSpeedController(cfg.Speed.SmoothingWindow)
videoPlayer, err := video.NewPlaybackController(cfg.Video, cfg.Speed)
if err != nil {
return appControllers{}, logger.VIDEO, errors.New("failed to create video player: " + err.Error())
}

// Create BLE controller
bleController, err := ble.NewBLEController(cfg.BLE, cfg.Speed)
if err != nil {
return appControllers{}, logger.BLE, errors.New("failed to create BLE controller: " + err.Error())
Expand All @@ -185,121 +119,63 @@ func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentTyp
}, logger.APP, nil
}

// startAppControllers is responsible for starting and managing the component controllers
func startAppControllers(ctx context.Context, controllers appControllers, wg *sync.WaitGroup) (logger.ComponentType, error) {
// Create shutdown signal context.
ctxWithCancel, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()

// Scan to find the speed characteristic
bleSpeedCharacter, err := runBLEScan(ctxWithCancel, controllers)
if err != nil {

// Check if the context was cancelled (user pressed Ctrl+C)
if errors.Is(err, context.Canceled) {
return logger.APP, nil
}

return logger.BLE, errors.New("BLE peripheral scan failed: " + err.Error())
}

errChan := make(chan componentErr, 2) // Buffered channel for component errors
wg.Add(2)

// Start BLE monitoring and video playback
startBLEMonitoring(ctxWithCancel, controllers, wg, bleSpeedCharacter, errChan)
startVideoPlaying(ctxWithCancel, controllers, wg, errChan)

// Wait for component results or cancellation
for i := 0; i < 2; i++ {

select {
case compErr := <-errChan:
// scanForBLECharacteristic handles the initial BLE device discovery and characteristic scanning
// using a context for cancellation and returns the discovered characteristic or an error
func scanForBLECharacteristic(ctx context.Context, controllers appControllers) (*bluetooth.DeviceCharacteristic, error) {

if compErr.err != nil {
return compErr.componentType, compErr.err
}

// Context cancelled, no error
case <-ctxWithCancel.Done():
return logger.APP, nil
}

}

return logger.APP, nil
}

// runBLEScan scans for the BLE speed characteristic.
func runBLEScan(ctx context.Context, controllers appControllers) (*bluetooth.DeviceCharacteristic, error) {
results := make(chan *bluetooth.DeviceCharacteristic, 1)
errChan := make(chan error, 1)
// Create a channel to receive the result of the BLE characteristic scan
resultsChan := make(chan struct {
char *bluetooth.DeviceCharacteristic
err error
}, 1)

go func() {
characteristic, err := controllers.bleController.GetBLECharacteristic(ctx, controllers.speedController)
if err != nil {
errChan <- err
return
}

results <- characteristic
defer close(resultsChan)
char, err := controllers.bleController.GetBLECharacteristic(ctx, controllers.speedController)
resultsChan <- struct {
char *bluetooth.DeviceCharacteristic
err error
}{char, err}
}()

select {
case <-ctx.Done():
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE characteristic scan...")
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE discovery...")
return nil, ctx.Err()
case err := <-errChan:
return nil, err
case characteristic := <-results:
return characteristic, nil
case result := <-resultsChan:
return result.char, result.err
}
}

// startBLEMonitoring starts the BLE monitoring goroutine
func startBLEMonitoring(ctx context.Context, controllers appControllers, wg *sync.WaitGroup, bleSpeedCharacter *bluetooth.DeviceCharacteristic, errChan chan<- componentErr) {
go func() {
defer wg.Done()

if err := monitorBLESpeed(ctx, controllers, bleSpeedCharacter); err != nil {

// Only send error if context was not cancelled
if !errors.Is(err, context.Canceled) {
errChan <- componentErr{componentType: logger.BLE, err: err}
}
// loadConfig loads and validates the TOML configuration file
func loadConfig(file string) *config.Config {

return
}
cfg, err := config.LoadFile(file)
if err != nil {
log.Println(logger.Red + "[FTL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
waveGoodbye()
}

errChan <- componentErr{componentType: logger.BLE, err: nil}
}()
return cfg
}

// startVideoPlaying starts the video playing goroutine.
func startVideoPlaying(ctx context.Context, controllers appControllers, wg *sync.WaitGroup, errChan chan<- componentErr) {
go func() {
defer wg.Done()
// configureTerminal handles terminal character echo settings, returning a cleanup function
// to restore original terminal settings
func configureTerminal() func() {

if err := playVideo(ctx, controllers); err != nil {

// Only send error if context was not cancelled
if !errors.Is(err, context.Canceled) {
errChan <- componentErr{componentType: logger.VIDEO, err: err}
}

return
}

errChan <- componentErr{componentType: logger.VIDEO, err: nil}
}()
}
rawMode := exec.Command("stty", "-echo")
rawMode.Stdin = os.Stdin
_ = rawMode.Run()

// monitorBLESpeed monitors the BLE speed characteristic
func monitorBLESpeed(ctx context.Context, controllers appControllers, bleSpeedCharacter *bluetooth.DeviceCharacteristic) error {
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController, bleSpeedCharacter)
return func() {
cooked := exec.Command("stty", "echo")
cooked.Stdin = os.Stdin
_ = cooked.Run()
}
}

// playVideo starts the video player.
func playVideo(ctx context.Context, controllers appControllers) error {
return controllers.videoPlayer.Start(ctx, controllers.speedController)
// waveGoodbye outputs a goodbye message and exits the program
func waveGoodbye() {
log.Println(appPrefix, appName, appVersion, "shutdown complete. Goodbye!")
os.Exit(0)
}
Loading
Loading