Explain why we use gousb directly instead of antgo/driver/usb (libusb context leak on error paths), document the defer cleanup ordering, and note that errors are returned for graceful retry.
145 lines
4.0 KiB
Go
145 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/google/gousb"
|
|
"github.com/half2me/antgo/ant"
|
|
"github.com/half2me/antgo/device"
|
|
)
|
|
|
|
type antUSB struct {
|
|
in *bufio.Reader
|
|
out *gousb.OutEndpoint
|
|
}
|
|
|
|
func (d *antUSB) Read(p []byte) (int, error) { return d.in.Read(p) }
|
|
func (d *antUSB) Write(p []byte) (int, error) { return d.out.Write(p) }
|
|
|
|
const (
|
|
antDataPageMask = 0x7F // Mask for data page number (7 bits, clears toggle bit)
|
|
antDataPageCyclingPower = 0x10 // Data page 16 = Cycling Power Feature page
|
|
)
|
|
|
|
func resetAndWait(drv io.ReadWriter) error {
|
|
if _, err := drv.Write(ant.SystemResetMessage()); err != nil {
|
|
return err
|
|
}
|
|
// drain until we see the startup message (0x6F)
|
|
for {
|
|
msg, err := ant.ReadMsg(drv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if msg.Class() == ant.MESSAGE_STARTUP {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func ListenForTrainer(vendorId uint16, productId uint16, deviceNumber uint32,
|
|
events chan<- Event) error {
|
|
|
|
// Use gousb directly instead of antgo/driver/usb to manage context
|
|
// lifecycle explicitly. The old code created a new gousb.Context on
|
|
// every call via libusb_init and leaked it on error paths (device not
|
|
// found, interface claim failure, etc.), which triggered a libusb
|
|
// assertion after repeated ListenForTrainer calls.
|
|
gctx := gousb.NewContext()
|
|
defer gctx.Close()
|
|
|
|
dev, err := gctx.OpenDeviceWithVIDPID(gousb.ID(vendorId), gousb.ID(productId))
|
|
if err != nil {
|
|
return fmt.Errorf("ant: open device: %w", err)
|
|
}
|
|
if dev == nil {
|
|
return fmt.Errorf("ant: device not found (0x%04x:0x%04x)", vendorId, productId)
|
|
}
|
|
defer dev.Close()
|
|
|
|
intf, done, err := dev.DefaultInterface()
|
|
if err != nil {
|
|
return fmt.Errorf("ant: claim interface: %w", err)
|
|
}
|
|
// Cleanup is ordered by defer stack: done() releases interface,
|
|
// dev.Close() closes device, gctx.Close() closes context, cancel()
|
|
// cancels the ANT message goroutine.
|
|
defer done()
|
|
|
|
out, err := intf.OutEndpoint(1)
|
|
if err != nil {
|
|
return fmt.Errorf("ant: open out endpoint: %w", err)
|
|
}
|
|
inp, err := intf.InEndpoint(1)
|
|
if err != nil {
|
|
return fmt.Errorf("ant: open in endpoint: %w", err)
|
|
}
|
|
|
|
drv := &antUSB{in: bufio.NewReader(inp), out: out}
|
|
|
|
logMsg("ant: device opened")
|
|
|
|
// Errors are returned (not panicked) so the caller can retry
|
|
// gracefully instead of requiring a full process restart.
|
|
if err := resetAndWait(drv); err != nil {
|
|
return err
|
|
}
|
|
if err := device.StartRxScanModeWait(drv); err != nil {
|
|
return err
|
|
}
|
|
logMsg("ant: scan mode started")
|
|
|
|
msgs := make(chan ant.BroadcastMessage, 100)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
// cancel is the final defer in the stack, ensuring the ANT message
|
|
// goroutine is torn down after all USB resources are released.
|
|
defer cancel()
|
|
go func() {
|
|
device.DumpBroadcastMessages(ctx, drv, msgs)
|
|
logMsg("ant: dump broadcast messages ended")
|
|
}()
|
|
|
|
logMsg("ant: listening...")
|
|
for {
|
|
select {
|
|
case msg := <-msgs:
|
|
if deviceNumber != 0 && msg.DeviceNumber() != deviceNumber {
|
|
logPrintf("ant: received msg from ignored device %v\n", msg.DeviceNumber())
|
|
continue
|
|
}
|
|
//fmt.Printf("raw msg: device type=0x%02x device number=%d content=%x\n",
|
|
// msg.DeviceType(), msg.DeviceNumber(), msg.Content())
|
|
|
|
switch msg.DeviceType() {
|
|
case ant.DEVICE_TYPE_POWER:
|
|
// Check data page first
|
|
page := msg[4] & antDataPageMask
|
|
if page != antDataPageCyclingPower {
|
|
continue
|
|
}
|
|
cur := ant.PowerMessage(msg)
|
|
logPrintf("ant: %v: power: %dW cadence: %d rpm\n",
|
|
msg.DeviceNumber(), cur.InstantaneousPower(), cur.InstantaneousCadence())
|
|
select {
|
|
case events <- Event{
|
|
Power: cur.InstantaneousPower(),
|
|
Cadence: cur.InstantaneousCadence(),
|
|
Now: time.Now(),
|
|
}:
|
|
default:
|
|
logPrintf("ant: dropped event, buffer full\n")
|
|
}
|
|
}
|
|
case <-time.After(10 * time.Second):
|
|
logMsg("ant: no more messages arrived, restarting")
|
|
// Return without error so the caller can retry gracefully
|
|
// rather than panicking and requiring a full restart.
|
|
return nil
|
|
}
|
|
}
|
|
}
|