From b24513fcc3928eb5b96ae21cdc2f82d90e06716a Mon Sep 17 00:00:00 2001 From: Erik Brakkee Date: Tue, 26 May 2026 20:05:46 +0000 Subject: [PATCH] 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. --- Makefile | 2 +- cmd/bridge/ant.go | 4 +-- cmd/bridge/cli.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/bridge/log.go | 18 ++++++++++ cmd/bridge/main.go | 20 ----------- go.mod | 6 ++++ go.sum | 9 +++++ 7 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 cmd/bridge/cli.go create mode 100644 cmd/bridge/log.go delete mode 100644 cmd/bridge/main.go diff --git a/Makefile b/Makefile index af52902..4c09306 100644 --- a/Makefile +++ b/Makefile @@ -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 ./... diff --git a/cmd/bridge/ant.go b/cmd/bridge/ant.go index 1acb186..1f7cd77 100644 --- a/cmd/bridge/ant.go +++ b/cmd/bridge/ant.go @@ -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) } diff --git a/cmd/bridge/cli.go b/cmd/bridge/cli.go new file mode 100644 index 0000000..6cfc38d --- /dev/null +++ b/cmd/bridge/cli.go @@ -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) + } +} diff --git a/cmd/bridge/log.go b/cmd/bridge/log.go new file mode 100644 index 0000000..4f18690 --- /dev/null +++ b/cmd/bridge/log.go @@ -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...) +} diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go deleted file mode 100644 index 2c17076..0000000 --- a/cmd/bridge/main.go +++ /dev/null @@ -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) -} diff --git a/go.mod b/go.mod index 7cb20c2..802438e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6c70954..677ee3d 100644 --- a/go.sum +++ b/go.sum @@ -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=