Skip to content

Commit

Permalink
Merge pull request #40 from richbl/dev
Browse files Browse the repository at this point in the history
fix(app): 🐛 Bug fixes to resolve command line output/formatting
  • Loading branch information
richbl authored Dec 21, 2024
2 parents 34b7db0 + a24103c commit 39a232d
Show file tree
Hide file tree
Showing 14 changed files with 178 additions and 50 deletions.
83 changes: 68 additions & 15 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"errors"
"log"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"

ble "github.com/richbl/go-ble-sync-cycle/internal/ble"
Expand All @@ -30,12 +32,19 @@ func main() {
// Load configuration
cfg, err := config.LoadFile("config.toml")
if err != nil {
log.Fatal(logger.Magenta+"[FATAL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
log.Fatal(logger.Magenta + "[FATAL]" + logger.Reset + " [APP] failed to load TOML configuration: " + err.Error())
}

// Initialize logger
logger.Initialize(cfg.App.LogLevel)

// Configure terminal output to prevent display of break (^C) character
restoreTerm := configureTerminal()
defer restoreTerm()

// Ensure goodbye message is always output last
defer logger.Info(logger.APP, "BLE Sync Cycle 0.6.2 shutdown complete. Goodbye!")

// Create contexts for managing goroutines and cancellations
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
Expand All @@ -46,18 +55,33 @@ func main() {
logger.Fatal(componentType, "failed to create controllers: "+err.Error())
}

// Run the application
if componentType, err := startAppControllers(rootCtx, controllers); err != nil {
// Create a WaitGroup to track goroutine lifetimes, and run the application controllers
var wg sync.WaitGroup

if componentType, err := startAppControllers(rootCtx, controllers, &wg); err != nil {
logger.Error(componentType, err.Error())
}

// Shutdown the application... buh bye!
log.Println("BLE Sync Cycle 0.6.2 shutdown complete. Goodbye!")
wg.Wait() // Wait here for all goroutines to finish in main()... be patient
}

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

// Return cleanup function
return func() {
cooked := exec.Command("stty", "echo")
cooked.Stdin = os.Stdin
_ = cooked.Run()
}
}

// setupAppControllers creates and initializes the 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)
Expand All @@ -79,12 +103,11 @@ 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) (logger.ComponentType, error) {

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
err error
}

// Create shutdown signal
Expand All @@ -94,53 +117,82 @@ func startAppControllers(ctx context.Context, controllers appControllers) (logge
// Scan for BLE peripheral of interest
bleSpeedCharacter, err := scanForBLESpeedCharacteristic(ctx, controllers)
if err != nil {

// Check if the context was cancelled (user pressed Ctrl+C)
if ctx.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)

// Add two goroutines to the WaitGroup
wg.Add(2) // One for BLE monitoring, one for video playback

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

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

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

// Wait for context cancellation or error
select {
case <-ctx.Done():
return logger.APP, ctx.Err()
case compErr := <-errs:
// Wait for both component results
for i := 0; i < 2; i++ {
compErr := <-errs

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

}

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
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
Expand All @@ -159,6 +211,7 @@ func scanForBLESpeedCharacteristic(ctx context.Context, controllers appControlle
case characteristic := <-results:
return characteristic, nil
}

}

// monitorBLESpeed monitors the BLE speed characteristic
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ require (
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

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: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
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
41 changes: 30 additions & 11 deletions internal/ble/sensor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ var (

// NewBLEController creates a new BLE central controller for accessing a BLE peripheral
func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig) (*BLEController, error) {

// Enable BLE adapter
bleAdapter := bluetooth.DefaultAdapter

if err := bleAdapter.Enable(); err != nil {
return nil, err
}
Expand All @@ -62,7 +62,6 @@ func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig

// ScanForBLEPeripheral scans for a BLE peripheral with the specified UUID
func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.ScanResult, error) {

// Create context with timeout
scanCtx, cancel := context.WithTimeout(ctx, time.Duration(m.bleConfig.ScanTimeoutSecs)*time.Second)
defer cancel()
Expand All @@ -76,6 +75,7 @@ func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.Sca
if err := m.startScanning(found); err != nil {
errChan <- err
}

}()

// Wait for device discovery or timeout
Expand All @@ -86,20 +86,24 @@ func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.Sca
case err := <-errChan:
return bluetooth.ScanResult{}, err
case <-scanCtx.Done():

if err := m.bleAdapter.StopScan(); err != nil {
logger.Error(logger.BLE, "failed to stop scan: "+err.Error())
}

return bluetooth.ScanResult{}, errors.New("scanning time limit reached")
}

}

