antplusbridge/cmd/bridge/bluetooth.go
Erik Brakkee 7ee776ba1a slightly more accurate computation of the time of the last full
revolution by using the average RPM over the last interval instead of
using the last RPM.
2026-06-07 23:24:19 +02:00

110 lines
3.4 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
prevRpm := float64(event.Cadence)
for event := range events {
now := event.Now
deltaT := now.Sub(lastUpdateTime).Seconds()
lastUpdateTime = now
watts := event.Power
rpm := float64(event.Cadence)
avgRpm := (rpm + prevRpm) / 2
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 / avgRpm
lastRevTicks = elapsedTicks - fracRev*ticksPerRev
lastCompletedRevs = completedRevs
} else {
// No change we need to emit the same data ss before.
}
prevRpm = rpm
payload := make([]byte, 8)
binary.LittleEndian.PutUint16(payload[0:], bleSignalDetectedFlag)
binary.LittleEndian.PutUint16(payload[2:], watts)
binary.LittleEndian.PutUint16(payload[4:], uint16(lastCompletedRevs))
binary.Little\Endian.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")
}