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 ) 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{0, 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") }