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.
106 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|