From cc3bf1210191a6a19c59acef27fadb8931ab87a7 Mon Sep 17 00:00:00 2001 From: Seth Terashima Date: Mon, 1 Jul 2024 18:07:53 -0700 Subject: [PATCH] Improve BLE reliability - Increase timeouts for BLE dialer and listener - Retry initial connection on non-fatal errors until context expires - Tweak scanning interval to reflect BLE advertisement parameters Ideas for future improvements: - Retry connection using client MAC address instead of scanning - Optimize scanning interval - Adjust timeouts using caller context --- cmd/tesla-control/commands.go | 2 +- pkg/connector/ble/ble.go | 36 +++++++++++++++++++++++++++++-- pkg/connector/ble/device_linux.go | 16 +++++++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/cmd/tesla-control/commands.go b/cmd/tesla-control/commands.go index 026406f..32add31 100644 --- a/cmd/tesla-control/commands.go +++ b/cmd/tesla-control/commands.go @@ -655,7 +655,7 @@ var commands = map[string]*Command{ }, "session-info": &Command{ help: "Retrieve session info for PUBLIC_KEY from DOMAIN", - requiresAuth: true, + requiresAuth: false, requiresFleetAPI: false, args: []Argument{ Argument{name: "PUBLIC_KEY", help: "file containing public key (or corresponding private key)"}, diff --git a/pkg/connector/ble/ble.go b/pkg/connector/ble/ble.go index ba931ac..99c80bd 100644 --- a/pkg/connector/ble/ble.go +++ b/pkg/connector/ble/ble.go @@ -6,16 +6,20 @@ import ( "context" "crypto/sha1" "fmt" + "strings" "sync" "time" "github.com/go-ble/ble" "github.com/teslamotors/vehicle-command/internal/log" "github.com/teslamotors/vehicle-command/pkg/connector" + "github.com/teslamotors/vehicle-command/pkg/protocol" ) const maxBLEMessageSize = 1024 +var ErrMaxConnectionsExceeded = protocol.NewError("the vehicle is already connected to the maximum number of BLE devices", false, false) + var ( rxTimeout = time.Second // Timeout interval between receiving chunks of a mesasge maxLatency = 4 * time.Second // Max allowed error when syncing vehicle clock @@ -122,6 +126,27 @@ func (c *Connection) VIN() string { } func NewConnection(ctx context.Context, vin string) (*Connection, error) { + var lastError error + for { + conn, err := tryToConnect(ctx, vin) + if err == nil { + return conn, nil + } + if strings.Contains(err.Error(), "operation not permitted") { + return nil, err + } + log.Warning("BLE connection attempt failed: %s", err) + if err := ctx.Err(); err != nil { + if lastError != nil { + return nil, lastError + } + return nil, err + } + lastError = err + } +} + +func tryToConnect(ctx context.Context, vin string) (*Connection, 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 @@ -145,19 +170,26 @@ func NewConnection(ctx context.Context, vin string) (*Connection, error) { localName := fmt.Sprintf("S%02xC", digest[:8]) log.Debug("Searching for BLE beacon %s...", localName) + canConnect := false filter := func(adv ble.Advertisement) bool { - if !adv.Connectable() || adv.LocalName() != localName { + ln := adv.LocalName() + if ln != localName { return false } + canConnect = adv.Connectable() return true } - log.Debug("Connecting to BLE beacon...") client, err := ble.Connect(ctx, filter) if err != nil { return nil, fmt.Errorf("failed to find BLE beacon for %s (%s): %s", vin, localName, err) } + if !canConnect { + return nil, ErrMaxConnectionsExceeded + } + + log.Debug("Connecting to BLE beacon %s...", client.Addr()) services, err := client.DiscoverServices([]ble.UUID{vehicleServiceUUID}) if err != nil { return nil, fmt.Errorf("ble: failed to enumerate device services: %s", err) diff --git a/pkg/connector/ble/device_linux.go b/pkg/connector/ble/device_linux.go index 29ebd1b..3798266 100644 --- a/pkg/connector/ble/device_linux.go +++ b/pkg/connector/ble/device_linux.go @@ -3,10 +3,24 @@ package ble import ( "github.com/go-ble/ble" "github.com/go-ble/ble/linux" + "github.com/go-ble/ble/linux/hci/cmd" + "time" ) +const bleTimeout = 20 * time.Second + +// TODO: Depending on the model and state, BLE advertisements come every 20ms or every 150ms. + +var scanParams = cmd.LESetScanParameters{ + LEScanType: 1, // Active scanning + LEScanInterval: 0x10, // 10ms + LEScanWindow: 0x10, // 10ms + OwnAddressType: 0, // Static + ScanningFilterPolicy: 2, // Basic filtered +} + func newDevice() (ble.Device, error) { - device, err := linux.NewDevice() + device, err := linux.NewDevice(ble.OptListenerTimeout(bleTimeout), ble.OptDialerTimeout(bleTimeout), ble.OptScanParams(scanParams)) if err != nil { return nil, err }