// startScanning starts the BLE scan and sends the result to the found channel when the target device is discovered
func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error {

// Start BLE scan
err := m.bleAdapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {

// Check if the target peripheral was found
if result.Address.String() == m.bleConfig.SensorUUID {

// Stop scanning
if err := m.bleAdapter.StopScan(); err != nil {
logger.Error(fmt.Sprintf(string(logger.BLE)+"failed to stop scan: %v", err))
Expand All @@ -108,7 +112,9 @@ func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error {
// Found the target peripheral
found <- result
}

})

if err != nil {
logger.Error(logger.BLE, "scan error: "+err.Error())
}
Expand All @@ -118,7 +124,6 @@ func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error {

// GetBLECharacteristic scans for the BLE peripheral and returns CSC services/characteristics
func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedController *speed.SpeedController) (*bluetooth.DeviceCharacteristic, error) {

// Scan for BLE peripheral
result, err := m.ScanForBLEPeripheral(ctx)
if err != nil {
Expand All @@ -129,6 +134,7 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle

// Connect to BLE peripheral device
var device bluetooth.Device

if device, err = m.bleAdapter.Connect(result.Address, bluetooth.ConnectionParams{}); err != nil {
return nil, err
}
Expand Down Expand Up @@ -158,24 +164,37 @@ func (m *BLEController) GetBLECharacteristic(ctx context.Context, speedControlle

// GetBLEUpdates enables BLE peripheral monitoring to report real-time sensor data
func (m *BLEController) GetBLEUpdates(ctx context.Context, speedController *speed.SpeedController, char *bluetooth.DeviceCharacteristic) error {

logger.Debug(logger.BLE, "starting real-time monitoring of BLE sensor notifications...")
errChan := make(chan error, 1)

// Subscribe to live BLE sensor notifications
// Enable notifications with cleanup handling
if err := char.EnableNotifications(func(buf []byte) {
speed := m.ProcessBLESpeed(buf)
speedController.UpdateSpeed(speed)
}); err != nil {
return err
}

<-ctx.Done()
return nil
// Ensure notifications are disabled on exit
defer func() {

if err := char.EnableNotifications(nil); err != nil {
logger.Error(logger.BLE, "failed to disable notifications: "+err.Error())
}

}()

// Handle context cancellation in separate goroutine
go func() {
<-ctx.Done()
errChan <- nil
}()

return <-errChan
}

// ProcessBLESpeed processes the raw speed data from the BLE peripheral
func (m *BLEController) ProcessBLESpeed(data []byte) float64 {

// Parse speed data
newSpeedData, err := m.parseSpeedData(data)
if err != nil {
Expand All @@ -192,7 +211,6 @@ func (m *BLEController) ProcessBLESpeed(data []byte) float64 {

// calculateSpeed calculates the current speed based on the sensor data
func (m *BLEController) calculateSpeed(sm SpeedMeasurement) float64 {

// First time through the loop set the last wheel revs and time
if lastWheelTime == 0 {
lastWheelRevs = sm.wheelRevs
Expand All @@ -202,6 +220,7 @@ func (m *BLEController) calculateSpeed(sm SpeedMeasurement) float64 {

// Calculate delta between time intervals
timeDiff := sm.wheelTime - lastWheelTime

if timeDiff == 0 {
return 0.0
}
Expand All @@ -211,6 +230,7 @@ func (m *BLEController) calculateSpeed(sm SpeedMeasurement) float64 {

// Determine speed unit conversion multiplier
speedConversion := kphConversion

if m.speedConfig.SpeedUnits == config.SpeedUnitsMPH {
speedConversion = mphConversion
}
Expand All @@ -225,7 +245,6 @@ func (m *BLEController) calculateSpeed(sm SpeedMeasurement) float64 {

// parseSpeedData parses the raw speed data from the BLE peripheral
func (m *BLEController) parseSpeedData(data []byte) (SpeedMeasurement, error) {

// Check for data
if len(data) < 1 {
return SpeedMeasurement{}, errors.New("empty data")
Expand Down
Loading

0 comments on commit 39a232d

Please sign in to comment.