Skip to content
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
13 changes: 10 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"
"errors"
"fmt"

"github.com/richbl/go-ble-sync-cycle/internal/flags"
Expand Down Expand Up @@ -29,14 +31,14 @@ func main() {

// Check for application mode (CLI or GUI)
if !flags.IsCLIMode() {
logger.Info(logger.BackgroundCtx, logger.APP, "now running in GUI mode...")
logger.Debug(logger.BackgroundCtx, logger.APP, "now running in GUI mode...")
ui.StartGUI()

return
}

// Continue running in CLI mode
logger.Info(logger.BackgroundCtx, logger.APP, "running in CLI mode")
logger.Debug(logger.BackgroundCtx, logger.APP, "running in CLI mode")

// Create session manager
sessionMgr := session.NewManager()
Expand All @@ -48,7 +50,12 @@ func main() {

// Start the session (initializes controllers, connects BLE, starts services)
if err := sessionMgr.StartSession(); err != nil {
logger.Fatal(logger.BackgroundCtx, logger.APP, err)

if errors.Is(err, context.Canceled) {
logger.Info(logger.BackgroundCtx, logger.APP, "application exiting due to user cancellation")
} else {
logger.Fatal(logger.BackgroundCtx, logger.APP, err)
}
}

// Wait patiently for shutdown (Ctrl+C or services error)
Expand Down
69 changes: 46 additions & 23 deletions internal/ble/sensor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -80,23 +79,23 @@ const (
)

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

AdapterMu.Lock()
defer AdapterMu.Unlock()

// Increment instance counter
instanceID := bleInstanceCounter.Add(1)

logger.Debug(logger.BackgroundCtx, logger.BLE, fmt.Sprintf("creating BLE controller object (id:%04d)...", instanceID))
logger.Debug(ctx, logger.BLE, fmt.Sprintf("creating BLE controller object (id:%04d)...", instanceID))

bleAdapter := bluetooth.DefaultAdapter

if err := bleAdapter.Enable(); err != nil {
return nil, fmt.Errorf(errFormat, "failed to enable BLE controller", err)
}

logger.Info(logger.BackgroundCtx, logger.BLE, fmt.Sprintf("created BLE controller object (id:%04d)", instanceID))
logger.Debug(ctx, logger.BLE, fmt.Sprintf("created BLE controller object (id:%04d)", instanceID))

return &Controller{
blePeripheralDetails: blePeripheralDetails{
Expand Down Expand Up @@ -161,23 +160,34 @@ func performBLEAction[T any](ctx context.Context, m *Controller, params actionPa
scanCtx, cancel := context.WithTimeout(ctx, time.Duration(m.blePeripheralDetails.bleConfig.ScanTimeoutSecs)*time.Second)
defer cancel()
found := make(chan T, 1)
done := make(chan struct{})
errChan := make(chan error, 1)

// Start the BLE discovery action
go func() {
defer close(done)
logger.Debug(scanCtx, logger.BLE, params.logMessage)
params.action(scanCtx, found, errChan)
}()

select {

case result := <-found:
return result, nil

case err := <-errChan:
var zero T

return zero, err

case <-scanCtx.Done():
var zero T
err := handleActionTimeout(scanCtx, m, params.stopAction)
logger.Debug(ctx, logger.BLE, "waiting for BLE peripheral disconnect...")

<-done // Wait for the action to complete

logger.Debug(ctx, logger.BLE, "BLE peripheral device disconnected")

return zero, err
}
Expand All @@ -190,7 +200,6 @@ func handleActionTimeout(ctx context.Context, m *Controller, stopAction func() e
if stopAction != nil {

if err := stopAction(); err != nil {
fmt.Fprint(os.Stdout, "\r") // Clear the ^C character from the terminal line
logger.Error(ctx, logger.BLE, fmt.Sprintf("failed to stop action: %v", err))
}

Expand All @@ -201,8 +210,6 @@ func handleActionTimeout(ctx context.Context, m *Controller, stopAction func() e
return fmt.Errorf("%w (%ds)", ErrScanTimeout, m.blePeripheralDetails.bleConfig.ScanTimeoutSecs)
}

fmt.Fprint(os.Stdout, "\r") // Clear the ^C character from the terminal line

return fmt.Errorf(errFormat, "user interrupt detected", ctx.Err())
}

Expand All @@ -217,7 +224,14 @@ func (m *Controller) scanAction(ctx context.Context, found chan<- bluetooth.Scan
return
}

found <- <-foundChan
// Wait for the scan to complete
select {

case result := <-foundChan:
found <- result

default:
}

}

Expand All @@ -235,37 +249,46 @@ func (m *Controller) connectAction(device bluetooth.ScanResult, found chan<- blu

}

// startScanning starts the BLE peripheral scan
// startScanning starts the BLE peripheral scan and handles device discovery
func (m *Controller) startScanning(ctx context.Context, found chan<- bluetooth.ScanResult) error {

// Check if already canceled before starting scanning operation
if err := ctx.Err(); err != nil {
return fmt.Errorf("session stop requested before BLE scan: %w", err)
}

AdapterMu.Lock()
defer AdapterMu.Unlock()

err := m.blePeripheralDetails.bleAdapter.Scan(func(_ *bluetooth.Adapter, result bluetooth.ScanResult) {
// Use an atomic flag to ensure we only trigger the device discovery logic once
var foundOnce atomic.Bool

err := m.blePeripheralDetails.bleAdapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) {

select {
case <-ctx.Done():
logger.Debug(ctx, logger.BLE, "scan canceled via context")
// Check if context canceled before continuing scanning operation
if ctx.Err() != nil {
_ = adapter.StopScan()

return
default:
}

// Address comparison
if result.Address.String() == m.blePeripheralDetails.bleConfig.SensorBDAddr {

if stopErr := m.blePeripheralDetails.bleAdapter.StopScan(); stopErr != nil {
logger.Error(ctx, logger.BLE, fmt.Sprintf("failed to stop scan: %v", stopErr))
}
if foundOnce.CompareAndSwap(false, true) {
logger.Debug(ctx, logger.BLE, "BLE peripheral found; stopping scan...")
_ = adapter.StopScan()

select {
case found <- result:
logger.Debug(ctx, logger.BLE, "scan result sent to controller")
default:
logger.Warn(ctx, logger.BLE, "controller object no longer listening; scan results ignored")
}

select {
case found <- result:
default:
logger.Warn(ctx, logger.BLE, "scan results channel full... try restarting the BLE device")
}

return
}

})

if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/ble/sensor_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func createTestController(speedUnits string) (*Controller, error) {
SpeedUnits: speedUnits,
}

return NewBLEController(bleConfig, speedConfig)
return NewBLEController(logger.BackgroundCtx, bleConfig, speedConfig)
}

// waitForScanReset waits for the scan to reset
Expand Down
6 changes: 3 additions & 3 deletions internal/ble/sensor_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (m *Controller) BatteryLevel(ctx context.Context, services []Characteristic
}

m.blePeripheralDetails.batteryLevel = batteryLevel
logger.Info(ctx, logger.BLE, "found battery characteristic UUID="+m.blePeripheralDetails.batteryCharacteristic.UUID().String())
logger.Debug(ctx, logger.BLE, "found battery characteristic UUID="+m.blePeripheralDetails.batteryCharacteristic.UUID().String())
logger.Info(ctx, logger.BLE, fmt.Sprintf("BLE sensor battery level: %d%%", m.blePeripheralDetails.batteryLevel))

return nil
Expand All @@ -253,7 +253,7 @@ func (m *Controller) CSCServices(ctx context.Context, device ServiceDiscoverer)
return nil, fmt.Errorf(errFormat, ErrCSCServiceDiscovery, err)
}

logger.Info(ctx, logger.BLE, "found CSC service UUID="+cscServiceConfig.serviceUUID.String())
logger.Debug(ctx, logger.BLE, "found CSC service UUID="+cscServiceConfig.serviceUUID.String())

return result, nil
}
Expand Down Expand Up @@ -282,7 +282,7 @@ func (m *Controller) CSCCharacteristics(ctx context.Context, services []Characte
return fmt.Errorf(errFormat, ErrCSCCharDiscovery, err)
}

logger.Info(ctx, logger.BLE, "found CSC characteristic UUID="+cscServiceConfig.characteristicUUID.String())
logger.Debug(ctx, logger.BLE, "found CSC characteristic UUID="+cscServiceConfig.characteristicUUID.String())

return nil
}
2 changes: 1 addition & 1 deletion internal/ble/sensor_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func createTestBLEController(t *testing.T) *Controller {

t.Helper()

controller, err := NewBLEController(
controller, err := NewBLEController(logger.BackgroundCtx,
config.BLEConfig{ScanTimeoutSecs: 10},
config.SpeedConfig{},
)
Expand Down
7 changes: 3 additions & 4 deletions internal/ble/sensor_updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/binary"
"fmt"
"math"
"os"

"github.com/richbl/go-ble-sync-cycle/internal/config"
"github.com/richbl/go-ble-sync-cycle/internal/logger"
Expand Down Expand Up @@ -50,7 +49,7 @@ func initSpeedData(wheelCircumferenceMM int, speedUnitMultiplier float64) *speed
// BLEUpdates starts the real-time monitoring of BLE sensor notifications
func (m *Controller) BLEUpdates(ctx context.Context, speedController *speed.Controller) error {

logger.Info(ctx, logger.BLE, "starting the monitoring for BLE sensor notifications...")
logger.Debug(ctx, logger.BLE, "starting the monitoring for BLE sensor notifications...")

errChan := make(chan error, 1)

Expand Down Expand Up @@ -78,8 +77,8 @@ func (m *Controller) BLEUpdates(ctx context.Context, speedController *speed.Cont
// Manage context cancellation
go func() {
<-ctx.Done()
fmt.Fprint(os.Stdout, "\r") // Clear the ^C character from the terminal line
logger.Info(ctx, logger.BLE, "interrupt detected, stopping the monitoring for BLE sensor notifications...")

logger.Debug(ctx, logger.BLE, "interrupt detected, stopping the monitoring for BLE sensor notifications...")

// Disable real-time notifications from BLE sensor
if err := m.blePeripheralDetails.bleCharacteristic.EnableNotifications(nil); err != nil {
Expand Down
15 changes: 11 additions & 4 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,11 @@ func AddWriter(w io.Writer) {
}

// SetLogLevel dynamically updates the logging level of the running application
func SetLogLevel(levelStr string) {
func SetLogLevel(ctx context.Context, levelStr string) {

newLevel := parseLogLevel(levelStr)
logLevelVar.Set(newLevel)

Debug(BackgroundCtx, APP, "logging level changed to "+newLevel.String())

}

// LogLevel returns the current logging level
Expand Down Expand Up @@ -368,9 +366,18 @@ func (h *CustomTextHandler) appendAttrs(buf *bytes.Buffer, attrs []slog.Attr) er
return nil
}

// UseGUIWriterOnly replaces all writers with only the GUI writer (used in GUI mode).
// UseGUIWriterOnly replaces all writers with only the GUI writer (used in GUI mode)
func UseGUIWriterOnly(w io.Writer) {
logOutput.mu.Lock()
defer logOutput.mu.Unlock()
logOutput.writers = []io.Writer{w}
}

// SetOutputToStdout resets the logger output to the standard output (terminal)
func SetOutputToStdout() {

logOutput.mu.Lock()
defer logOutput.mu.Unlock()
logOutput.writers = []io.Writer{os.Stdout}

}
14 changes: 12 additions & 2 deletions internal/services/shutdown_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ var shutdownInstanceCounter atomic.Int64
func NewShutdownManager(timeout time.Duration) *ShutdownManager {

instanceID := shutdownInstanceCounter.Add(1)

logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("creating shutdown manager object (id:%04d)...", instanceID))

// Create a context with a timeout
ctx, cancel := context.WithCancel(logger.BackgroundCtx)

logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("created shutdown manager object (id:%04d)", instanceID))

return &ShutdownManager{
Expand Down Expand Up @@ -90,8 +88,11 @@ func (sm *ShutdownManager) Start() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

// Wait for a shutdown signal
go func() {
<-sigChan
logger.ClearCLILine()
logger.Info(logger.BackgroundCtx, logger.APP, "shutdown request detected, shutting down now...")
sm.Shutdown()
}()

Expand All @@ -111,6 +112,7 @@ func (sm *ShutdownManager) Shutdown() {
}()

select {

case <-done:
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("shutdown manager (id:%04d) services stopped", sm.InstanceID))

Expand All @@ -137,8 +139,10 @@ func (sm *ShutdownManager) Context() *context.Context {
func (sm *ShutdownManager) Wait() {

select {

case <-sm.context.ctx.Done():
sm.Shutdown()

case err := <-sm.errChan:
if err != nil {
logger.Error(sm.context.ctx, logger.APP, fmt.Sprintf("service error: %v", err))
Expand All @@ -157,8 +161,14 @@ func WaveHello(ctx context.Context) {
// WaveGoodbye outputs a goodbye message and exits the program
func WaveGoodbye(ctx context.Context) {

// Redirect logging to the console, clear the CLI line, and set the log level so this final
// shutdown message is visible regardless of application mode (CLI or GUI)
logger.SetOutputToStdout()
logger.ClearCLILine()
logger.SetLogLevel(ctx, "debug")

logger.Info(ctx, logger.APP, config.GetFullVersion()+" shutdown complete. Goodbye")

os.Exit(0)

}
Loading
Loading