antplusbridge/cmd/bridge/bluetooth.go

107 lines
3.3 KiB
Go

package main
import (
"encoding/binary"
"math"
"time"
"tinygo.org/x/bluetooth"
)
const (
bleCyclingPowerServiceUUID = 0x1818 // BLE Cycling Power Service UUID
bleCyclingPowerFeatureCharacteristic = 0x2A65 // BLE Cycling Power Feature characteristic
bleCyclingPowerMeasurementCharacteristic = 0x2A63 // BLE Cycling Power Measurement characteristic
bleSignalDetectedFlag = 0x20 // Signal Detected bit in BLE payload header
bleCyclingPowerFeatureCumulativeTorque = 0x01 // Cumulative Torque feature bit
bleCyclingPowerFeaturePowerOutput = 0x02 // Power Output feature bit
)
var adapter = bluetooth.DefaultAdapter
func must(err error) {
if err != nil {
panic(err)
}
}
func ExposeBluetooth(events <-chan Event) {
must(adapter.Enable())
var powerChar bluetooth.Characteristic
must(adapter.AddService(&bluetooth.Service{
UUID: bluetooth.New16BitUUID(bleCyclingPowerServiceUUID),
Characteristics: []bluetooth.CharacteristicConfig{
{
UUID: bluetooth.New16BitUUID(bleCyclingPowerFeatureCharacteristic),
Flags: bluetooth.CharacteristicReadPermission,
Value: []byte{bleCyclingPowerFeatureCumulativeTorque | bleCyclingPowerFeaturePowerOutput, 0, 0, 0},
},
{
UUID: bluetooth.New16BitUUID(bleCyclingPowerMeasurementCharacteristic),
Flags: bluetooth.CharacteristicNotifyPermission,
Handle: &powerChar,
},
},
}))
must(adapter.DefaultAdvertisement().Configure(bluetooth.AdvertisementOptions{
LocalName: "LinuxAntBtBridge",
ServiceUUIDs: []bluetooth.UUID{bluetooth.New16BitUUID(bleCyclingPowerServiceUUID)},
}))
must(adapter.DefaultAdvertisement().Start())
logMsg("bt: BLE advertising...")
var totalRevs float64 = 0.0
var elapsedTicks float64 = 0.0
// initial event.
event := <-events
lastUpdateTime := event.Now
var lastCompletedRevs float64 = 0.0
var lastRevTicks float64 = 0.0 // time in 1/1024s of last completed revolution
for event := range events {
now := event.Now
deltaT := now.Sub(lastUpdateTime).Seconds()
lastUpdateTime = now
watts := event.Power
rpm := float64(event.Cadence)
totalRevs += rpm / 60.0 * deltaT
elapsedTicks += deltaT * 1024.0
completedRevs := math.Floor(totalRevs)
if completedRevs > lastCompletedRevs {
// we crossed a new full RPM so comput the last time of that rotation
// time at which the last completed revolution occurred
// = current time - fractional part of revolution * time per revolution
fracRev := totalRevs - completedRevs
ticksPerRev := 1024.0 * 60.0 / rpm
lastRevTicks = elapsedTicks - fracRev*ticksPerRev
lastCompletedRevs = completedRevs
} else {
// No change we need to emit the same data ss before.
}
payload := make([]byte, 8)
binary.LittleEndian.PutUint16(payload[0:], bleSignalDetectedFlag)
binary.LittleEndian.PutUint16(payload[2:], watts)
binary.LittleEndian.PutUint16(payload[4:], uint16(lastCompletedRevs))
binary.LittleEndian.PutUint16(payload[6:], uint16(lastRevTicks))
if _, err := powerChar.Write(payload); err != nil {
logPrintf("bt: failed to write power characteristic: %v\n", err)
}
logPrintf("bt: watts: %d rpm: %.0f completedRevs: %.0f lastRevTime: %.0f processingDelay: %v\n",
watts, rpm, completedRevs, lastRevTicks, time.Now().Sub(now))
}
logMsg("bt: BLE advertising stopped")
}