Skip to content

Commit

Permalink
refactor(app): ♻️ Broke out ble_controller into logical groups (separ…
Browse files Browse the repository at this point in the history
…ation of concerns)

Signed-off-by: richbl <[email protected]>
  • Loading branch information
richbl committed Jan 6, 2025
1 parent 3aa86e7 commit e675c86
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 211 deletions.
290 changes: 79 additions & 211 deletions internal/ble/sensor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,38 @@ package ble

import (
"context"
"encoding/binary"
"fmt"
"strconv"
"time"

"tinygo.org/x/bluetooth"

config "github.com/richbl/go-ble-sync-cycle/internal/configuration"
logger "github.com/richbl/go-ble-sync-cycle/internal/logging"
speed "github.com/richbl/go-ble-sync-cycle/internal/speed"
)

// Constants for BLE data parsing and speed calculations
const (
minDataLength = 7
wheelRevFlag = uint8(0x01)
kphConversion = 3.6 // Conversion factor for kilometers per hour
mphConversion = 2.23694 // Conversion factor for miles per hour
)

// SpeedMeasurement represents the wheel revolution and time data from a BLE sensor
type SpeedMeasurement struct {
wheelRevs uint32
wheelTime uint16
}

// BLEDetails holds BLE peripheral details
type BLEDetails struct {
// bleDetails holds details about the BLE peripheral
type bleDetails struct {
bleConfig config.BLEConfig
bleAdapter bluetooth.Adapter
bleCharacteristic *bluetooth.DeviceCharacteristic
}

// BLEController holds the BLE controller component and sensor data
// BLEController is a central controller for managing the BLE peripheral
type BLEController struct {
bleDetails BLEDetails
bleDetails bleDetails
speedConfig config.SpeedConfig
lastWheelRevs uint32
lastWheelTime uint16
}

// actionParams encapsulates parameters for BLE actions
type actionParams struct {
ctx context.Context
action func(chan<- interface{}, chan<- error)
logMessage string
stopAction func() error
}

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

Expand All @@ -55,73 +46,26 @@ func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig
logger.Info(logger.BLE, "created new BLE central controller")

return &BLEController{
bleDetails: BLEDetails{
bleDetails: bleDetails{
bleConfig: bleConfig,
bleAdapter: *bleAdapter,
},
speedConfig: speedConfig,
}, nil
}

// performBLEAction performs the provided BLE setup action
func (m *BLEController) performBLEAction(ctx context.Context, action func(found chan<- interface{}, errChan chan<- error), logMessage string, stopAction func() error) (interface{}, error) {

// Create a context with a timeout for the scan
scanCtx, cancel := context.WithTimeout(ctx, time.Duration(m.bleDetails.bleConfig.ScanTimeoutSecs)*time.Second)
defer cancel()

found := make(chan interface{}, 1)
errChan := make(chan error, 1)

// Run the action in a goroutine and handle the results
go func() {
logger.Debug(logger.BLE, logMessage)
action(found, errChan)
}()

select {
case result := <-found:
return result, nil
case err := <-errChan:
return nil, err
case <-scanCtx.Done():

if stopAction != nil {

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

}

if scanCtx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("scanning time limit reached")
}

fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE device setup...")
return nil, scanCtx.Err()
}

}

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

