antplusbridge/cmd/bridge/cli.go
Erik Brakkee 9efdfc9b83 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.
2026-06-14 15:20:56 +02:00

106 lines
2.9 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"strconv"
"time"
"github.com/spf13/cobra"
)
// Version and BuildTime are injected via ldflags at build time.
var Version = "unknown"
var BuildTime = "unknown"
func parseHex(s string, bits int) (uint64, error) {
return strconv.ParseUint(s, 0, bits)
}
type config struct {
vendor string
product string
device string
vendorID uint64
productID uint64
deviceNumber uint32
}
func newConfig() *config {
return &config{}
}
func main() {
rootCmd := &cobra.Command{
Use: "bridge",
Short: "ANT+ to Bluetooth LE bridge",
Long: `Bridge an ANT+ cycling power trainer over Bluetooth Low Energy.
This allows these trainers to be used with modern devices that don't support ANT+.`,
Version: fmt.Sprintf("%s (built %s)", Version, BuildTime),
RunE: func(cmd *cobra.Command, args []string) error {
c := newConfig()
c.vendor = cmd.Flags().Lookup("vendor").Value.String()
c.product = cmd.Flags().Lookup("product").Value.String()
c.device = cmd.Flags().Lookup("device").Value.String()
vendorID, err := parseHex(c.vendor, 16)
if err != nil {
return fmt.Errorf("invalid --vendor %q: %w", c.vendor, err)
}
productID, err := parseHex(c.product, 16)
if err != nil {
return fmt.Errorf("invalid --product %q: %w", c.product, err)
}
c.vendorID = uint64(vendorID)
c.productID = uint64(productID)
if c.device != "" {
deviceNumber, err := parseHex(c.device, 32)
if err != nil {
return fmt.Errorf("invalid --device %q: %w", c.device, err)
}
c.deviceNumber = uint32(deviceNumber)
}
deviceNumber := c.deviceNumber
logMsgf("ANT Bridge %s built %s", Version, BuildTime)
logMsgf("usb: vendor: 0x%04x/%d", vendorID, vendorID)
logMsgf("usb: product: 0x%04x/%d", productID, productID)
if deviceNumber == 0 {
logMsgf("ant: device: any")
} else {
logMsgf("ant: device: 0x%03x/%d", deviceNumber, deviceNumber)
}
if err := exec.Command("hciconfig", "hci0", "down").Run(); err != nil {
logPrintf("bt: hciconfig down failed: %v\n", err)
}
if err := exec.Command("hciconfig", "hci0", "up").Run(); err != nil {
logPrintf("bt: hciconfig up failed: %v\n", err)
}
events := make(chan Event, 1)
go func() {
for {
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)
return nil
},
}
rootCmd.Flags().String("vendor", "0x0fcf", "USB vendor ID (hex, e.g. 0x0fcf)")
rootCmd.Flags().String("product", "0x1008", "USB product ID (hex, e.g. 0x1008)")
rootCmd.Flags().String("device", "", "ANT+ device number (hex or decimal). Use 0 or omit to listen to all devices")
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}