From b3dd4cc205123ea9645335d48a2dd855b5ba1d99 Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Wed, 5 Feb 2025 10:48:00 +0100 Subject: [PATCH 1/4] Allow targeting specific BD address for BLE on linux --- pkg/connector/ble/ble.go | 38 +++++++++++++++++-- pkg/connector/ble/device_darwin.go | 2 +- pkg/connector/ble/device_linux.go | 58 +++++++++++++++++++++++++++-- pkg/connector/ble/device_windows.go | 3 +- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/pkg/connector/ble/ble.go b/pkg/connector/ble/ble.go index 2de89706..97a331d5 100644 --- a/pkg/connector/ble/ble.go +++ b/pkg/connector/ble/ble.go @@ -132,7 +132,23 @@ func VehicleLocalName(vin string) string { return fmt.Sprintf("S%02xC", digest[:8]) } -func initDevice() error { +// InitDevice initializes a device with the local Bluetooth device address. +// If bdAddr is nil, the first available BLE device will be used. +// On linux it is a MAC address, on MacOS it is a UUID. +// If this function is not called before making a connection, the first available +// BLE device will be used, and this function will return an error. +// NOTE: Currently targeting an address is only supported on Linux. +func InitDevice(bdAddr ble.Addr) error { + mu.Lock() + defer mu.Unlock() + if device != nil { + return fmt.Errorf("ble: device already initialized") + } + + return initDevice(bdAddr) +} + +func initDevice(bdAddr ble.Addr) error { var err error // We don't want concurrent calls to NewConnection that would defeat // the point of reusing the existing BLE device. Note that this is not @@ -141,7 +157,7 @@ func initDevice() error { log.Debug("Reusing existing BLE device") } else { log.Debug("Creating new BLE device") - device, err = newDevice() + device, err = newDevice(bdAddr) if err != nil { return fmt.Errorf("failed to find a BLE device: %s", err) } @@ -150,13 +166,27 @@ func initDevice() error { return nil } +func StopDevice() error { + mu.Lock() + defer mu.Unlock() + if device == nil { + log.Debug("Closing a non-existent BLE device") + return nil + } + if err := device.Stop(); err != nil { + return fmt.Errorf("ble: failed to stop device: %s", err) + } + device = nil + return nil +} + type Advertisement = ble.Advertisement func ScanVehicleBeacon(ctx context.Context, vin string) (Advertisement, error) { mu.Lock() defer mu.Unlock() - if err := initDevice(); err != nil { + if err := initDevice(nil); err != nil { return nil, err } @@ -235,7 +265,7 @@ func tryToConnect(ctx context.Context, vin string, target Advertisement) (*Conne mu.Lock() defer mu.Unlock() - if err = initDevice(); err != nil { + if err = initDevice(nil); err != nil { return nil, false, err } diff --git a/pkg/connector/ble/device_darwin.go b/pkg/connector/ble/device_darwin.go index 6b14f0e9..1b752ee7 100644 --- a/pkg/connector/ble/device_darwin.go +++ b/pkg/connector/ble/device_darwin.go @@ -5,7 +5,7 @@ import ( "github.com/go-ble/ble/darwin" ) -func newDevice() (ble.Device, error) { +func newDevice(ble.Addr) (ble.Device, error) { device, err := darwin.NewDevice() if err != nil { return nil, err diff --git a/pkg/connector/ble/device_linux.go b/pkg/connector/ble/device_linux.go index 3798266f..479b6859 100644 --- a/pkg/connector/ble/device_linux.go +++ b/pkg/connector/ble/device_linux.go @@ -1,10 +1,15 @@ package ble import ( + "fmt" + "strings" + "time" + "github.com/go-ble/ble" "github.com/go-ble/ble/linux" + "github.com/go-ble/ble/linux/hci" "github.com/go-ble/ble/linux/hci/cmd" - "time" + "github.com/teslamotors/vehicle-command/internal/log" ) const bleTimeout = 20 * time.Second @@ -19,8 +24,55 @@ var scanParams = cmd.LESetScanParameters{ ScanningFilterPolicy: 2, // Basic filtered } -func newDevice() (ble.Device, error) { - device, err := linux.NewDevice(ble.OptListenerTimeout(bleTimeout), ble.OptDialerTimeout(bleTimeout), ble.OptScanParams(scanParams)) +func newDevice(bdAddr ble.Addr) (ble.Device, error) { + maxHciDevices := 16 + hciX := -1 + log.Debug("Scanning for HCI devices") + bdAddrStr := "" + if bdAddr != nil { + bdAddrStr = bdAddr.String() + } + var lastErr error + for i := 0; i < maxHciDevices; i++ { + devHci, err := hci.NewHCI(ble.OptDeviceID(i)) + if err != nil { + return nil, fmt.Errorf("can't create HCI %d: %v", i, err) + } + + if err = devHci.Init(); err != nil { + if !strings.Contains(err.Error(), "no such device") { + lastErr = err + log.Debug("Can't init HCI %d: %v", i, err) + } + continue + } + if err = devHci.Close(); err != nil { + return nil, fmt.Errorf("can't close HCI %d: %v", i, err) + } + + log.Debug("Found HCI %d: %s", i, devHci.Addr()) + if bdAddrStr == "" || devHci.Addr().String() == bdAddrStr { + hciX = i + break + } + } + + if hciX == -1 && lastErr != nil { + return nil, lastErr + } else if hciX == -1 { + return nil, fmt.Errorf("no device with address %s", bdAddr) + } + log.Debug("Using HCI %d", hciX) + + opts := []ble.Option{ + ble.OptDeviceID(hciX), + ble.OptListenerTimeout(bleTimeout), + ble.OptDialerTimeout(bleTimeout), + ble.OptScanParams(scanParams), + } + + device, err := linux.NewDeviceWithName("vehicle-command", opts...) + if err != nil { return nil, err } diff --git a/pkg/connector/ble/device_windows.go b/pkg/connector/ble/device_windows.go index 52493141..a83aa01f 100644 --- a/pkg/connector/ble/device_windows.go +++ b/pkg/connector/ble/device_windows.go @@ -2,9 +2,10 @@ package ble import ( "errors" + "github.com/go-ble/ble" ) -func newDevice() (ble.Device, error) { +func newDevice(ble.Addr) (ble.Device, error) { return nil, errors.New("not supported on Windows") } From 86a1ab4118769945077a63c78b5ac2ca47f88b1d Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Wed, 5 Feb 2025 10:48:25 +0100 Subject: [PATCH 2/4] Create small BLE test program --- .gitignore | 1 + cmd/test-ble/main.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 cmd/test-ble/main.go diff --git a/.gitignore b/.gitignore index 00b5ed29..a4a52e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ cscope.* cmd/tesla-control/tesla-control cmd/tesla-auth-token/tesla-auth-token cmd/tesla-key-setup/tesla-key-setup +cmd/test-ble/test-ble examples/unlock/unlock examples/ble/ble *.DS_Store diff --git a/cmd/test-ble/main.go b/cmd/test-ble/main.go new file mode 100644 index 00000000..a3553c40 --- /dev/null +++ b/cmd/test-ble/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "strings" + + goble "github.com/go-ble/ble" + "github.com/teslamotors/vehicle-command/internal/log" + "github.com/teslamotors/vehicle-command/pkg/connector/ble" +) + +var ( + bdAddr = flag.String("bdAddr", "", "Bluetooth device address") +) + +func main() { + flag.Parse() + log.SetLevel(log.LevelDebug) + + var err error + if bdAddr != nil { + log.Info("Target BLE device address: %s", *bdAddr) + err = ble.InitDevice(goble.NewAddr(*bdAddr)) + } else { + log.Info("Using first available BLE device") + err = ble.InitDevice(nil) + } + + if err != nil { + if strings.Contains(err.Error(), "failed to find a BLE device") { + log.Error("No BLE device found") + } else { + log.Error("Failed to initialize BLE device: %v", err) + } + return + } + + log.Info("BLE is working") +} From 022a0eae1ce453e41d9ceaf26245a2881032ac0b Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Wed, 5 Feb 2025 11:25:34 +0100 Subject: [PATCH 3/4] Allow setting target BD address on linux in cli applications --- pkg/cli/config.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/cli/config.go b/pkg/cli/config.go index f67c7d62..394f6f0d 100644 --- a/pkg/cli/config.go +++ b/pkg/cli/config.go @@ -66,6 +66,7 @@ import ( "github.com/teslamotors/vehicle-command/pkg/vehicle" "github.com/99designs/keyring" + goble "github.com/go-ble/ble" ) var DomainsByName = map[string]protocol.Domain{ @@ -150,6 +151,7 @@ type Config struct { DisableCache bool Backend keyring.Config BackendType backendType + BdAddress string Debug bool // Enable keyring debug messages // Domains can limit a vehicle connection to relevant subsystems, which can reduce @@ -183,6 +185,9 @@ func (c *Config) RegisterCommandLineFlags() { if c.Flags.isSet(FlagVIN) { flag.StringVar(&c.VIN, "vin", "", "Vehicle Identification Number. Defaults to $TESLA_VIN.") } + if c.Flags.isSet(FlagBLE) { + flag.StringVar(&c.BdAddress, "bdAddr", "", "Bluetooth device address") + } if c.Flags.isSet(FlagPrivateKey) { if !c.Flags.isSet(FlagVIN) { log.Debug("FlagPrivateKey is set but FlagVIN is not. A VIN is required to send vehicle commands.") @@ -469,6 +474,14 @@ func (c *Config) ConnectRemote(ctx context.Context, skey protocol.ECDHPrivateKey // ConnectLocal connects to a vehicle over BLE. func (c *Config) ConnectLocal(ctx context.Context, skey protocol.ECDHPrivateKey) (car *vehicle.Vehicle, err error) { + var addr goble.Addr + if c.BdAddress != "" { + addr = goble.NewAddr(c.BdAddress) + } + err = ble.InitDevice(addr) + if err != nil { + return nil, err + } conn, err := ble.NewConnection(ctx, c.VIN) if err != nil { return nil, err From aa48216fb0840b6dd3c23cbf719d41fa8dd6d2ac Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Wed, 5 Feb 2025 11:29:55 +0100 Subject: [PATCH 4/4] Add warning for using bdAddress on darwin --- pkg/connector/ble/device_darwin.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/connector/ble/device_darwin.go b/pkg/connector/ble/device_darwin.go index 1b752ee7..49154b37 100644 --- a/pkg/connector/ble/device_darwin.go +++ b/pkg/connector/ble/device_darwin.go @@ -3,9 +3,13 @@ package ble import ( "github.com/go-ble/ble" "github.com/go-ble/ble/darwin" + "github.com/teslamotors/vehicle-command/internal/log" ) -func newDevice(ble.Addr) (ble.Device, error) { +func newDevice(bdAddr ble.Addr) (ble.Device, error) { + if bdAddr != nil || bdAddr.String() != "" { + log.Warning("Setting the Bluetooth device address is not supported on Darwin") + } device, err := darwin.NewDevice() if err != nil { return nil, err