Skip to content

Commit

Permalink
Merge pull request #43 from richbl/dev
Browse files Browse the repository at this point in the history
refactor(APP): ♻️ Refactor startAppControllers(); general clea…
  • Loading branch information
richbl authored Dec 23, 2024
2 parents d83579d + 98aab89 commit 8a2c082
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 91 deletions.
173 changes: 87 additions & 86 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,34 @@ type shutdownHandler struct {
restoreTerm func()
}

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

func main() {
log.Println("----- ----- Starting BLE Sync Cycle 0.6.2")

// Load configuration
cfg, err := config.LoadFile("config.toml")
if err != nil {
log.Println(logger.Red + "[FTL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
log.Println("----- ----- BLE Sync Cycle 0.6.2 shutdown complete. Goodbye!")
os.Exit(0)
}
// 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 shutdown coordination
// Initialize shutdown services: signal handling, context cancellation and term configuration
var wg sync.WaitGroup
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()

sh := &shutdownHandler{
done: make(chan struct{}),
componentsDown: make(chan struct{}),
wg: &wg,
rootCancel: rootCancel,
restoreTerm: configureTerminal(),
}
defer rootCancel()

// Set up shutdown handlers
setupSignalHandling(sh)
Expand All @@ -76,33 +79,46 @@ func main() {

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

<-sh.done
}

// loadConfig loads the TOML configuration file
func loadConfig(file string) *config.Config {
cfg, err := config.LoadFile(file)
if err != nil {
log.Println(logger.Red + "[FTL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
waveGoodbye()
os.Exit(0)
}

return cfg
}

// cleanup handles graceful shutdown of all components
func (sh *shutdownHandler) cleanup() {

sh.cleanupOnce.Do(func() {

// Signal components to shut down and wait for them to finish
sh.rootCancel()
sh.wg.Wait()
close(sh.componentsDown)

// Perform final cleanup
sh.restoreTerm()

log.Println("----- ----- BLE Sync Cycle 0.6.2 shutdown complete. Goodbye!")
waveGoodbye()
close(sh.done)
})
}

// waveGoodbye prints a goodbye message to the log
func waveGoodbye() {
log.Println(appPrefix, appName, appVersion, "shutdown complete. Goodbye!")
}

// setupSignalHandling configures OS signal handling
func setupSignalHandling(sh *shutdownHandler) {
sigChan := make(chan os.Signal, 1)
Expand Down Expand Up @@ -153,104 +169,54 @@ func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentTyp

// startAppControllers is responsible for starting and managing the component controllers
func startAppControllers(ctx context.Context, controllers appControllers, wg *sync.WaitGroup) (logger.ComponentType, error) {
// componentErr holds the error type and component type used for logging
type componentErr struct {
componentType logger.ComponentType
err error
}

// Create shutdown signal
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
// Create shutdown signal context.
ctxWithCancel, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()

// Scan for BLE peripheral of interest
bleSpeedCharacter, err := scanForBLESpeedCharacteristic(ctx, controllers)
bleSpeedCharacter, err := runBLEScan(ctxWithCancel, controllers)
if err != nil {

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

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

// Start component controllers concurrently
errs := make(chan componentErr, 1)
errChan := make(chan componentErr, 2) // Buffered channel for component errors.

// Add the goroutines to WaitGroup before starting them
wg.Add(2)
// Monitor BLE speed (goroutine)
go func() {
defer wg.Done()

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

// Check if the context was cancelled (user pressed Ctrl+C)
if ctx.Err() == context.Canceled {
errs <- componentErr{logger.BLE, nil}
return
}

errs <- componentErr{logger.BLE, err}
return
}

errs <- componentErr{logger.BLE, nil}
}()

// Play video (goroutine)
go func() {
defer wg.Done()

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

// Check if the context was cancelled (user pressed Ctrl+C)
if ctx.Err() == context.Canceled {
errs <- componentErr{logger.VIDEO, nil}
return
}
startBLEMonitoring(ctxWithCancel, controllers, wg, bleSpeedCharacter, errChan)
startVideoPlaying(ctxWithCancel, controllers, wg, errChan)

errs <- componentErr{logger.VIDEO, err}
return
}

errs <- componentErr{logger.VIDEO, nil}
}()

// Wait for both component results
// Wait for component results or cancellation.
for i := 0; i < 2; i++ {
compErr := <-errs

if compErr.err != nil {
return compErr.componentType, compErr.err
select {
case compErr := <-errChan:
if compErr.err != nil {
return compErr.componentType, compErr.err
}
case <-ctxWithCancel.Done():
return logger.APP, nil // Context cancelled, no error.
}

}

return logger.APP, nil
}

// scanForBLESpeedCharacteristic scans for the BLE CSC speed characteristic
func scanForBLESpeedCharacteristic(ctx context.Context, controllers appControllers) (*bluetooth.DeviceCharacteristic, error) {
// create a channel to receive the characteristic
// 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)

// Scan for the BLE CSC speed characteristic
go func() {
characteristic, err := controllers.bleController.GetBLECharacteristic(ctx, controllers.speedController)

if err != nil {
errChan <- err
return
}

// Return the characteristic
results <- characteristic
}()

// Wait for the characteristic or an error
select {
case <-ctx.Done():
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE characteristic scan...")
Expand All @@ -260,15 +226,50 @@ func scanForBLESpeedCharacteristic(ctx context.Context, controllers appControlle
case characteristic := <-results:
return characteristic, nil
}
}

// 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}
}
return
}
errChan <- componentErr{componentType: logger.BLE, err: nil}
}()
}

// startVideoPlaying starts the video playing goroutine.
func startVideoPlaying(ctx context.Context, controllers appControllers, wg *sync.WaitGroup, errChan chan<- componentErr) {
go func() {
defer wg.Done()
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}
}()
}

// monitorBLESpeed monitors the BLE speed characteristic
// 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)
}

// playVideo starts the video player
// playVideo starts the video player.
func playVideo(ctx context.Context, controllers appControllers) error {
return controllers.videoPlayer.Start(ctx, controllers.speedController)
}

// componentErr holds the error type and component type used for logging
type componentErr struct {
componentType logger.ComponentType
err error
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,5 @@ require (
require (
github.com/gen2brain/go-mpv v0.2.3
github.com/stretchr/testify v1.10.0
golang.org/x/term v0.27.0
tinygo.org/x/bluetooth v0.10.0
)
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N0
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down

0 comments on commit 8a2c082

Please sign in to comment.