Robustness for the USB connection.

ant: replace usb.GetDevice with direct gousb usage

Remove dependency on antgo/driver/usb to fix libusb context leaks on
device-not-found errors. The previous code called usb.GetDevice which
creates a gousb.Context via libusb_init but leaks it on every error
path (device not found, interface claim failure, etc.). Calling
ListenForTrainer repeatedly (e.g. starting with no dongle and plugging
it in later) triggered a libusb assertion in pthread_key_create because
the leaked contexts were never torn down.

Replace with direct gousb calls in ListenForTrainer, managing the
gousb.Context lifecycle explicitly. Cleanup is ordered correctly on
all paths: release interface, close device, close context, cancel
the ANT message goroutine context.

Also convert panics to returned errors for graceful retry.
This commit is contained in:
Erik Brakkee 2026-06-14 15:20:56 +02:00
parent c75c2ea0e1
commit 9efdfc9b83
2 changed files with 45 additions and 9 deletions

View File

@ -1,16 +1,25 @@
package main
import (
"bufio"
"context"
"fmt"
"io"
"time"
"github.com/google/gousb"
"github.com/half2me/antgo/ant"
"github.com/half2me/antgo/device"
"github.com/half2me/antgo/driver/usb"
)
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
@ -33,21 +42,44 @@ func resetAndWait(drv io.ReadWriter) error {
}
func ListenForTrainer(vendorId uint16, productId uint16, deviceNumber uint32,
events chan<- Event) {
events chan<- Event) error {
drv, err := usb.GetDevice(gousb.ID(vendorId), gousb.ID(productId))
gctx := gousb.NewContext()
defer gctx.Close()
dev, err := gctx.OpenDeviceWithVIDPID(gousb.ID(vendorId), gousb.ID(productId))
if err != nil {
panic(err)
return fmt.Errorf("ant: open device: %w", err)
}
defer drv.Close()
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)
}
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")
if err := resetAndWait(drv); err != nil {
panic(err)
return err
}
if err := device.StartRxScanModeWait(drv); err != nil {
panic(err)
return err
}
logMsg("ant: scan mode started")
@ -92,7 +124,7 @@ func ListenForTrainer(vendorId uint16, productId uint16, deviceNumber uint32,
}
case <-time.After(10 * time.Second):
logMsg("ant: no more messages arrived, restarting")
return
return nil
}
}
}

View File

@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"strconv"
"time"
"github.com/spf13/cobra"
)
@ -83,7 +84,10 @@ This allows these trainers to be used with modern devices that don't support ANT
events := make(chan Event, 1)
go func() {
for {
ListenForTrainer(uint16(c.vendorID), uint16(c.productID), c.deviceNumber, events)
if err := ListenForTrainer(uint16(c.vendorID), uint16(c.productID), c.deviceNumber, events); err != nil {
logPrintf("ant: listener error: %v, retry in 10 seconds\n", err)
time.Sleep(10 * time.Second)
}
}
}()
ExposeBluetooth(events)