// Pass anonymous function into performBLEAction to scan for BLE peripheral
result, err := m.performBLEAction(ctx, func(found chan<- interface{}, errChan chan<- error) {

foundChan := make(chan bluetooth.ScanResult, 1)

// Start scanning for BLE peripherals
if err := m.startScanning(foundChan); err != nil {
errChan <- err
return
}
params := actionParams{
ctx: ctx,
action: m.scanAction,
logMessage: fmt.Sprintf("scanning for BLE peripheral UUID %s", m.bleDetails.bleConfig.SensorUUID),
stopAction: m.bleDetails.bleAdapter.StopScan,
}

found <- <-foundChan
}, fmt.Sprintf("scanning for BLE peripheral UUID %s", m.bleDetails.bleConfig.SensorUUID), m.bleDetails.bleAdapter.StopScan)
// Perform the BLE scan
result, err := m.performBLEAction(params)
if err != nil {
return bluetooth.ScanResult{}, err
}
Expand All @@ -134,19 +78,14 @@ func (m *BLEController) ScanForBLEPeripheral(ctx context.Context) (bluetooth.Sca
// ConnectToBLEPeripheral connects to the specified BLE peripheral
func (m *BLEController) ConnectToBLEPeripheral(ctx context.Context, device bluetooth.ScanResult) (bluetooth.Device, error) {

// Pass anonymous function into performBLEAction to connect to BLE peripheral
result, err := m.performBLEAction(ctx, func(found chan<- interface{}, errChan chan<- error) {

// Connect to the BLE peripheral
dev, err := m.bleDetails.bleAdapter.Connect(device.Address, bluetooth.ConnectionParams{})

if err != nil {
errChan <- err
return
}
params := actionParams{
ctx: ctx,
action: func(found chan<- interface{}, errChan chan<- error) { m.connectAction(device, found, errChan) },
logMessage: fmt.Sprintf("connecting to BLE peripheral %s", device.Address.String()),
stopAction: nil,
}

found <- dev
}, fmt.Sprintf("connecting to BLE peripheral %s", device.Address.String()), nil)
result, err := m.performBLEAction(params)
if err != nil {
return bluetooth.Device{}, err
}
Expand All @@ -156,118 +95,95 @@ func (m *BLEController) ConnectToBLEPeripheral(ctx context.Context, device bluet
return typedResult, nil
}

// GetBLEServiceCharacteristic retrieves CSC services from the BLE peripheral
func (m *BLEController) GetBLEServices(ctx context.Context, device bluetooth.Device) ([]bluetooth.DeviceService, error) {
// performBLEAction is a wrapper for performing BLE discovery actions
func (m *BLEController) performBLEAction(params actionParams) (interface{}, error) {

// Pass anonymous function into performBLEAction to discover CSC services
result, err := m.performBLEAction(ctx, func(found chan<- interface{}, errChan chan<- error) {
// Create a context with a timeout
scanCtx, cancel := context.WithTimeout(params.ctx, time.Duration(m.bleDetails.bleConfig.ScanTimeoutSecs)*time.Second)
defer cancel()

// Discover CSC services
services, err := device.DiscoverServices([]bluetooth.UUID{bluetooth.New16BitUUID(0x1816)})
// Create channels for signaling action completion
found := make(chan interface{}, 1)
errChan := make(chan error, 1)

if err != nil {
errChan <- err
return
}
go func() {
logger.Debug(logger.BLE, params.logMessage)
params.action(found, errChan)
}()

found <- services
}, "discovering CSC service "+bluetooth.New16BitUUID(0x1816).String(), nil)
if err != nil {
return m.handleActionCompletion(scanCtx, found, errChan, params.stopAction)
}

// handleActionCompletion handles the completion of the BLE action
func (m *BLEController) handleActionCompletion(ctx context.Context, found <-chan interface{}, errChan <-chan error, stopAction func() error) (interface{}, error) {

select {
case result := <-found:
return result, nil
case err := <-errChan:
return nil, err
case <-ctx.Done():
return m.handleActionTimeout(ctx, stopAction)
}

typedResult := result.([]bluetooth.DeviceService)
logger.Info(logger.BLE, "found CSC service", typedResult[0].UUID().String())
return typedResult, nil
}

// GetBLECharacteristics retrieves CSC characteristics from the BLE peripheral
func (m *BLEController) GetBLECharacteristics(ctx context.Context, services []bluetooth.DeviceService) error {
// handleActionTimeout handles the timeout or cancellation of the BLE action
func (m *BLEController) handleActionTimeout(ctx context.Context, stopAction func() error) (interface{}, error) {

// Pass anonymous function into performBLEAction to discover CSC characteristics
_, err := m.performBLEAction(ctx, func(found chan<- interface{}, errChan chan<- error) {
if stopAction != nil {

// Discover CSC characteristics
characteristics, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{bluetooth.New16BitUUID(0x2A5B)})

if err != nil {
errChan <- err
return
if err := stopAction(); err != nil {
fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Error(logger.BLE, "failed to stop action:", err.Error())
}

m.bleDetails.bleCharacteristic = &characteristics[0]
found <- characteristics
}, "discovering CSC characteristic "+bluetooth.New16BitUUID(0x2A5B).String(), nil)
if err != nil {
logger.Error(logger.BLE, "CSC characteristics discovery failed:", err.Error())
return err
}

logger.Info(logger.BLE, "found CSC characteristic", m.bleDetails.bleCharacteristic.UUID().String())
return nil
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("scanning time limit reached")
}

fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE device setup...")
return nil, ctx.Err()
}

// GetBLEUpdates enables real-time monitoring of BLE peripheral sensor data, handling notification
// setup/teardown, and updates the speed controller with new readings
func (m *BLEController) GetBLEUpdates(ctx context.Context, speedController *speed.SpeedController) error {
// scanAction performs the BLE peripheral scan
func (m *BLEController) scanAction(found chan<- interface{}, errChan chan<- error) {

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

if err := m.bleDetails.bleCharacteristic.EnableNotifications(func(buf []byte) {
speed := m.ProcessBLESpeed(buf)
speedController.UpdateSpeed(speed)
}); err != nil {
return err
if err := m.startScanning(foundChan); err != nil {
errChan <- err
return
}

// Need to disable BLE notifications when done
defer func() {
if err := m.bleDetails.bleCharacteristic.EnableNotifications(nil); err != nil {
logger.Error(logger.BLE, "failed to disable notifications:", err.Error())
}
}()

go func() {
<-ctx.Done()
fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE peripheral reporting...")
errChan <- nil
}()

return <-errChan
found <- <-foundChan
}

// ProcessBLESpeed processes raw speed data from the BLE peripheral and returns the calculated speed
func (m *BLEController) ProcessBLESpeed(data []byte) float64 {
// connectAction performs the connection to the BLE peripheral
func (m *BLEController) connectAction(device bluetooth.ScanResult, found chan<- interface{}, errChan chan<- error) {

dev, err := m.bleDetails.bleAdapter.Connect(device.Address, bluetooth.ConnectionParams{})

newSpeedData, err := m.parseSpeedData(data)
if err != nil {
logger.Error(logger.SPEED, "invalid BLE data:", err.Error())
return 0.0
errChan <- err
return
}

speed := m.calculateSpeed(newSpeedData)
logger.Debug(logger.SPEED, logger.Blue+"BLE sensor speed:", strconv.FormatFloat(speed, 'f', 2, 64), m.speedConfig.SpeedUnits)

return speed
found <- dev
}

// startScanning starts the BLE scan and sends results to the found channel
func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error {

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

if result.Address.String() == m.bleDetails.bleConfig.SensorUUID {

// Found the BLE peripheral, stop scanning
if err := m.bleDetails.bleAdapter.StopScan(); err != nil {
logger.Error(logger.BLE, "failed to stop scan:", err.Error())
}

found <- result
}

})

if err != nil {
Expand All @@ -276,51 +192,3 @@ func (m *BLEController) startScanning(found chan<- bluetooth.ScanResult) error {

return nil
}

// calculateSpeed calculates the current speed based on wheel revolution data... interestingly,
// a BLE speed sensor has no concept of rate: just wheel revolutions and timestamps
func (m *BLEController) calculateSpeed(sm SpeedMeasurement) float64 {

// Initialize last wheel data if not set
if m.lastWheelTime == 0 {
m.lastWheelRevs = sm.wheelRevs
m.lastWheelTime = sm.wheelTime
return 0.0
}

// Calculate time difference between current and last wheel data
timeDiff := sm.wheelTime - m.lastWheelTime
if timeDiff == 0 {
return 0.0
}

// Calculate the rev difference between current and last wheel data
revDiff := int32(sm.wheelRevs - m.lastWheelRevs)
speedConversion := kphConversion
if m.speedConfig.SpeedUnits == config.SpeedUnitsMPH {
speedConversion = mphConversion
}

speed := float64(revDiff) * float64(m.speedConfig.WheelCircumferenceMM) * speedConversion / float64(timeDiff)
m.lastWheelRevs = sm.wheelRevs
m.lastWheelTime = sm.wheelTime

return speed
}

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

if len(data) < 1 {
return SpeedMeasurement{}, fmt.Errorf("empty data")
}

if data[0]&wheelRevFlag == 0 || len(data) < minDataLength {
return SpeedMeasurement{}, fmt.Errorf("invalid data format or length")
}

return SpeedMeasurement{
wheelRevs: binary.LittleEndian.Uint32(data[1:]),
wheelTime: binary.LittleEndian.Uint16(data[5:]),
}, nil
}
Loading

0 comments on commit e675c86

Please sign in to comment.