feat: add CLI with Cobra and timestamped logging

Introduce structured CLI argument parsing using Cobra with three
configurable flags:
  --vendor: USB vendor ID (default: 0x0fcf)
  --product: USB product ID (default: 0x1008)
  --device: ANT+ device number (default: 3001)

Add timestamped logging for all log statements with format
YYYY-MM-DD HH:MM:SS. Log vendor, product, and device values at
startup in both hex and decimal.

Remove hardcoded USB identifiers from main.go and consolidate all
startup logic into cli.go with a dedicated Config struct.

Update ant.go ListenForTrainer signature to accept uint16 for
vendor and product IDs.
This commit is contained in:
Erik Brakkee 2026-05-26 20:05:46 +00:00
parent 39b5a95eb4
commit b24513fcc3
7 changed files with 125 additions and 23 deletions

View File

@ -15,7 +15,7 @@ vet: fmt
build: vet
mkdir -p bin
go build -ldflags "-X main.Version=$(shell git rev-parse HEAD ) -X main.BuildTime=$(shell date +'%Y-%m-%dT%H:%M:%S')" -o bin ./cmd/...
go build -ldflags "-X main.Version=$(shell git rev-parse HEAD ) -X main.BuildTime=$(shell date +'%Y-%m-%dT%H:%M:%S')" -o bin/bridge ./cmd/...
install: build
go install ./...

View File

@ -26,9 +26,9 @@ func resetAndWait(drv io.ReadWriter) error {
}
}
func ListenForTrainer(vendorId gousb.ID, productId gousb.ID, deviceNumber uint32,
func ListenForTrainer(vendorId uint16, productId uint16, deviceNumber uint32,
events chan<- Event) {
drv, err := usb.GetDevice(vendorId, productId)
drv, err := usb.GetDevice(gousb.ID(vendorId), gousb.ID(productId))
if err != nil {
panic(err)
}

89
cmd/bridge/cli.go Normal file
View File

@ -0,0 +1,89 @@
package main
import (
"fmt"
"os"
"os/exec"
"strconv"
"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{}
}
var 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)
}
deviceNumber, err := parseHex(c.device, 32)
if err != nil {
return fmt.Errorf("invalid --device %q: %w", c.device, err)
}
c.vendorID = vendorID
c.productID = productID
c.deviceNumber = uint32(deviceNumber)
logMsgf("ANT Bridge %s built %s", Version, BuildTime)
logMsgf("vendor: 0x%04x/%d", vendorID, vendorID)
logMsgf("product: 0x%04x/%d", productID, productID)
logMsgf("device: 0x%03x/%d", deviceNumber, deviceNumber)
exec.Command("hciconfig", "hci0", "down").Run()
exec.Command("hciconfig", "hci0", "up").Run()
events := make(chan Event)
go ListenForTrainer(uint16(c.vendorID), uint16(c.productID), c.deviceNumber, events)
ExposeBluetooth(events)
return nil
},
}
func init() {
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", "3001", "ANT+ device number (hex or decimal, e.g. 3001 or 0xbb9)")
}
func main() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

18
cmd/bridge/log.go Normal file
View File

@ -0,0 +1,18 @@
package main
import (
"fmt"
"time"
)
func logMsg(msg string) {
fmt.Println(time.Now().Format("2006-01-02 15:04:05") + " " + msg)
}
func logPrintf(format string, a ...any) {
fmt.Printf(time.Now().Format("2006-01-02 15:04:05")+" "+format, a...)
}
func logMsgf(format string, a ...any) {
logPrintf(format+"\n", a...)
}

View File

@ -1,20 +0,0 @@
package main
import (
"os/exec"
"github.com/google/gousb"
)
func main() {
exec.Command("hciconfig", "hci0", "down").Run()
exec.Command("hciconfig", "hci0", "up").Run()
var vendorId gousb.ID = 0x0fcf
var productId gousb.ID = 0x1008
var deviceNumber uint32 = 3001
events := make(chan Event)
go ListenForTrainer(vendorId, productId, deviceNumber, events)
ExposeBluetooth(events)
}

6
go.mod
View File

@ -4,9 +4,15 @@ go 1.26.2
require (
github.com/half2me/antgo v0.0.0-20240825222428-e6b9ddc9c32f
github.com/spf13/cobra v1.10.2
tinygo.org/x/bluetooth v0.15.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)
require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect

9
go.sum
View File

@ -1,3 +1,4 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -9,9 +10,12 @@ github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
github.com/half2me/antgo v0.0.0-20240825222428-e6b9ddc9c32f h1:T1EVnxPi3CqwmJdOs/s76vplnG8t0MEFmwo3bvHrZAo=
github.com/half2me/antgo v0.0.0-20240825222428-e6b9ddc9c32f/go.mod h1:BXp/M+7//3V0GY2ytRJw4vcxZve8mQ0zRVuow+fANkE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 h1:IXxzj3yjfDNXZJ35foY+RpFShqPsZZ81hhCckgfh5PI=
github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
@ -23,6 +27,10 @@ github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I=
github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@ -32,6 +40,7 @@ github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=