From f31130cad14df498d62740f60331112c7931e753 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 13 Feb 2025 15:29:15 -0500 Subject: [PATCH 01/37] Add bluetooth wifi provisioner interface only (still need to add nested interface for OS abstraction). --- .../bluetooth/bluetooth_wifi_provisioner.go | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go new file mode 100644 index 0000000..7545e47 --- /dev/null +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -0,0 +1,153 @@ +// Package bluetooth contains an interface for using bluetooth to retrieve WiFi and robot part credentials for an unprovisioned Viam agent. +package bluetooth + +import ( + "context" + "encoding/json" + "sync" + "time" + + "github.com/pkg/errors" + ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetooth_low_energy" + "go.uber.org/multierr" + "go.viam.com/rdk/logging" + "go.viam.com/utils" +) + +// BluetoothWiFiProvisioner provides an interface for managing the bluetooth (bluetooth-low-energy) service as it pertains to WiFi setup. +type BluetoothWiFiProvisioner interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error + RefreshAvailableWiFi(ctx context.Context, awns *AvailableWiFiNetworks) error + WaitForCloudCredentials(ctx context.Context) (*CloudCredentials, error) + WaitForWiFiCredentials(ctx context.Context) (*WiFiCredentials, error) +} + +// CloudCredentials represent the information needed by the Agent to assign the device a corresponding cloud robot. +type CloudCredentials struct { + RobotPartKeyID string + RobotPartKey string +} + +// WiFiCredentials represent the information needed by the Agent to provision WiFi in the system network manager. +type WiFiCredentials struct { + Ssid string + Psk string +} + +// AvailableWiFiNetworks represent the information needed by the client to display WiFi networks that are accessible by the device. +type AvailableWiFiNetworks struct { + Networks []*struct { + Ssid string `json:"ssid"` + Strength float64 `json:"strength"` // Inclusive range [0.0, 1.0], represents the percentage strength of a WiFi network. + RequiresPsk bool `json:"requires_psk"` + } `json:"networks"` +} + +// ToBytes represents a list of available WiFi networks as bytes, which is essential for transmitting the information over bluetooth. +func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { + return json.Marshal(awns) +} + +// bluetoothWiFiProvisioner provides an interface for managing a BLE (bluetooth-low-energy) peripheral advertisement on Linux. +type bluetoothWiFiProvisioner struct { + bleService ble.BLEService +} + +// Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. +func (bm *bluetoothWiFiProvisioner) Start(ctx context.Context) error { + return bm.bleService.StartAdvertising(ctx) +} + +// Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. +func (bm *bluetoothWiFiProvisioner) Stop(ctx context.Context) error { + return bm.bleService.StopAdvertising() +} + +// RefreshAvailableWiFi updates the list of networks that are advertised via bluetooth as available for connection. +func (bm *bluetoothWiFiProvisioner) RefreshAvailableWiFi(ctx context.Context, awns *ble.AvailableWiFiNetworks) error { + return nil +} + +// WaitForCloudCredentials returns cloud credentials which represent the information required to provision a device as a cloud robot. +func (bm *bluetoothWiFiProvisioner) WaitForCloudCredentials(ctx context.Context) (*CloudCredentials, error) { + var robotPartKeyID, robotPartKey string + var robotPartKeyIDErr, robotPartKeyErr error + + wg := &sync.WaitGroup{} + wg.Add(2) + utils.ManagedGo( + func() { + robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bm.bleService.ReadRobotPartKeyID, "robot part key ID") + }, + wg.Done, + ) + utils.ManagedGo( + func() { + robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bm.bleService.ReadRobotPartKey, "robot part key") + }, + wg.Done, + ) + wg.Wait() + + return &CloudCredentials{RobotPartKeyID: robotPartKeyID, RobotPartKey: robotPartKey}, multierr.Combine(robotPartKeyIDErr, robotPartKeyErr) +} + +// WaitForWiFiCredentials returns WiFi credentials which represent the information required to provision WiFi ona device. +func (bm *bluetoothWiFiProvisioner) WaitForWiFiCredentials(ctx context.Context) (*WiFiCredentials, error) { + var ssid, psk string + var ssidErr, pskErr error + + wg := &sync.WaitGroup{} + wg.Add(2) + utils.ManagedGo( + func() { + ssid, ssidErr = waitForBLEValue(ctx, bm.bleService.ReadSsid, "ssid") + }, + wg.Done, + ) + utils.ManagedGo( + func() { + psk, pskErr = waitForBLEValue(ctx, bm.bleService.ReadPsk, "psk") + }, + wg.Done, + ) + wg.Wait() + + return &WiFiCredentials{Ssid: ssid, Psk: psk}, multierr.Combine(ssidErr, pskErr) +} + +// waitForBLE is used to check for the existence of a new value in a BLE characteristic. +func waitForBLEValue( + ctx context.Context, fn func() (string, error), description string, +) (string, error) { + for { + if ctx.Err() != nil { + return "", ctx.Err() + } + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + time.Sleep(time.Second) + } + v, err := fn() + if err != nil { + var errBLECharNoValue *ble.EmptyBluetoothCharacteristicError + if errors.As(err, &errBLECharNoValue) { + continue + } + return "", errors.WithMessagef(err, "failed to read %s", description) + } + return v, nil + } +} + +// NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. +func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (BluetoothWiFiProvisioner, error) { + bleService, err := ble.NewLinuxBLEService(ctx, logger, name) + if err != nil { + return nil, errors.WithMessage(err, "failed to set up bluetooth-low-energy peripheral (Linux)") + } + return &bluetoothWiFiProvisioner{bleService: bleService}, nil +} From 68694d90ac30749dda95e6a4a8d2f09e246b45d9 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 11:00:01 -0500 Subject: [PATCH 02/37] Add bluetooth service (low-level) interface for abstracting away OS/firware differences. --- go.mod | 2 +- .../bluetooth/bluetooth_low_energy/service.go | 81 +++++++++++++++++++ .../bluetooth/bluetooth_wifi_provisioner.go | 17 +--- 3 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go diff --git a/go.mod b/go.mod index 35a030d..fb4275d 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sergeymakinen/go-systemdconf/v2 v2.0.2 github.com/ulikunitz/xz v0.5.12 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 go.viam.com/api v0.1.357 go.viam.com/rdk v0.51.0 @@ -79,7 +80,6 @@ require ( go.mongodb.org/mongo-driver v1.17.1 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/goleak v1.3.0 // indirect - go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect diff --git a/subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go b/subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go new file mode 100644 index 0000000..c89891c --- /dev/null +++ b/subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go @@ -0,0 +1,81 @@ +// Package ble contains an interface for interacting with the bluetooth stack on a Linux device, specifically with respect to provisioning. +package ble + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "go.viam.com/rdk/logging" +) + +// BLEService represents an interface for managing a bluetooth-low-energy peripheral for reading WiFi and robot part credentials. +// It is the lowest-level interface, and its purpose is to abstract away operating-system and firmware specific differences on machines. +type BLEService interface { + StartAdvertising(ctx context.Context) error + StopAdvertising() error + UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error + ReadSsid() (string, error) + ReadPsk() (string, error) + ReadRobotPartKeyID() (string, error) + ReadRobotPartKey() (string, error) +} + +type AvailableWiFiNetworks struct { + Networks []*struct { + Ssid string `json:"ssid"` + Strength float64 `json:"strength"` // In the inclusive range [0.0, 1.0], it represents the % strength of a WiFi network. + RequiresPsk bool `json:"requires_psk"` + } `json:"networks"` +} + +type linuxBLEService struct{} + +func NewLinuxBLEService(ctx context.Context, logger logging.Logger, name string) (*linuxBLEService, error) { + return nil, errors.New("TODO [APP-7644]") +} + +// StartAdvertising begins advertising a BLE service. +func (s *linuxBLEService) StartAdvertising(ctx context.Context) error { + return errors.New("TODO [APP-7644]") +} + +// StopAdvertising stops advertising a BLE service. +func (s *linuxBLEService) StopAdvertising() error { + return errors.New("TODO [APP-7644]") +} + +// UpdateAvailableWiFiNetworks passes the (assumed) most recently available WiFi networks through a channel so that +// they can be written to the BLE characteristic (and thus updated on paired devices which are "provisioning"). +func (s *linuxBLEService) UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error { + return errors.New("TODO [APP-7644]") +} + +// ReadSsid returns the written ssid value or raises an EmptyBluetoothCharacteristicError error. +func (s *linuxBLEService) ReadSsid() (string, error) { + return "", errors.New("TODO [APP-7644]") +} + +// ReadPsk returns the written psk value or raises an EmptyBluetoothCharacteristicError error. +func (s *linuxBLEService) ReadPsk() (string, error) { + return "", errors.New("TODO [APP-7644]") +} + +// ReadRobotPartKeyID returns the written robot part key ID value or raises an EmptyBluetoothCharacteristicError error. +func (s *linuxBLEService) ReadRobotPartKeyID() (string, error) { + return "", errors.New("TODO [APP-7644]") +} + +// ReadRobotPartKey returns the written robot part key value or raises an EmptyBluetoothCharacteristicError error. +func (s *linuxBLEService) ReadRobotPartKey() (string, error) { + return "", errors.New("TODO [APP-7644]") +} + +// EmptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. +type EmptyBluetoothCharacteristicError struct { + missingValue string +} + +func (e *EmptyBluetoothCharacteristicError) Error() string { + return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) +} diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 7545e47..cb7d89f 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -3,7 +3,6 @@ package bluetooth import ( "context" - "encoding/json" "sync" "time" @@ -18,7 +17,7 @@ import ( type BluetoothWiFiProvisioner interface { Start(ctx context.Context) error Stop(ctx context.Context) error - RefreshAvailableWiFi(ctx context.Context, awns *AvailableWiFiNetworks) error + RefreshAvailableWiFi(ctx context.Context, awns *ble.AvailableWiFiNetworks) error WaitForCloudCredentials(ctx context.Context) (*CloudCredentials, error) WaitForWiFiCredentials(ctx context.Context) (*WiFiCredentials, error) } @@ -35,20 +34,6 @@ type WiFiCredentials struct { Psk string } -// AvailableWiFiNetworks represent the information needed by the client to display WiFi networks that are accessible by the device. -type AvailableWiFiNetworks struct { - Networks []*struct { - Ssid string `json:"ssid"` - Strength float64 `json:"strength"` // Inclusive range [0.0, 1.0], represents the percentage strength of a WiFi network. - RequiresPsk bool `json:"requires_psk"` - } `json:"networks"` -} - -// ToBytes represents a list of available WiFi networks as bytes, which is essential for transmitting the information over bluetooth. -func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { - return json.Marshal(awns) -} - // bluetoothWiFiProvisioner provides an interface for managing a BLE (bluetooth-low-energy) peripheral advertisement on Linux. type bluetoothWiFiProvisioner struct { bleService ble.BLEService From 7d101b07b765d5fddc7a0eb98660337d24f4e9ad Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 12:10:58 -0500 Subject: [PATCH 03/37] WIP --- .../service.go => bluetooth_service.go} | 22 +++---- .../bluetooth/bluetooth_wifi_provisioner.go | 24 +++---- subsystems/provisioning/example.go | 66 +++++++++++++++++++ 3 files changed, 87 insertions(+), 25 deletions(-) rename subsystems/provisioning/bluetooth/{bluetooth_low_energy/service.go => bluetooth_service.go} (76%) create mode 100644 subsystems/provisioning/example.go diff --git a/subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go similarity index 76% rename from subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go rename to subsystems/provisioning/bluetooth/bluetooth_service.go index c89891c..c976d6e 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_low_energy/service.go +++ b/subsystems/provisioning/bluetooth/bluetooth_service.go @@ -9,9 +9,9 @@ import ( "go.viam.com/rdk/logging" ) -// BLEService represents an interface for managing a bluetooth-low-energy peripheral for reading WiFi and robot part credentials. +// BluetoothService represents an interface for managing a bluetooth-low-energy peripheral for reading WiFi and robot part credentials. // It is the lowest-level interface, and its purpose is to abstract away operating-system and firmware specific differences on machines. -type BLEService interface { +type BluetoothService interface { StartAdvertising(ctx context.Context) error StopAdvertising() error UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error @@ -29,45 +29,45 @@ type AvailableWiFiNetworks struct { } `json:"networks"` } -type linuxBLEService struct{} +type linux struct{} -func NewLinuxBLEService(ctx context.Context, logger logging.Logger, name string) (*linuxBLEService, error) { +func NewLinuxBluetooth(ctx context.Context, logger logging.Logger, name string) (*linux, error) { return nil, errors.New("TODO [APP-7644]") } // StartAdvertising begins advertising a BLE service. -func (s *linuxBLEService) StartAdvertising(ctx context.Context) error { +func (s *linux) StartAdvertising(ctx context.Context) error { return errors.New("TODO [APP-7644]") } // StopAdvertising stops advertising a BLE service. -func (s *linuxBLEService) StopAdvertising() error { +func (s *linux) StopAdvertising() error { return errors.New("TODO [APP-7644]") } // UpdateAvailableWiFiNetworks passes the (assumed) most recently available WiFi networks through a channel so that // they can be written to the BLE characteristic (and thus updated on paired devices which are "provisioning"). -func (s *linuxBLEService) UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error { +func (s *linux) UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error { return errors.New("TODO [APP-7644]") } // ReadSsid returns the written ssid value or raises an EmptyBluetoothCharacteristicError error. -func (s *linuxBLEService) ReadSsid() (string, error) { +func (s *linux) ReadSsid() (string, error) { return "", errors.New("TODO [APP-7644]") } // ReadPsk returns the written psk value or raises an EmptyBluetoothCharacteristicError error. -func (s *linuxBLEService) ReadPsk() (string, error) { +func (s *linux) ReadPsk() (string, error) { return "", errors.New("TODO [APP-7644]") } // ReadRobotPartKeyID returns the written robot part key ID value or raises an EmptyBluetoothCharacteristicError error. -func (s *linuxBLEService) ReadRobotPartKeyID() (string, error) { +func (s *linux) ReadRobotPartKeyID() (string, error) { return "", errors.New("TODO [APP-7644]") } // ReadRobotPartKey returns the written robot part key value or raises an EmptyBluetoothCharacteristicError error. -func (s *linuxBLEService) ReadRobotPartKey() (string, error) { +func (s *linux) ReadRobotPartKey() (string, error) { return "", errors.New("TODO [APP-7644]") } diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index cb7d89f..0b8a586 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -7,7 +7,7 @@ import ( "time" "github.com/pkg/errors" - ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetooth_low_energy" + ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetoothlowenergy" "go.uber.org/multierr" "go.viam.com/rdk/logging" "go.viam.com/utils" @@ -36,17 +36,17 @@ type WiFiCredentials struct { // bluetoothWiFiProvisioner provides an interface for managing a BLE (bluetooth-low-energy) peripheral advertisement on Linux. type bluetoothWiFiProvisioner struct { - bleService ble.BLEService + bluetoothService ble.bluetoothService } // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. func (bm *bluetoothWiFiProvisioner) Start(ctx context.Context) error { - return bm.bleService.StartAdvertising(ctx) + return bm.bluetoothService.StartAdvertising(ctx) } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. func (bm *bluetoothWiFiProvisioner) Stop(ctx context.Context) error { - return bm.bleService.StopAdvertising() + return bm.bluetoothService.StopAdvertising() } // RefreshAvailableWiFi updates the list of networks that are advertised via bluetooth as available for connection. @@ -63,13 +63,13 @@ func (bm *bluetoothWiFiProvisioner) WaitForCloudCredentials(ctx context.Context) wg.Add(2) utils.ManagedGo( func() { - robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bm.bleService.ReadRobotPartKeyID, "robot part key ID") + robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bm.bluetoothService.ReadRobotPartKeyID, "robot part key ID") }, wg.Done, ) utils.ManagedGo( func() { - robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bm.bleService.ReadRobotPartKey, "robot part key") + robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bm.bluetoothService.ReadRobotPartKey, "robot part key") }, wg.Done, ) @@ -87,13 +87,13 @@ func (bm *bluetoothWiFiProvisioner) WaitForWiFiCredentials(ctx context.Context) wg.Add(2) utils.ManagedGo( func() { - ssid, ssidErr = waitForBLEValue(ctx, bm.bleService.ReadSsid, "ssid") + ssid, ssidErr = waitForBLEValue(ctx, bm.bluetoothService.ReadSsid, "ssid") }, wg.Done, ) utils.ManagedGo( func() { - psk, pskErr = waitForBLEValue(ctx, bm.bleService.ReadPsk, "psk") + psk, pskErr = waitForBLEValue(ctx, bm.bluetoothService.ReadPsk, "psk") }, wg.Done, ) @@ -129,10 +129,6 @@ func waitForBLEValue( } // NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (BluetoothWiFiProvisioner, error) { - bleService, err := ble.NewLinuxBLEService(ctx, logger, name) - if err != nil { - return nil, errors.WithMessage(err, "failed to set up bluetooth-low-energy peripheral (Linux)") - } - return &bluetoothWiFiProvisioner{bleService: bleService}, nil +func NewBluetoothWiFiProvisioner[T ble.bluetoothService](ctx context.Context, logger logging.Logger, name string, bluetoothService T) (BluetoothWiFiProvisioner, error) { + return &bluetoothWiFiProvisioner{bluetoothService: bluetoothService}, nil } diff --git a/subsystems/provisioning/example.go b/subsystems/provisioning/example.go new file mode 100644 index 0000000..75fb670 --- /dev/null +++ b/subsystems/provisioning/example.go @@ -0,0 +1,66 @@ +package provisioning + +import ( + "context" + + "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth" + ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetoothlowenergy" + "go.viam.com/rdk/logging" +) + +func bluetoothWiFiProvisioningExample(ctx context.Context, logger logging.Logger) { + bwp, err := bluetooth.NewBluetoothWiFiProvisioner(ctx, logger, "Max's Bluetooth Peripheral") + if err != nil { + logger.Fatal(err) + } + + // Pass available WiFi networks from Agent's network manager to the bluetooth peripheral so it + // can send those "options" over bluetooth. Assuming a mobile app is connected via bluetooth, + // it can advertise the available WiFi networks to the user in a dropdown selection. + wifiNetworks := &ble.AvailableWiFiNetworks{ + Networks: []*struct { + Ssid string `json:"ssid"` + Strength float64 `json:"strength"` + RequiresPsk bool `json:"requires_psk"` + }{ + { + Ssid: "HomeWiFi", + Strength: 0.85, + RequiresPsk: true, + }, + { + Ssid: "GuestWiFi", + Strength: 0.65, + RequiresPsk: false, + }, + }, + } + if err := bwp.RefreshAvailableWiFi(ctx, wifiNetworks); err != nil { + logger.Fatal(err) + } + + // RefreshAvailableWiFi is separate from Start because we will repeatedly call to refresh + // the advertised available networks, but we will only call start once at the beginning. + if err := bwp.Start(ctx); err != nil { + logger.Fatal(err) + } + + wifiCredentials, err := bwp.WaitForWiFiCredentials(ctx) // This is blocking. + if err != nil { + logger.Fatal(err) + } + logger.Infof("user provided SSID: %s and Psk: %s, will attempt to connect with those WiFi credentials...", + wifiCredentials.Ssid, wifiCredentials.Psk) + + cloudCredentials, err := bwp.WaitForCloudCredentials(ctx) // This is blocking. + if err != nil { + logger.Fatal(err) + } + logger.Infof("user provided Robot Part Key ID: %s and Robot Part Key: %s, will attempt to connect with those cloud config credentials...", + cloudCredentials.RobotPartKeyID, cloudCredentials.RobotPartKey) + + // Stop once we've gotten all required credentials, at which point the existing Agent provisioning loop can proceed. + if err := bwp.Stop(ctx); err != nil { + logger.Fatal(err) + } +} From 6fa786f5607dadf85b3aa56c3eb93a39536650e6 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 14:07:41 -0500 Subject: [PATCH 04/37] Remove example.go file. --- .../bluetooth/bluetooth_service.go | 84 +++------ .../bluetooth/bluetooth_wifi_provisioner.go | 167 ++++++++++-------- subsystems/provisioning/example.go | 66 ------- 3 files changed, 114 insertions(+), 203 deletions(-) delete mode 100644 subsystems/provisioning/example.go diff --git a/subsystems/provisioning/bluetooth/bluetooth_service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go index c976d6e..f580d84 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_service.go +++ b/subsystems/provisioning/bluetooth/bluetooth_service.go @@ -4,78 +4,34 @@ package ble import ( "context" "fmt" - - "github.com/pkg/errors" - "go.viam.com/rdk/logging" ) -// BluetoothService represents an interface for managing a bluetooth-low-energy peripheral for reading WiFi and robot part credentials. -// It is the lowest-level interface, and its purpose is to abstract away operating-system and firmware specific differences on machines. -type BluetoothService interface { - StartAdvertising(ctx context.Context) error - StopAdvertising() error - UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error - ReadSsid() (string, error) - ReadPsk() (string, error) - ReadRobotPartKeyID() (string, error) - ReadRobotPartKey() (string, error) -} - -type AvailableWiFiNetworks struct { - Networks []*struct { - Ssid string `json:"ssid"` - Strength float64 `json:"strength"` // In the inclusive range [0.0, 1.0], it represents the % strength of a WiFi network. - RequiresPsk bool `json:"requires_psk"` - } `json:"networks"` -} - -type linux struct{} - -func NewLinuxBluetooth(ctx context.Context, logger logging.Logger, name string) (*linux, error) { - return nil, errors.New("TODO [APP-7644]") -} - -// StartAdvertising begins advertising a BLE service. -func (s *linux) StartAdvertising(ctx context.Context) error { - return errors.New("TODO [APP-7644]") -} - -// StopAdvertising stops advertising a BLE service. -func (s *linux) StopAdvertising() error { - return errors.New("TODO [APP-7644]") -} - -// UpdateAvailableWiFiNetworks passes the (assumed) most recently available WiFi networks through a channel so that -// they can be written to the BLE characteristic (and thus updated on paired devices which are "provisioning"). -func (s *linux) UpdateAvailableWiFiNetworks(awns *AvailableWiFiNetworks) error { - return errors.New("TODO [APP-7644]") -} - -// ReadSsid returns the written ssid value or raises an EmptyBluetoothCharacteristicError error. -func (s *linux) ReadSsid() (string, error) { - return "", errors.New("TODO [APP-7644]") -} +type bluetoothService interface { + startAdvertisingBLE(ctx context.Context) error + stopAdvertisingBLE() error + enableAutoAcceptPairRequest() -// ReadPsk returns the written psk value or raises an EmptyBluetoothCharacteristicError error. -func (s *linux) ReadPsk() (string, error) { - return "", errors.New("TODO [APP-7644]") -} - -// ReadRobotPartKeyID returns the written robot part key ID value or raises an EmptyBluetoothCharacteristicError error. -func (s *linux) ReadRobotPartKeyID() (string, error) { - return "", errors.New("TODO [APP-7644]") -} + // Networks need to be written to (and thus readble from) a bluetooth service. + writeAvailableNetworks(networks *AvailableWiFiNetworks) error -// ReadRobotPartKey returns the written robot part key value or raises an EmptyBluetoothCharacteristicError error. -func (s *linux) ReadRobotPartKey() (string, error) { - return "", errors.New("TODO [APP-7644]") + // Credentials need to be extracted from a bluetooth service (inputted by a client). + readSsid() (string, error) + readPsk() (string, error) + readRobotPartKeyID() (string, error) + readRobotPartKey() (string, error) } -// EmptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. -type EmptyBluetoothCharacteristicError struct { +// emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. +type emptyBluetoothCharacteristicError struct { missingValue string } -func (e *EmptyBluetoothCharacteristicError) Error() string { +func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } + +func newEmptyBluetoothCharacteristicError(missingValue string) error { + return &emptyBluetoothCharacteristicError{ + missingValue: missingValue, + } +} diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 0b8a586..9ec99bb 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -1,105 +1,117 @@ -// Package bluetooth contains an interface for using bluetooth to retrieve WiFi and robot part credentials for an unprovisioned Viam agent. -package bluetooth +// Package ble contains an interface for using bluetooth-low-energy to retrieve WiFi and robot part credentials for an unprovisioned Agent. +package ble import ( "context" + "encoding/json" + "runtime" "sync" "time" "github.com/pkg/errors" - ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetoothlowenergy" "go.uber.org/multierr" "go.viam.com/rdk/logging" "go.viam.com/utils" ) -// BluetoothWiFiProvisioner provides an interface for managing the bluetooth (bluetooth-low-energy) service as it pertains to WiFi setup. -type BluetoothWiFiProvisioner interface { - Start(ctx context.Context) error - Stop(ctx context.Context) error - RefreshAvailableWiFi(ctx context.Context, awns *ble.AvailableWiFiNetworks) error - WaitForCloudCredentials(ctx context.Context) (*CloudCredentials, error) - WaitForWiFiCredentials(ctx context.Context) (*WiFiCredentials, error) -} - -// CloudCredentials represent the information needed by the Agent to assign the device a corresponding cloud robot. -type CloudCredentials struct { +// Credentials represent the minimum required information needed to provision a Viam Agent. +type Credentials struct { + Ssid string + Psk string RobotPartKeyID string RobotPartKey string } -// WiFiCredentials represent the information needed by the Agent to provision WiFi in the system network manager. -type WiFiCredentials struct { - Ssid string - Psk string +// AvailableWiFiNetworks represent the networks that the device has detected (and which may be available for connection). +type AvailableWiFiNetworks struct { + Networks []*struct { + Ssid string `json:"ssid"` + Strength float64 `json:"strength"` // Inclusive range [0.0, 1.0], represents the % strength of a WiFi network. + RequiresPsk bool `json:"requires_psk"` + } `json:"networks"` } -// bluetoothWiFiProvisioner provides an interface for managing a BLE (bluetooth-low-energy) peripheral advertisement on Linux. -type bluetoothWiFiProvisioner struct { - bluetoothService ble.bluetoothService +func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { + return json.Marshal(awns) } -// Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bm *bluetoothWiFiProvisioner) Start(ctx context.Context) error { - return bm.bluetoothService.StartAdvertising(ctx) +// BluetoothWiFiProvisioner provides an interface for managing the bluetooth (bluetooth-low-energy) service as it pertains to WiFi setup. +type BluetoothWiFiProvisioner interface { + Start(ctx context.Context) error + Stop() error + RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error + WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) } -// Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bm *bluetoothWiFiProvisioner) Stop(ctx context.Context) error { - return bm.bluetoothService.StopAdvertising() +// linuxBluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. +type bluetoothWiFiProvisioner[T bluetoothService] struct { + svc T } -// RefreshAvailableWiFi updates the list of networks that are advertised via bluetooth as available for connection. -func (bm *bluetoothWiFiProvisioner) RefreshAvailableWiFi(ctx context.Context, awns *ble.AvailableWiFiNetworks) error { +// Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. +func (bwp *bluetoothWiFiProvisioner[T]) Start(ctx context.Context) error { + if err := bwp.svc.startAdvertisingBLE(ctx); err != nil { + return err + } + bwp.svc.enableAutoAcceptPairRequest() // Enables auto-accept of pair request on this device. return nil } -// WaitForCloudCredentials returns cloud credentials which represent the information required to provision a device as a cloud robot. -func (bm *bluetoothWiFiProvisioner) WaitForCloudCredentials(ctx context.Context) (*CloudCredentials, error) { - var robotPartKeyID, robotPartKey string - var robotPartKeyIDErr, robotPartKeyErr error - - wg := &sync.WaitGroup{} - wg.Add(2) - utils.ManagedGo( - func() { - robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bm.bluetoothService.ReadRobotPartKeyID, "robot part key ID") - }, - wg.Done, - ) - utils.ManagedGo( - func() { - robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bm.bluetoothService.ReadRobotPartKey, "robot part key") - }, - wg.Done, - ) - wg.Wait() - - return &CloudCredentials{RobotPartKeyID: robotPartKeyID, RobotPartKey: robotPartKey}, multierr.Combine(robotPartKeyIDErr, robotPartKeyErr) +// Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. +func (bwp *bluetoothWiFiProvisioner[T]) Stop() error { + return bwp.svc.stopAdvertisingBLE() } -// WaitForWiFiCredentials returns WiFi credentials which represent the information required to provision WiFi ona device. -func (bm *bluetoothWiFiProvisioner) WaitForWiFiCredentials(ctx context.Context) (*WiFiCredentials, error) { - var ssid, psk string - var ssidErr, pskErr error +// Update updates the list of networks that are advertised via bluetooth as available. +func (bwp *bluetoothWiFiProvisioner[T]) RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error { + if ctx.Err() != nil { + return ctx.Err() + } + return bwp.svc.writeAvailableNetworks(awns) +} +// WaitForCredentials returns credentials which represent the information required to provision a robot part and its WiFi. +func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { + if !requiresWiFiCredentials && !requiresCloudCredentials { + return nil, errors.New("should be waiting for either cloud credentials or WiFi credentials") + } + var ssid, psk, robotPartKeyID, robotPartKey string + var ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error wg := &sync.WaitGroup{} - wg.Add(2) - utils.ManagedGo( - func() { - ssid, ssidErr = waitForBLEValue(ctx, bm.bluetoothService.ReadSsid, "ssid") - }, - wg.Done, - ) - utils.ManagedGo( - func() { - psk, pskErr = waitForBLEValue(ctx, bm.bluetoothService.ReadPsk, "psk") - }, - wg.Done, - ) + if requiresWiFiCredentials { + wg.Add(2) + utils.ManagedGo( + func() { + ssid, ssidErr = waitForBLEValue(ctx, bwp.svc.readSsid, "ssid") + }, + wg.Done, + ) + utils.ManagedGo( + func() { + psk, pskErr = waitForBLEValue(ctx, bwp.svc.readPsk, "psk") + }, + wg.Done, + ) + } + if requiresCloudCredentials { + wg.Add(2) + utils.ManagedGo( + func() { + robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bwp.svc.readRobotPartKeyID, "robot part key ID") + }, + wg.Done, + ) + utils.ManagedGo( + func() { + robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bwp.svc.readRobotPartKey, "robot part key") + }, + wg.Done, + ) + } wg.Wait() - - return &WiFiCredentials{Ssid: ssid, Psk: psk}, multierr.Combine(ssidErr, pskErr) + return &Credentials{ + Ssid: ssid, Psk: psk, RobotPartKeyID: robotPartKeyID, RobotPartKey: robotPartKey, + }, multierr.Combine(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) } // waitForBLE is used to check for the existence of a new value in a BLE characteristic. @@ -118,7 +130,7 @@ func waitForBLEValue( } v, err := fn() if err != nil { - var errBLECharNoValue *ble.EmptyBluetoothCharacteristicError + var errBLECharNoValue *emptyBluetoothCharacteristicError if errors.As(err, &errBLECharNoValue) { continue } @@ -129,6 +141,15 @@ func waitForBLEValue( } // NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioner[T ble.bluetoothService](ctx context.Context, logger logging.Logger, name string, bluetoothService T) (BluetoothWiFiProvisioner, error) { - return &bluetoothWiFiProvisioner{bluetoothService: bluetoothService}, nil +func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (BluetoothWiFiProvisioner, error) { + switch os := runtime.GOOS; os { + case "linux": + fallthrough + case "windows": + fallthrough + case "darwin": + fallthrough + default: + return nil, errors.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported") + } } diff --git a/subsystems/provisioning/example.go b/subsystems/provisioning/example.go deleted file mode 100644 index 75fb670..0000000 --- a/subsystems/provisioning/example.go +++ /dev/null @@ -1,66 +0,0 @@ -package provisioning - -import ( - "context" - - "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth" - ble "github.com/viamrobotics/agent/subsystems/provisioning/bluetooth/bluetoothlowenergy" - "go.viam.com/rdk/logging" -) - -func bluetoothWiFiProvisioningExample(ctx context.Context, logger logging.Logger) { - bwp, err := bluetooth.NewBluetoothWiFiProvisioner(ctx, logger, "Max's Bluetooth Peripheral") - if err != nil { - logger.Fatal(err) - } - - // Pass available WiFi networks from Agent's network manager to the bluetooth peripheral so it - // can send those "options" over bluetooth. Assuming a mobile app is connected via bluetooth, - // it can advertise the available WiFi networks to the user in a dropdown selection. - wifiNetworks := &ble.AvailableWiFiNetworks{ - Networks: []*struct { - Ssid string `json:"ssid"` - Strength float64 `json:"strength"` - RequiresPsk bool `json:"requires_psk"` - }{ - { - Ssid: "HomeWiFi", - Strength: 0.85, - RequiresPsk: true, - }, - { - Ssid: "GuestWiFi", - Strength: 0.65, - RequiresPsk: false, - }, - }, - } - if err := bwp.RefreshAvailableWiFi(ctx, wifiNetworks); err != nil { - logger.Fatal(err) - } - - // RefreshAvailableWiFi is separate from Start because we will repeatedly call to refresh - // the advertised available networks, but we will only call start once at the beginning. - if err := bwp.Start(ctx); err != nil { - logger.Fatal(err) - } - - wifiCredentials, err := bwp.WaitForWiFiCredentials(ctx) // This is blocking. - if err != nil { - logger.Fatal(err) - } - logger.Infof("user provided SSID: %s and Psk: %s, will attempt to connect with those WiFi credentials...", - wifiCredentials.Ssid, wifiCredentials.Psk) - - cloudCredentials, err := bwp.WaitForCloudCredentials(ctx) // This is blocking. - if err != nil { - logger.Fatal(err) - } - logger.Infof("user provided Robot Part Key ID: %s and Robot Part Key: %s, will attempt to connect with those cloud config credentials...", - cloudCredentials.RobotPartKeyID, cloudCredentials.RobotPartKey) - - // Stop once we've gotten all required credentials, at which point the existing Agent provisioning loop can proceed. - if err := bwp.Stop(ctx); err != nil { - logger.Fatal(err) - } -} From c5c1bc60c89a98b6043e2cdfdaa12f9f55baa71f Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 14:08:50 -0500 Subject: [PATCH 05/37] Improve inline commenting. --- subsystems/provisioning/bluetooth/bluetooth_service.go | 4 ++-- .../provisioning/bluetooth/bluetooth_wifi_provisioner.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go index f580d84..c2ffeea 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_service.go +++ b/subsystems/provisioning/bluetooth/bluetooth_service.go @@ -11,10 +11,10 @@ type bluetoothService interface { stopAdvertisingBLE() error enableAutoAcceptPairRequest() - // Networks need to be written to (and thus readble from) a bluetooth service. + // Networks need to be written to a bluetooth service. Clients can then read this data. writeAvailableNetworks(networks *AvailableWiFiNetworks) error - // Credentials need to be extracted from a bluetooth service (inputted by a client). + // Credentials that ae written by a client need to be extracted from a bluetooth service. readSsid() (string, error) readPsk() (string, error) readRobotPartKeyID() (string, error) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 9ec99bb..2ab5b51 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -43,7 +43,7 @@ type BluetoothWiFiProvisioner interface { WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) } -// linuxBluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. +// bluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. type bluetoothWiFiProvisioner[T bluetoothService] struct { svc T } @@ -70,7 +70,7 @@ func (bwp *bluetoothWiFiProvisioner[T]) RefreshAvailableNetworks(ctx context.Con return bwp.svc.writeAvailableNetworks(awns) } -// WaitForCredentials returns credentials which represent the information required to provision a robot part and its WiFi. +// WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { if !requiresWiFiCredentials && !requiresCloudCredentials { return nil, errors.New("should be waiting for either cloud credentials or WiFi credentials") From 4b5fcc6ce0299a9b3c5c4d311a7950efb79fa092 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 14:44:12 -0500 Subject: [PATCH 06/37] Update subsystems/provisioning/bluetooth/bluetooth_service.go --- subsystems/provisioning/bluetooth/bluetooth_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go index c2ffeea..6cb054a 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_service.go +++ b/subsystems/provisioning/bluetooth/bluetooth_service.go @@ -11,7 +11,7 @@ type bluetoothService interface { stopAdvertisingBLE() error enableAutoAcceptPairRequest() - // Networks need to be written to a bluetooth service. Clients can then read this data. + // Available WiFi networks need to be written to a bluetooth service. Clients read from this inputted data. writeAvailableNetworks(networks *AvailableWiFiNetworks) error // Credentials that ae written by a client need to be extracted from a bluetooth service. From 6edd5cd76ef7ddf3f9879754452d314ddecee8e6 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Fri, 14 Feb 2025 14:44:17 -0500 Subject: [PATCH 07/37] Update subsystems/provisioning/bluetooth/bluetooth_service.go --- subsystems/provisioning/bluetooth/bluetooth_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go index 6cb054a..21a8803 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_service.go +++ b/subsystems/provisioning/bluetooth/bluetooth_service.go @@ -14,7 +14,7 @@ type bluetoothService interface { // Available WiFi networks need to be written to a bluetooth service. Clients read from this inputted data. writeAvailableNetworks(networks *AvailableWiFiNetworks) error - // Credentials that ae written by a client need to be extracted from a bluetooth service. + // Credentials that are written by a client need to be extracted from a bluetooth service. readSsid() (string, error) readPsk() (string, error) readRobotPartKeyID() (string, error) From 02dd56f69ab0adfecc32b42711c9999e7af3ee0d Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 10:26:58 -0500 Subject: [PATCH 08/37] Remove both interfaces and add inline TODOs where Linux functionality is yet to be implemented. --- .../bluetooth/bluetooth_service.go | 37 ------- .../bluetooth/bluetooth_wifi_provisioner.go | 97 +++++++++---------- .../bluetooth_wifi_provisioner_utils.go | 50 ++++++++++ 3 files changed, 97 insertions(+), 87 deletions(-) delete mode 100644 subsystems/provisioning/bluetooth/bluetooth_service.go create mode 100644 subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go diff --git a/subsystems/provisioning/bluetooth/bluetooth_service.go b/subsystems/provisioning/bluetooth/bluetooth_service.go deleted file mode 100644 index 21a8803..0000000 --- a/subsystems/provisioning/bluetooth/bluetooth_service.go +++ /dev/null @@ -1,37 +0,0 @@ -// Package ble contains an interface for interacting with the bluetooth stack on a Linux device, specifically with respect to provisioning. -package ble - -import ( - "context" - "fmt" -) - -type bluetoothService interface { - startAdvertisingBLE(ctx context.Context) error - stopAdvertisingBLE() error - enableAutoAcceptPairRequest() - - // Available WiFi networks need to be written to a bluetooth service. Clients read from this inputted data. - writeAvailableNetworks(networks *AvailableWiFiNetworks) error - - // Credentials that are written by a client need to be extracted from a bluetooth service. - readSsid() (string, error) - readPsk() (string, error) - readRobotPartKeyID() (string, error) - readRobotPartKey() (string, error) -} - -// emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. -type emptyBluetoothCharacteristicError struct { - missingValue string -} - -func (e *emptyBluetoothCharacteristicError) Error() string { - return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) -} - -func newEmptyBluetoothCharacteristicError(missingValue string) error { - return &emptyBluetoothCharacteristicError{ - missingValue: missingValue, - } -} diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 2ab5b51..87c36f3 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -6,7 +6,6 @@ import ( "encoding/json" "runtime" "sync" - "time" "github.com/pkg/errors" "go.uber.org/multierr" @@ -35,43 +34,33 @@ func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { return json.Marshal(awns) } -// BluetoothWiFiProvisioner provides an interface for managing the bluetooth (bluetooth-low-energy) service as it pertains to WiFi setup. -type BluetoothWiFiProvisioner interface { - Start(ctx context.Context) error - Stop() error - RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error - WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) -} - // bluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. -type bluetoothWiFiProvisioner[T bluetoothService] struct { - svc T -} +type BluetoothWiFiProvisioner struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothWiFiProvisioner[T]) Start(ctx context.Context) error { - if err := bwp.svc.startAdvertisingBLE(ctx); err != nil { +func (bwp *BluetoothWiFiProvisioner) Start(ctx context.Context) error { + if err := bwp.startAdvertisingBLE(ctx); err != nil { return err } - bwp.svc.enableAutoAcceptPairRequest() // Enables auto-accept of pair request on this device. + bwp.enableAutoAcceptPairRequest() // Enables auto-accept of pair request on this device. return nil } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothWiFiProvisioner[T]) Stop() error { - return bwp.svc.stopAdvertisingBLE() +func (bwp *BluetoothWiFiProvisioner) Stop() error { + return bwp.stopAdvertisingBLE() } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *bluetoothWiFiProvisioner[T]) RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error { +func (bwp *BluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error { if ctx.Err() != nil { return ctx.Err() } - return bwp.svc.writeAvailableNetworks(awns) + return bwp.writeAvailableNetworks(awns) } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { +func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { if !requiresWiFiCredentials && !requiresCloudCredentials { return nil, errors.New("should be waiting for either cloud credentials or WiFi credentials") } @@ -82,13 +71,13 @@ func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, wg.Add(2) utils.ManagedGo( func() { - ssid, ssidErr = waitForBLEValue(ctx, bwp.svc.readSsid, "ssid") + ssid, ssidErr = waitForBLEValue(ctx, bwp.readSsid, "ssid") }, wg.Done, ) utils.ManagedGo( func() { - psk, pskErr = waitForBLEValue(ctx, bwp.svc.readPsk, "psk") + psk, pskErr = waitForBLEValue(ctx, bwp.readPsk, "psk") }, wg.Done, ) @@ -97,13 +86,13 @@ func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, wg.Add(2) utils.ManagedGo( func() { - robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bwp.svc.readRobotPartKeyID, "robot part key ID") + robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bwp.readRobotPartKeyID, "robot part key ID") }, wg.Done, ) utils.ManagedGo( func() { - robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bwp.svc.readRobotPartKey, "robot part key") + robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bwp.readRobotPartKey, "robot part key") }, wg.Done, ) @@ -114,34 +103,42 @@ func (bwp *bluetoothWiFiProvisioner[T]) WaitForCredentials(ctx context.Context, }, multierr.Combine(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) } -// waitForBLE is used to check for the existence of a new value in a BLE characteristic. -func waitForBLEValue( - ctx context.Context, fn func() (string, error), description string, -) (string, error) { - for { - if ctx.Err() != nil { - return "", ctx.Err() - } - select { - case <-ctx.Done(): - return "", ctx.Err() - default: - time.Sleep(time.Second) - } - v, err := fn() - if err != nil { - var errBLECharNoValue *emptyBluetoothCharacteristicError - if errors.As(err, &errBLECharNoValue) { - continue - } - return "", errors.WithMessagef(err, "failed to read %s", description) - } - return v, nil - } +/** Unexported helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ + +func (bwp *BluetoothWiFiProvisioner) startAdvertisingBLE(ctx context.Context) error { + return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) stopAdvertisingBLE() error { + return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() error { + return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(networks *AvailableWiFiNetworks) error { + return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) readSsid() (string, error) { + return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) readPsk() (string, error) { + return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) readRobotPartKeyID() (string, error) { + return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") +} + +func (bwp *BluetoothWiFiProvisioner) readRobotPartKey() (string, error) { + return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") } // NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (BluetoothWiFiProvisioner, error) { +func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (*BluetoothWiFiProvisioner, error) { switch os := runtime.GOOS; os { case "linux": fallthrough @@ -150,6 +147,6 @@ func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, nam case "darwin": fallthrough default: - return nil, errors.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported") + return nil, errors.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) } } diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go new file mode 100644 index 0000000..2c8171d --- /dev/null +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go @@ -0,0 +1,50 @@ +package ble + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" +) + +// emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. +type emptyBluetoothCharacteristicError struct { + missingValue string +} + +func (e *emptyBluetoothCharacteristicError) Error() string { + return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) +} + +func newEmptyBluetoothCharacteristicError(missingValue string) error { + return &emptyBluetoothCharacteristicError{ + missingValue: missingValue, + } +} + +// waitForBLE is used to check for the existence of a new value in a BLE characteristic. +func waitForBLEValue( + ctx context.Context, fn func() (string, error), description string, +) (string, error) { + for { + if ctx.Err() != nil { + return "", ctx.Err() + } + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + time.Sleep(time.Second) + } + v, err := fn() + if err != nil { + var errBLECharNoValue *emptyBluetoothCharacteristicError + if errors.As(err, &errBLECharNoValue) { + continue + } + return "", errors.WithMessagef(err, "failed to read %s", description) + } + return v, nil + } +} From 80d09299f2997463a4e66181293bc38fc3fc05b8 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 10:53:36 -0500 Subject: [PATCH 09/37] Remove error check from enableAutoAcceptPairRequest due to asynch nature of method. --- .../provisioning/bluetooth/bluetooth_wifi_provisioner.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 87c36f3..57c7f24 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -113,9 +113,7 @@ func (bwp *BluetoothWiFiProvisioner) stopAdvertisingBLE() error { return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") } -func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() error { - return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") -} +func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() {} func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(networks *AvailableWiFiNetworks) error { return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") From 6ac1d5660c2df8288ffb1bb25e29a5e6e74f66e0 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 11:02:39 -0500 Subject: [PATCH 10/37] Use standard library errors and import custom errors package with prefix used elsewhere in the Agent. --- .../provisioning/bluetooth/bluetooth_wifi_provisioner.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 57c7f24..efc22f0 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -7,7 +7,9 @@ import ( "runtime" "sync" - "github.com/pkg/errors" + "errors" + + errw "github.com/pkg/errors" "go.uber.org/multierr" "go.viam.com/rdk/logging" "go.viam.com/utils" @@ -42,7 +44,7 @@ func (bwp *BluetoothWiFiProvisioner) Start(ctx context.Context) error { if err := bwp.startAdvertisingBLE(ctx); err != nil { return err } - bwp.enableAutoAcceptPairRequest() // Enables auto-accept of pair request on this device. + bwp.enableAutoAcceptPairRequest() // Async goroutine (hence no error check) which auto-accepts pair requests on this device. return nil } @@ -145,6 +147,6 @@ func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, nam case "darwin": fallthrough default: - return nil, errors.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) + return nil, errw.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) } } From 0356d47b02230ba2c6a3607f6861560f65e9656f Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 11:04:49 -0500 Subject: [PATCH 11/37] Pass context one layer lower in call stack to properly protect against sending information to a closed channel. --- .../provisioning/bluetooth/bluetooth_wifi_provisioner.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index efc22f0..a084c3e 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -55,10 +55,7 @@ func (bwp *BluetoothWiFiProvisioner) Stop() error { // Update updates the list of networks that are advertised via bluetooth as available. func (bwp *BluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error { - if ctx.Err() != nil { - return ctx.Err() - } - return bwp.writeAvailableNetworks(awns) + return bwp.writeAvailableNetworks(ctx, awns) } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. @@ -117,7 +114,7 @@ func (bwp *BluetoothWiFiProvisioner) stopAdvertisingBLE() error { func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() {} -func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(networks *AvailableWiFiNetworks) error { +func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(ctx context.Context, networks *AvailableWiFiNetworks) error { return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") } From f362c7440583585c79a7964bf686633833dc71c1 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 12:59:29 -0500 Subject: [PATCH 12/37] Remove unused helper method for custom error type. --- .../bluetooth/bluetooth_wifi_provisioner_utils.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go index 2c8171d..80cd270 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go @@ -17,12 +17,6 @@ func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } -func newEmptyBluetoothCharacteristicError(missingValue string) error { - return &emptyBluetoothCharacteristicError{ - missingValue: missingValue, - } -} - // waitForBLE is used to check for the existence of a new value in a BLE characteristic. func waitForBLEValue( ctx context.Context, fn func() (string, error), description string, From 9a47333b24f006885471223239dd5bc0681827bf Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 15:10:11 -0500 Subject: [PATCH 13/37] Add context cancellation in case of error in WaitForCredentials method and rename waitForBLEValue to retryCallbackOnEmptyCharacteristicError. --- .../bluetooth/bluetooth_wifi_provisioner.go | 28 ++++++++++++++++--- .../bluetooth_wifi_provisioner_utils.go | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index a084c3e..f537cbd 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -60,6 +60,9 @@ func (bwp *BluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Contex // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + if !requiresWiFiCredentials && !requiresCloudCredentials { return nil, errors.New("should be waiting for either cloud credentials or WiFi credentials") } @@ -70,13 +73,22 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req wg.Add(2) utils.ManagedGo( func() { - ssid, ssidErr = waitForBLEValue(ctx, bwp.readSsid, "ssid") + if ssid, ssidErr = retryCallbackOnEmptyCharacteristicError( + ctx, bwp.readSsid, "ssid", + ); ssidErr != nil { + cancel() + } }, wg.Done, ) utils.ManagedGo( func() { - psk, pskErr = waitForBLEValue(ctx, bwp.readPsk, "psk") + if psk, pskErr = retryCallbackOnEmptyCharacteristicError( + ctx, bwp.readPsk, "psk", + ); pskErr != nil { + cancel() + } + }, wg.Done, ) @@ -85,13 +97,21 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req wg.Add(2) utils.ManagedGo( func() { - robotPartKeyID, robotPartKeyIDErr = waitForBLEValue(ctx, bwp.readRobotPartKeyID, "robot part key ID") + if robotPartKeyID, robotPartKeyIDErr = retryCallbackOnEmptyCharacteristicError( + ctx, bwp.readRobotPartKeyID, "robot part key ID", + ); robotPartKeyIDErr != nil { + cancel() + } }, wg.Done, ) utils.ManagedGo( func() { - robotPartKey, robotPartKeyErr = waitForBLEValue(ctx, bwp.readRobotPartKey, "robot part key") + if robotPartKey, robotPartKeyErr = retryCallbackOnEmptyCharacteristicError( + ctx, bwp.readRobotPartKey, "robot part key", + ); robotPartKeyErr != nil { + cancel() + } }, wg.Done, ) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go index 80cd270..078f3b4 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go @@ -18,7 +18,7 @@ func (e *emptyBluetoothCharacteristicError) Error() string { } // waitForBLE is used to check for the existence of a new value in a BLE characteristic. -func waitForBLEValue( +func retryCallbackOnEmptyCharacteristicError( ctx context.Context, fn func() (string, error), description string, ) (string, error) { for { From 59236590df5e71ac6f9cfd80097002278fab8bf2 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 15:13:28 -0500 Subject: [PATCH 14/37] Update subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go --- .../provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go index 078f3b4..a4d6d39 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go @@ -17,7 +17,7 @@ func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } -// waitForBLE is used to check for the existence of a new value in a BLE characteristic. +// retryCallbackOnEmptyCharacteristicError retries the provided callback to at one second intervals as long as an expected error is thrown. func retryCallbackOnEmptyCharacteristicError( ctx context.Context, fn func() (string, error), description string, ) (string, error) { From 16516e3681d7838b152b7fb4ed68bbe7bfbdf542 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 15:18:11 -0500 Subject: [PATCH 15/37] Make retryCallbackOnExpectedError fn much more general. --- .../bluetooth/bluetooth_wifi_provisioner.go | 20 ++++++++++--------- .../bluetooth_wifi_provisioner_utils.go | 11 +++++----- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index f537cbd..a66a90c 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -59,7 +59,9 @@ func (bwp *BluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Contex } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool) (*Credentials, error) { +func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( + ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool, +) (*Credentials, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -73,8 +75,8 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req wg.Add(2) utils.ManagedGo( func() { - if ssid, ssidErr = retryCallbackOnEmptyCharacteristicError( - ctx, bwp.readSsid, "ssid", + if ssid, ssidErr = retryCallbackOnExpectedError( + ctx, bwp.readSsid, &emptyBluetoothCharacteristicError{}, "failed to read ssid", ); ssidErr != nil { cancel() } @@ -83,8 +85,8 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req ) utils.ManagedGo( func() { - if psk, pskErr = retryCallbackOnEmptyCharacteristicError( - ctx, bwp.readPsk, "psk", + if psk, pskErr = retryCallbackOnExpectedError( + ctx, bwp.readPsk, &emptyBluetoothCharacteristicError{}, "failed to read psk", ); pskErr != nil { cancel() } @@ -97,8 +99,8 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req wg.Add(2) utils.ManagedGo( func() { - if robotPartKeyID, robotPartKeyIDErr = retryCallbackOnEmptyCharacteristicError( - ctx, bwp.readRobotPartKeyID, "robot part key ID", + if robotPartKeyID, robotPartKeyIDErr = retryCallbackOnExpectedError( + ctx, bwp.readRobotPartKeyID, &emptyBluetoothCharacteristicError{}, "failed to read robot part key ID", ); robotPartKeyIDErr != nil { cancel() } @@ -107,8 +109,8 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials(ctx context.Context, req ) utils.ManagedGo( func() { - if robotPartKey, robotPartKeyErr = retryCallbackOnEmptyCharacteristicError( - ctx, bwp.readRobotPartKey, "robot part key", + if robotPartKey, robotPartKeyErr = retryCallbackOnExpectedError( + ctx, bwp.readRobotPartKey, &emptyBluetoothCharacteristicError{}, "failed to read robot part key", ); robotPartKeyErr != nil { cancel() } diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go index a4d6d39..8af1163 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go @@ -17,9 +17,9 @@ func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } -// retryCallbackOnEmptyCharacteristicError retries the provided callback to at one second intervals as long as an expected error is thrown. -func retryCallbackOnEmptyCharacteristicError( - ctx context.Context, fn func() (string, error), description string, +// retryCallbackOnExpectedError retries the provided callback to at one second intervals as long as an expected error is thrown. +func retryCallbackOnExpectedError( + ctx context.Context, fn func() (string, error), expectedErr error, description string, ) (string, error) { for { if ctx.Err() != nil { @@ -33,11 +33,10 @@ func retryCallbackOnEmptyCharacteristicError( } v, err := fn() if err != nil { - var errBLECharNoValue *emptyBluetoothCharacteristicError - if errors.As(err, &errBLECharNoValue) { + if errors.As(err, &expectedErr) { continue } - return "", errors.WithMessagef(err, "failed to read %s", description) + return "", errors.WithMessagef(err, "%s", description) } return v, nil } From 74bf5c3a127f801ba124c6c59986f4b976784c77 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 15:21:35 -0500 Subject: [PATCH 16/37] Update subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go --- subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index a66a90c..83e1196 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -36,7 +36,7 @@ func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { return json.Marshal(awns) } -// bluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. +// BluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. type BluetoothWiFiProvisioner struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. From cf43f2852d26eae92ad00741d9276574a918d2db Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 16:49:50 -0500 Subject: [PATCH 17/37] Update subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go --- subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 83e1196..746e0ca 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -70,7 +70,7 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( } var ssid, psk, robotPartKeyID, robotPartKey string var ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error - wg := &sync.WaitGroup{} + wg := sync.WaitGroup{} if requiresWiFiCredentials { wg.Add(2) utils.ManagedGo( From 12afb2805a5da37110f131bc87e27910e30fe81f Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 17:29:00 -0500 Subject: [PATCH 18/37] Update error message TODOs to be more specific. --- go.mod | 2 +- .../bluetooth/bluetooth_wifi_provisioner.go | 33 +++++++++++-------- subsystems/provisioning/definitions.go | 12 +++---- subsystems/provisioning/networkmanager.go | 18 +++++----- subsystems/provisioning/portal.go | 6 ++-- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index fb4275d..35a030d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/sergeymakinen/go-systemdconf/v2 v2.0.2 github.com/ulikunitz/xz v0.5.12 - go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 go.viam.com/api v0.1.357 go.viam.com/rdk v0.51.0 @@ -80,6 +79,7 @@ require ( go.mongodb.org/mongo-driver v1.17.1 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go index 746e0ca..b3a21cd 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go @@ -4,13 +4,12 @@ package ble import ( "context" "encoding/json" + "fmt" "runtime" "sync" "errors" - errw "github.com/pkg/errors" - "go.uber.org/multierr" "go.viam.com/rdk/logging" "go.viam.com/utils" ) @@ -66,7 +65,7 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( defer cancel() if !requiresWiFiCredentials && !requiresCloudCredentials { - return nil, errors.New("should be waiting for either cloud credentials or WiFi credentials") + return nil, errors.New("should be waiting for cloud credentials or WiFi credentials, or both") } var ssid, psk, robotPartKeyID, robotPartKey string var ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error @@ -121,51 +120,59 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( wg.Wait() return &Credentials{ Ssid: ssid, Psk: psk, RobotPartKeyID: robotPartKeyID, RobotPartKey: robotPartKey, - }, multierr.Combine(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) + }, errors.Join(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) } /** Unexported helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ func (bwp *BluetoothWiFiProvisioner) startAdvertisingBLE(ctx context.Context) error { - return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } func (bwp *BluetoothWiFiProvisioner) stopAdvertisingBLE() error { - return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } -func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() {} +func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() { + // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. +} func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(ctx context.Context, networks *AvailableWiFiNetworks) error { - return errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") } func (bwp *BluetoothWiFiProvisioner) readSsid() (string, error) { - return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + + " values from BLE peripheral characteristics") } func (bwp *BluetoothWiFiProvisioner) readPsk() (string, error) { - return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + + " values from BLE peripheral characteristics") } func (bwp *BluetoothWiFiProvisioner) readRobotPartKeyID() (string, error) { - return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + + " values from BLE peripheral characteristics") } func (bwp *BluetoothWiFiProvisioner) readRobotPartKey() (string, error) { - return "", errors.New("TODO APP-7644: Add Linux-specific bluetooth calls for automatic pairing and read/write to BLE characteristics") + return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + + " values from BLE peripheral characteristics") } // NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (*BluetoothWiFiProvisioner, error) { switch os := runtime.GOOS; os { case "linux": + // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE + // to WiFi provisioning. fallthrough case "windows": fallthrough case "darwin": fallthrough default: - return nil, errw.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) + return nil, fmt.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) } } diff --git a/subsystems/provisioning/definitions.go b/subsystems/provisioning/definitions.go index d838743..fe89863 100644 --- a/subsystems/provisioning/definitions.go +++ b/subsystems/provisioning/definitions.go @@ -182,7 +182,7 @@ type CloudConfig struct { Secret string `json:"secret"` } -func WriteDeviceConfig(file string, input userInput) error { +func WriteDeviceConfig(file string, input UserInput) error { if input.RawConfig != "" { return os.WriteFile(file, []byte(input.RawConfig), 0o600) } @@ -206,9 +206,9 @@ type portalData struct { mu sync.Mutex Updated time.Time - inputChan chan<- userInput + inputChan chan<- UserInput - input *userInput + input *UserInput workers sync.WaitGroup // used to cancel background threads @@ -224,7 +224,7 @@ func (p *portalData) sendInput(connState *connectionState) { if (input.SSID != "" && input.PartID != "") || (input.SSID != "" && connState.getConfigured()) || (input.PartID != "" && connState.getOnline()) { - p.input = &userInput{} + p.input = &UserInput{} p.inputChan <- input if p.cancel != nil { p.cancel() @@ -249,12 +249,12 @@ func (p *portalData) sendInput(connState *connectionState) { return case <-time.After(time.Second * 10): } - p.input = &userInput{} + p.input = &UserInput{} p.inputChan <- input }() } -type userInput struct { +type UserInput struct { // network SSID string PSK string diff --git a/subsystems/provisioning/networkmanager.go b/subsystems/provisioning/networkmanager.go index 572276d..3d8c726 100644 --- a/subsystems/provisioning/networkmanager.go +++ b/subsystems/provisioning/networkmanager.go @@ -161,7 +161,7 @@ func (w *Provisioning) checkConnections() error { } // StartProvisioning puts the wifi in hotspot mode and starts a captive portal. -func (w *Provisioning) StartProvisioning(ctx context.Context, inputChan chan<- userInput) error { +func (w *Provisioning) StartProvisioning(ctx context.Context, inputChan chan<- UserInput) error { if w.connState.getProvisioning() { return errors.New("provisioning mode already started") } @@ -575,7 +575,7 @@ func (w *Provisioning) mainLoop(ctx context.Context) { defer w.monitorWorkers.Done() scanChan := make(chan bool, 16) - inputChan := make(chan userInput, 1) + inputChan := make(chan UserInput, 1) w.monitorWorkers.Add(1) go w.backgroundLoop(ctx, scanChan) @@ -586,10 +586,10 @@ func (w *Provisioning) mainLoop(ctx context.Context) { select { case <-ctx.Done(): return - case userInput := <-inputChan: - if userInput.RawConfig != "" || userInput.PartID != "" { + case UserInput := <-inputChan: + if UserInput.RawConfig != "" || UserInput.PartID != "" { w.logger.Info("Device config received") - err := WriteDeviceConfig(w.AppCfgPath, userInput) + err := WriteDeviceConfig(w.AppCfgPath, UserInput) if err != nil { w.errors.Add(err) w.logger.Error(err) @@ -601,16 +601,16 @@ func (w *Provisioning) mainLoop(ctx context.Context) { var newSSID string var changesMade bool - if userInput.SSID != "" { - w.logger.Infof("Wifi settings received for %s", userInput.SSID) + if UserInput.SSID != "" { + w.logger.Infof("Wifi settings received for %s", UserInput.SSID) priority := int32(999) if w.cfg.RoamingMode { priority = 100 } cfg := NetworkConfig{ Type: NetworkTypeWifi, - SSID: userInput.SSID, - PSK: userInput.PSK, + SSID: UserInput.SSID, + PSK: UserInput.PSK, Priority: priority, } var err error diff --git a/subsystems/provisioning/portal.go b/subsystems/provisioning/portal.go index 086a944..c3f6a11 100644 --- a/subsystems/provisioning/portal.go +++ b/subsystems/provisioning/portal.go @@ -29,10 +29,10 @@ type templateData struct { //go:embed templates/* var templates embed.FS -func (w *Provisioning) startPortal(inputChan chan<- userInput) error { +func (w *Provisioning) startPortal(inputChan chan<- UserInput) error { w.dataMu.Lock() defer w.dataMu.Unlock() - w.portalData = &portalData{input: &userInput{}, inputChan: inputChan} + w.portalData = &portalData{input: &UserInput{}, inputChan: inputChan} if err := w.startGRPC(); err != nil { return errw.Wrap(err, "starting GRPC service") @@ -87,7 +87,7 @@ func (w *Provisioning) stopPortal() error { w.portalData.cancel() } w.portalData.workers.Wait() - w.portalData = &portalData{input: &userInput{}} + w.portalData = &portalData{input: &UserInput{}} return err } From 8400c94ad3aacf1e794fe5b3a628c8b53e3c2dc4 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 18 Feb 2025 17:40:16 -0500 Subject: [PATCH 19/37] Move bluetooth wifi provisioner file into provisioning package and remove custom types that are now redundant. --- .../bluetooth_wifi_provisioner_utils.go | 43 -------- .../bluetooth_wifi_provisioner.go | 100 ++++++++++-------- subsystems/provisioning/definitions.go | 12 +-- subsystems/provisioning/networkmanager.go | 18 ++-- subsystems/provisioning/portal.go | 6 +- 5 files changed, 75 insertions(+), 104 deletions(-) delete mode 100644 subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go rename subsystems/provisioning/{bluetooth => }/bluetooth_wifi_provisioner.go (65%) diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go b/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go deleted file mode 100644 index 8af1163..0000000 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner_utils.go +++ /dev/null @@ -1,43 +0,0 @@ -package ble - -import ( - "context" - "fmt" - "time" - - "github.com/pkg/errors" -) - -// emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. -type emptyBluetoothCharacteristicError struct { - missingValue string -} - -func (e *emptyBluetoothCharacteristicError) Error() string { - return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) -} - -// retryCallbackOnExpectedError retries the provided callback to at one second intervals as long as an expected error is thrown. -func retryCallbackOnExpectedError( - ctx context.Context, fn func() (string, error), expectedErr error, description string, -) (string, error) { - for { - if ctx.Err() != nil { - return "", ctx.Err() - } - select { - case <-ctx.Done(): - return "", ctx.Err() - default: - time.Sleep(time.Second) - } - v, err := fn() - if err != nil { - if errors.As(err, &expectedErr) { - continue - } - return "", errors.WithMessagef(err, "%s", description) - } - return v, nil - } -} diff --git a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth_wifi_provisioner.go similarity index 65% rename from subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go rename to subsystems/provisioning/bluetooth_wifi_provisioner.go index b3a21cd..3b49d22 100644 --- a/subsystems/provisioning/bluetooth/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth_wifi_provisioner.go @@ -1,12 +1,11 @@ -// Package ble contains an interface for using bluetooth-low-energy to retrieve WiFi and robot part credentials for an unprovisioned Agent. -package ble +package provisioning import ( "context" - "encoding/json" "fmt" "runtime" "sync" + "time" "errors" @@ -14,32 +13,11 @@ import ( "go.viam.com/utils" ) -// Credentials represent the minimum required information needed to provision a Viam Agent. -type Credentials struct { - Ssid string - Psk string - RobotPartKeyID string - RobotPartKey string -} - -// AvailableWiFiNetworks represent the networks that the device has detected (and which may be available for connection). -type AvailableWiFiNetworks struct { - Networks []*struct { - Ssid string `json:"ssid"` - Strength float64 `json:"strength"` // Inclusive range [0.0, 1.0], represents the % strength of a WiFi network. - RequiresPsk bool `json:"requires_psk"` - } `json:"networks"` -} - -func (awns *AvailableWiFiNetworks) ToBytes() ([]byte, error) { - return json.Marshal(awns) -} - -// BluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. -type BluetoothWiFiProvisioner struct{} +// bluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. +type bluetoothWiFiProvisioner struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *BluetoothWiFiProvisioner) Start(ctx context.Context) error { +func (bwp *bluetoothWiFiProvisioner) Start(ctx context.Context) error { if err := bwp.startAdvertisingBLE(ctx); err != nil { return err } @@ -48,19 +26,19 @@ func (bwp *BluetoothWiFiProvisioner) Start(ctx context.Context) error { } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *BluetoothWiFiProvisioner) Stop() error { +func (bwp *bluetoothWiFiProvisioner) Stop() error { return bwp.stopAdvertisingBLE() } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *BluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Context, awns *AvailableWiFiNetworks) error { +func (bwp *bluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { return bwp.writeAvailableNetworks(ctx, awns) } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( +func (bwp *bluetoothWiFiProvisioner) WaitForCredentials( ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool, -) (*Credentials, error) { +) (*userInput, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -118,51 +96,51 @@ func (bwp *BluetoothWiFiProvisioner) WaitForCredentials( ) } wg.Wait() - return &Credentials{ - Ssid: ssid, Psk: psk, RobotPartKeyID: robotPartKeyID, RobotPartKey: robotPartKey, + return &userInput{ + SSID: ssid, PSK: psk, PartID: robotPartKeyID, Secret: robotPartKey, }, errors.Join(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) } -/** Unexported helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ +/** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ -func (bwp *BluetoothWiFiProvisioner) startAdvertisingBLE(ctx context.Context) error { +func (bwp *bluetoothWiFiProvisioner) startAdvertisingBLE(ctx context.Context) error { return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } -func (bwp *BluetoothWiFiProvisioner) stopAdvertisingBLE() error { +func (bwp *bluetoothWiFiProvisioner) stopAdvertisingBLE() error { return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } -func (bwp *BluetoothWiFiProvisioner) enableAutoAcceptPairRequest() { +func (bwp *bluetoothWiFiProvisioner) enableAutoAcceptPairRequest() { // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. } -func (bwp *BluetoothWiFiProvisioner) writeAvailableNetworks(ctx context.Context, networks *AvailableWiFiNetworks) error { +func (bwp *bluetoothWiFiProvisioner) writeAvailableNetworks(ctx context.Context, networks []*NetworkInfo) error { return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") } -func (bwp *BluetoothWiFiProvisioner) readSsid() (string, error) { +func (bwp *bluetoothWiFiProvisioner) readSsid() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *BluetoothWiFiProvisioner) readPsk() (string, error) { +func (bwp *bluetoothWiFiProvisioner) readPsk() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *BluetoothWiFiProvisioner) readRobotPartKeyID() (string, error) { +func (bwp *bluetoothWiFiProvisioner) readRobotPartKeyID() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *BluetoothWiFiProvisioner) readRobotPartKey() (string, error) { +func (bwp *bluetoothWiFiProvisioner) readRobotPartKey() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } // NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (*BluetoothWiFiProvisioner, error) { +func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (*bluetoothWiFiProvisioner, error) { switch os := runtime.GOOS; os { case "linux": // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE @@ -176,3 +154,39 @@ func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, nam return nil, fmt.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) } } + +/** Custom error type and miscellaneous utils that are helpful for managing low-level bluetooth on Linux **/ + +// emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. +type emptyBluetoothCharacteristicError struct { + missingValue string +} + +func (e *emptyBluetoothCharacteristicError) Error() string { + return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) +} + +// retryCallbackOnExpectedError retries the provided callback to at one second intervals as long as an expected error is thrown. +func retryCallbackOnExpectedError( + ctx context.Context, fn func() (string, error), expectedErr error, description string, +) (string, error) { + for { + if ctx.Err() != nil { + return "", ctx.Err() + } + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + time.Sleep(time.Second) + } + v, err := fn() + if err != nil { + if errors.As(err, &expectedErr) { + continue + } + return "", fmt.Errorf("%w: %s", err, description) + } + return v, nil + } +} diff --git a/subsystems/provisioning/definitions.go b/subsystems/provisioning/definitions.go index fe89863..d838743 100644 --- a/subsystems/provisioning/definitions.go +++ b/subsystems/provisioning/definitions.go @@ -182,7 +182,7 @@ type CloudConfig struct { Secret string `json:"secret"` } -func WriteDeviceConfig(file string, input UserInput) error { +func WriteDeviceConfig(file string, input userInput) error { if input.RawConfig != "" { return os.WriteFile(file, []byte(input.RawConfig), 0o600) } @@ -206,9 +206,9 @@ type portalData struct { mu sync.Mutex Updated time.Time - inputChan chan<- UserInput + inputChan chan<- userInput - input *UserInput + input *userInput workers sync.WaitGroup // used to cancel background threads @@ -224,7 +224,7 @@ func (p *portalData) sendInput(connState *connectionState) { if (input.SSID != "" && input.PartID != "") || (input.SSID != "" && connState.getConfigured()) || (input.PartID != "" && connState.getOnline()) { - p.input = &UserInput{} + p.input = &userInput{} p.inputChan <- input if p.cancel != nil { p.cancel() @@ -249,12 +249,12 @@ func (p *portalData) sendInput(connState *connectionState) { return case <-time.After(time.Second * 10): } - p.input = &UserInput{} + p.input = &userInput{} p.inputChan <- input }() } -type UserInput struct { +type userInput struct { // network SSID string PSK string diff --git a/subsystems/provisioning/networkmanager.go b/subsystems/provisioning/networkmanager.go index 3d8c726..572276d 100644 --- a/subsystems/provisioning/networkmanager.go +++ b/subsystems/provisioning/networkmanager.go @@ -161,7 +161,7 @@ func (w *Provisioning) checkConnections() error { } // StartProvisioning puts the wifi in hotspot mode and starts a captive portal. -func (w *Provisioning) StartProvisioning(ctx context.Context, inputChan chan<- UserInput) error { +func (w *Provisioning) StartProvisioning(ctx context.Context, inputChan chan<- userInput) error { if w.connState.getProvisioning() { return errors.New("provisioning mode already started") } @@ -575,7 +575,7 @@ func (w *Provisioning) mainLoop(ctx context.Context) { defer w.monitorWorkers.Done() scanChan := make(chan bool, 16) - inputChan := make(chan UserInput, 1) + inputChan := make(chan userInput, 1) w.monitorWorkers.Add(1) go w.backgroundLoop(ctx, scanChan) @@ -586,10 +586,10 @@ func (w *Provisioning) mainLoop(ctx context.Context) { select { case <-ctx.Done(): return - case UserInput := <-inputChan: - if UserInput.RawConfig != "" || UserInput.PartID != "" { + case userInput := <-inputChan: + if userInput.RawConfig != "" || userInput.PartID != "" { w.logger.Info("Device config received") - err := WriteDeviceConfig(w.AppCfgPath, UserInput) + err := WriteDeviceConfig(w.AppCfgPath, userInput) if err != nil { w.errors.Add(err) w.logger.Error(err) @@ -601,16 +601,16 @@ func (w *Provisioning) mainLoop(ctx context.Context) { var newSSID string var changesMade bool - if UserInput.SSID != "" { - w.logger.Infof("Wifi settings received for %s", UserInput.SSID) + if userInput.SSID != "" { + w.logger.Infof("Wifi settings received for %s", userInput.SSID) priority := int32(999) if w.cfg.RoamingMode { priority = 100 } cfg := NetworkConfig{ Type: NetworkTypeWifi, - SSID: UserInput.SSID, - PSK: UserInput.PSK, + SSID: userInput.SSID, + PSK: userInput.PSK, Priority: priority, } var err error diff --git a/subsystems/provisioning/portal.go b/subsystems/provisioning/portal.go index c3f6a11..086a944 100644 --- a/subsystems/provisioning/portal.go +++ b/subsystems/provisioning/portal.go @@ -29,10 +29,10 @@ type templateData struct { //go:embed templates/* var templates embed.FS -func (w *Provisioning) startPortal(inputChan chan<- UserInput) error { +func (w *Provisioning) startPortal(inputChan chan<- userInput) error { w.dataMu.Lock() defer w.dataMu.Unlock() - w.portalData = &portalData{input: &UserInput{}, inputChan: inputChan} + w.portalData = &portalData{input: &userInput{}, inputChan: inputChan} if err := w.startGRPC(); err != nil { return errw.Wrap(err, "starting GRPC service") @@ -87,7 +87,7 @@ func (w *Provisioning) stopPortal() error { w.portalData.cancel() } w.portalData.workers.Wait() - w.portalData = &portalData{input: &UserInput{}} + w.portalData = &portalData{input: &userInput{}} return err } From 9377326bbce6d8ef3366823e8077d3dcf6216a86 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 10:44:01 -0500 Subject: [PATCH 20/37] Remove nested goroutines for single goroutine. --- .../bluetooth_wifi_provisioner.go | 128 ++++++++---------- 1 file changed, 57 insertions(+), 71 deletions(-) diff --git a/subsystems/provisioning/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth_wifi_provisioner.go index 3b49d22..83f42c2 100644 --- a/subsystems/provisioning/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth_wifi_provisioner.go @@ -46,59 +46,70 @@ func (bwp *bluetoothWiFiProvisioner) WaitForCredentials( return nil, errors.New("should be waiting for cloud credentials or WiFi credentials, or both") } var ssid, psk, robotPartKeyID, robotPartKey string - var ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error + var ctxErr, ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error + wg := sync.WaitGroup{} - if requiresWiFiCredentials { - wg.Add(2) - utils.ManagedGo( - func() { - if ssid, ssidErr = retryCallbackOnExpectedError( - ctx, bwp.readSsid, &emptyBluetoothCharacteristicError{}, "failed to read ssid", - ); ssidErr != nil { - cancel() - } - }, - wg.Done, - ) - utils.ManagedGo( - func() { - if psk, pskErr = retryCallbackOnExpectedError( - ctx, bwp.readPsk, &emptyBluetoothCharacteristicError{}, "failed to read psk", - ); pskErr != nil { - cancel() - } + wg.Add(1) - }, - wg.Done, - ) - } - if requiresCloudCredentials { - wg.Add(2) - utils.ManagedGo( - func() { - if robotPartKeyID, robotPartKeyIDErr = retryCallbackOnExpectedError( - ctx, bwp.readRobotPartKeyID, &emptyBluetoothCharacteristicError{}, "failed to read robot part key ID", - ); robotPartKeyIDErr != nil { - cancel() + utils.ManagedGo(func() { + for { + if ctxErr = ctx.Err(); ctxErr != nil { + return + } + select { + case <-ctx.Done(): + ctxErr = ctx.Err() + return + default: + if requiresWiFiCredentials { + if ssid == "" { + ssid, ssidErr = bwp.readSsid() + if ssidErr != nil && !errors.As(ssidErr, emptyBluetoothCharacteristicError{}) { + return + } + } + if psk == "" { + psk, pskErr = bwp.readPsk() + if pskErr != nil && !errors.As(pskErr, emptyBluetoothCharacteristicError{}) { + return + } + } } - }, - wg.Done, - ) - utils.ManagedGo( - func() { - if robotPartKey, robotPartKeyErr = retryCallbackOnExpectedError( - ctx, bwp.readRobotPartKey, &emptyBluetoothCharacteristicError{}, "failed to read robot part key", - ); robotPartKeyErr != nil { - cancel() + if requiresCloudCredentials { + if robotPartKeyID == "" { + robotPartKeyID, robotPartKeyIDErr = bwp.readRobotPartKeyID() + if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, emptyBluetoothCharacteristicError{}) { + return + } + } + if robotPartKey == "" { + robotPartKey, robotPartKeyErr = bwp.readRobotPartKey() + if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, emptyBluetoothCharacteristicError{}) { + return + } + } } - }, - wg.Done, - ) - } + if requiresWiFiCredentials && requiresCloudCredentials && + ssid != "" && psk != "" && robotPartKeyID != "" && robotPartKey != "" { + return + } else if requiresWiFiCredentials && ssid != "" && psk != "" { + return + } else if requiresCloudCredentials && robotPartKeyID != "" && robotPartKey != "" { + return + } + + // Not ready to return (do not have the minimum required set of credentials), so sleep and try again. + time.Sleep(time.Second) + } + } + + }, wg.Done) + wg.Wait() + return &userInput{ SSID: ssid, PSK: psk, PartID: robotPartKeyID, Secret: robotPartKey, - }, errors.Join(ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) + }, errors.Join(ctxErr, ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) } /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ @@ -165,28 +176,3 @@ type emptyBluetoothCharacteristicError struct { func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } - -// retryCallbackOnExpectedError retries the provided callback to at one second intervals as long as an expected error is thrown. -func retryCallbackOnExpectedError( - ctx context.Context, fn func() (string, error), expectedErr error, description string, -) (string, error) { - for { - if ctx.Err() != nil { - return "", ctx.Err() - } - select { - case <-ctx.Done(): - return "", ctx.Err() - default: - time.Sleep(time.Second) - } - v, err := fn() - if err != nil { - if errors.As(err, &expectedErr) { - continue - } - return "", fmt.Errorf("%w: %s", err, description) - } - return v, nil - } -} From 0aa914905d95847327bdbb5fbd85b022193aff4f Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 11:26:46 -0500 Subject: [PATCH 21/37] Removed some of the unneccessary layers of abstraction, and added an unexported interface so that the BT functionality can be mocked from provisioning tests. --- .../bluetooth_wifi_provisioner.go | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/subsystems/provisioning/bluetooth_wifi_provisioner.go b/subsystems/provisioning/bluetooth_wifi_provisioner.go index 83f42c2..051f631 100644 --- a/subsystems/provisioning/bluetooth_wifi_provisioner.go +++ b/subsystems/provisioning/bluetooth_wifi_provisioner.go @@ -13,30 +13,28 @@ import ( "go.viam.com/utils" ) -// bluetoothWiFiProvisioner provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. -type bluetoothWiFiProvisioner struct{} +// bluetoothWiFiProvisioningServiceLinux provides an interface for managing BLE (bluetooth-low-energy) peripheral advertisement on Linux. +type bluetoothWiFiProvisioningServiceLinux struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothWiFiProvisioner) Start(ctx context.Context) error { - if err := bwp.startAdvertisingBLE(ctx); err != nil { - return err - } +func (bwp *bluetoothWiFiProvisioningServiceLinux) start(ctx context.Context) error { + // TODO APP-7651: Implement helper methods to start/stop advertising BLE connection bwp.enableAutoAcceptPairRequest() // Async goroutine (hence no error check) which auto-accepts pair requests on this device. - return nil + return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothWiFiProvisioner) Stop() error { - return bwp.stopAdvertisingBLE() +func (bwp *bluetoothWiFiProvisioningServiceLinux) stop() error { + return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *bluetoothWiFiProvisioner) RefreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { - return bwp.writeAvailableNetworks(ctx, awns) +func (bwp *bluetoothWiFiProvisioningServiceLinux) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { + return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *bluetoothWiFiProvisioner) WaitForCredentials( +func (bwp *bluetoothWiFiProvisioningServiceLinux) waitForCredentials( ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool, ) (*userInput, error) { ctx, cancel := context.WithCancel(ctx) @@ -114,44 +112,40 @@ func (bwp *bluetoothWiFiProvisioner) WaitForCredentials( /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ -func (bwp *bluetoothWiFiProvisioner) startAdvertisingBLE(ctx context.Context) error { - return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") -} - -func (bwp *bluetoothWiFiProvisioner) stopAdvertisingBLE() error { - return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") -} - -func (bwp *bluetoothWiFiProvisioner) enableAutoAcceptPairRequest() { +func (bwp *bluetoothWiFiProvisioningServiceLinux) enableAutoAcceptPairRequest() { // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. } -func (bwp *bluetoothWiFiProvisioner) writeAvailableNetworks(ctx context.Context, networks []*NetworkInfo) error { - return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") -} - -func (bwp *bluetoothWiFiProvisioner) readSsid() (string, error) { +func (bwp *bluetoothWiFiProvisioningServiceLinux) readSsid() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothWiFiProvisioner) readPsk() (string, error) { +func (bwp *bluetoothWiFiProvisioningServiceLinux) readPsk() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothWiFiProvisioner) readRobotPartKeyID() (string, error) { +func (bwp *bluetoothWiFiProvisioningServiceLinux) readRobotPartKeyID() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothWiFiProvisioner) readRobotPartKey() (string, error) { +func (bwp *bluetoothWiFiProvisioningServiceLinux) readRobotPartKey() (string, error) { return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -// NewBluetoothWiFiProvisioner returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioner(ctx context.Context, logger logging.Logger, name string) (*bluetoothWiFiProvisioner, error) { +// bluetoothWiFiProvisioningService provides an interface for managing a bluetooth service for provisioning a robot and its WiFi. +type bluetoothWiFiProvisioningService interface { + start(ctx context.Context) error + stop() error + refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) + waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) +} + +// NewBluetoothWiFiProvisioningServiceLinux returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. +func NewBluetoothWiFiProvisioningService(ctx context.Context, logger logging.Logger, name string) (bluetoothWiFiProvisioningService, error) { switch os := runtime.GOOS; os { case "linux": // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE From 325f9b7d9790e89fa3d3afdb8b5da99d45daf423 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 13:13:55 -0500 Subject: [PATCH 22/37] Add nolint tags where necessary to get the code to pass lint (most is unimplemented and left for follow up PRs). --- subsystems/networking/bluetooth.go | 58 ++++++++++++++---------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 914c534..6053925 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -2,48 +2,39 @@ package networking import ( "context" + "errors" "fmt" "runtime" "sync" "time" - "errors" - "go.viam.com/rdk/logging" "go.viam.com/utils" ) -// bluetoothService provides an interface for retrieving cloud config and WiFi credentials for a robot over bluetooth. -type bluetoothService interface { - start(ctx context.Context) error - stop() error - refreshAvailableNetworks(ctx context.Context, availableNetworks []*NetworkInfo) error - waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) -} - -// bluetoothServiceLinux provides methods to retrieve cloud config and WiFi credentials for a robot over bluetooth. -type bluetoothServiceLinux struct{} +// bluetoothService provides methods to retrieve cloud config and/or WiFi credentials for a robot over bluetooth. +type bluetoothService struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothServiceLinux) start(ctx context.Context) error { +func (bwp *bluetoothService) start(ctx context.Context) error { //nolint:unused // TODO APP-7651: Implement helper methods to start/stop advertising BLE connection bwp.enableAutoAcceptPairRequest() // Async goroutine (hence no error check) which auto-accepts pair requests on this device. return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothServiceLinux) stop() error { +func (bwp *bluetoothService) stop() error { //nolint:unused return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *bluetoothServiceLinux) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { +func (bwp *bluetoothService) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { //nolint:unused return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *bluetoothServiceLinux) waitForCredentials( - ctx context.Context, requiresCloudCredentials bool, requiresWiFiCredentials bool, +func (bwp *bluetoothService) waitForCredentials( //nolint:unused + ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, ) (*userInput, error) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -69,33 +60,37 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( default: if requiresWiFiCredentials { if ssid == "" { + var e *emptyBluetoothCharacteristicError ssid, ssidErr = bwp.readSsid() - if ssidErr != nil && !errors.As(ssidErr, emptyBluetoothCharacteristicError{}) { + if ssidErr != nil && !errors.As(ssidErr, &e) { return } } if psk == "" { + var e *emptyBluetoothCharacteristicError psk, pskErr = bwp.readPsk() - if pskErr != nil && !errors.As(pskErr, emptyBluetoothCharacteristicError{}) { + if pskErr != nil && !errors.As(pskErr, &e) { return } } } if requiresCloudCredentials { if robotPartKeyID == "" { + var e *emptyBluetoothCharacteristicError robotPartKeyID, robotPartKeyIDErr = bwp.readRobotPartKeyID() - if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, emptyBluetoothCharacteristicError{}) { + if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, &e) { return } } if robotPartKey == "" { + var e *emptyBluetoothCharacteristicError robotPartKey, robotPartKeyErr = bwp.readRobotPartKey() - if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, emptyBluetoothCharacteristicError{}) { + if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, &e) { return } } } - if requiresWiFiCredentials && requiresCloudCredentials && + if requiresWiFiCredentials && requiresCloudCredentials && //nolint:gocritic ssid != "" && psk != "" && robotPartKeyID != "" && robotPartKey != "" { return } else if requiresWiFiCredentials && ssid != "" && psk != "" { @@ -108,7 +103,6 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( time.Sleep(time.Second) } } - }, wg.Done) wg.Wait() @@ -120,32 +114,32 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ -func (bwp *bluetoothServiceLinux) enableAutoAcceptPairRequest() { +func (bwp *bluetoothService) enableAutoAcceptPairRequest() { //nolint:unused // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. } -func (bwp *bluetoothServiceLinux) readSsid() (string, error) { +func (bwp *bluetoothService) readSsid() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothServiceLinux) readPsk() (string, error) { +func (bwp *bluetoothService) readPsk() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothServiceLinux) readRobotPartKeyID() (string, error) { +func (bwp *bluetoothService) readRobotPartKeyID() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothServiceLinux) readRobotPartKey() (string, error) { +func (bwp *bluetoothService) readRobotPartKey() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -// NewBluetoothWiFiProvisioningServiceLinux returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothWiFiProvisioningService(ctx context.Context, logger logging.Logger, name string) (*bluetoothServiceLinux, error) { +// NewBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. +func NewBluetoothService(ctx context.Context, logger logging.Logger, name string) (*bluetoothService, error) { switch os := runtime.GOOS; os { case "linux": // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE @@ -163,10 +157,10 @@ func NewBluetoothWiFiProvisioningService(ctx context.Context, logger logging.Log /** Custom error type and miscellaneous utils that are helpful for managing low-level bluetooth on Linux **/ // emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. -type emptyBluetoothCharacteristicError struct { +type emptyBluetoothCharacteristicError struct { //nolint:unused missingValue string } -func (e *emptyBluetoothCharacteristicError) Error() string { +func (e *emptyBluetoothCharacteristicError) Error() string { //nolint:unused return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } From 3d36b0aa91491eb388f157cba46ac82792c884c5 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 15:18:48 -0500 Subject: [PATCH 23/37] Add calling code (rebased from the large commit including a refactor for machine settings config updates). --- go.mod | 6 +- go.sum | 25 +++-- subsystems/networking/bluetooth.go | 62 +++++++----- subsystems/networking/networking.go | 11 ++- subsystems/networking/networkmanager.go | 126 ++++++++++++++++++++---- 5 files changed, 163 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index 42223be..025cc7d 100644 --- a/go.mod +++ b/go.mod @@ -16,8 +16,8 @@ require ( go.uber.org/zap v1.27.0 go.viam.com/api v0.1.395 go.viam.com/rdk v0.51.0 - go.viam.com/test v1.2.3 - go.viam.com/utils v0.1.112 + go.viam.com/test v1.2.4 + go.viam.com/utils v0.1.130 golang.org/x/sys v0.28.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 @@ -29,7 +29,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/dgottlieb/smarty-assertions v1.2.5 // indirect + github.com/dgottlieb/smarty-assertions v1.2.6 // indirect github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 // indirect github.com/edaniels/zeroconf v1.0.10 // indirect github.com/goccy/go-json v0.10.2 // indirect diff --git a/go.sum b/go.sum index 2575373..93577e1 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,6 @@ cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842Bg cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= @@ -107,8 +106,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/denis-tingajkin/go-header v0.4.2/go.mod h1:eLRHAVXzE5atsKAnNRDB90WHCFFnBUn4RN0nRcs1LJA= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= -github.com/dgottlieb/smarty-assertions v1.2.5 h1:CQQeh5VM6AVRxJmKlnqusMAqVSm9a3ng60gFD3l4QFU= -github.com/dgottlieb/smarty-assertions v1.2.5/go.mod h1:x1wpV/RTxYWtN+vgrcRuCF4hjUmonK5NR59ZzQSym2k= +github.com/dgottlieb/smarty-assertions v1.2.6 h1:YAXgSslRBbVtd54iTqM4yGT2k1a2qS6cffNQo0SDxDY= +github.com/dgottlieb/smarty-assertions v1.2.6/go.mod h1:x1wpV/RTxYWtN+vgrcRuCF4hjUmonK5NR59ZzQSym2k= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -700,12 +699,12 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -732,10 +731,10 @@ go.viam.com/api v0.1.395 h1:I7TzCBG0HCFOxkH+i9w+XI0R2+3nfQVaaV2KWzcdQXQ= go.viam.com/api v0.1.395/go.mod h1:drvlBWaiHFxPziz5jayHvibez1qG7lylcNCC1LF8onU= go.viam.com/rdk v0.51.0 h1:1SFf4wVY5wNbXRDUC0GsBVmXikD74Tu5PHLJXngN8oA= go.viam.com/rdk v0.51.0/go.mod h1:MobIDjs3EFbwwmQE+b0oVdToUTguNMFIWdttzEGoDE4= -go.viam.com/test v1.2.3 h1:tT2QqthC2BL2tiloUC2T1AIwuLILyMRx8mmxunN+cT4= -go.viam.com/test v1.2.3/go.mod h1:5pXMnEyvTygilOCaFtonnKNMqsCCBbe2ZXU8ZsJ2zjY= -go.viam.com/utils v0.1.112 h1:yuVkNITUijdP/CMI3BaDozUMZwP4Ari57BvRQfORFK0= -go.viam.com/utils v0.1.112/go.mod h1:SYvcY/TKy9yv1d95era4IEehImkXffWu/5diDBS/4X4= +go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug= +go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI= +go.viam.com/utils v0.1.130 h1:5vggHNK/ar/YHXjf9z5WuihX+n9jYFnA2CEFOYi99d8= +go.viam.com/utils v0.1.130/go.mod h1:g1CaEkf7aJCrSI/Sfkx+6cBS1+Y3fPT2pvMQbp7TTBI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 6053925..3e0bff9 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -12,28 +12,52 @@ import ( "go.viam.com/utils" ) -// bluetoothService provides methods to retrieve cloud config and/or WiFi credentials for a robot over bluetooth. -type bluetoothService struct{} +// bluetoothService provides an interface for retrieving cloud config and/or WiFi credentials for a robot over bluetooth. +type bluetoothService interface { + start(ctx context.Context) error + stop() error + refreshAvailableNetworks(ctx context.Context, availableNetworks []NetworkInfo) error + waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) +} + +// newBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. +func newBluetoothService(ctx context.Context, logger logging.Logger, name string) (bluetoothService, error) { + switch os := runtime.GOOS; os { + case "linux": + // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE + // to WiFi provisioning. + fallthrough + case "windows": + fallthrough + case "darwin": + fallthrough + default: + return nil, fmt.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) + } +} + +// bluetoothServiceLinux provides methods to retrieve cloud config and/or WiFi credentials for a robot over bluetooth. +type bluetoothServiceLinux struct{} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothService) start(ctx context.Context) error { //nolint:unused +func (bwp *bluetoothServiceLinux) start(ctx context.Context) error { //nolint:unused // TODO APP-7651: Implement helper methods to start/stop advertising BLE connection bwp.enableAutoAcceptPairRequest() // Async goroutine (hence no error check) which auto-accepts pair requests on this device. return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothService) stop() error { //nolint:unused +func (bwp *bluetoothServiceLinux) stop() error { //nolint:unused return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *bluetoothService) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { //nolint:unused +func (bwp *bluetoothServiceLinux) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { //nolint:unused return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *bluetoothService) waitForCredentials( //nolint:unused +func (bwp *bluetoothServiceLinux) waitForCredentials( //nolint:unused ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, ) (*userInput, error) { ctx, cancel := context.WithCancel(ctx) @@ -114,46 +138,30 @@ func (bwp *bluetoothService) waitForCredentials( //nolint:unused /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ -func (bwp *bluetoothService) enableAutoAcceptPairRequest() { //nolint:unused +func (bwp *bluetoothServiceLinux) enableAutoAcceptPairRequest() { //nolint:unused // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. } -func (bwp *bluetoothService) readSsid() (string, error) { //nolint:unused +func (bwp *bluetoothServiceLinux) readSsid() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothService) readPsk() (string, error) { //nolint:unused +func (bwp *bluetoothServiceLinux) readPsk() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothService) readRobotPartKeyID() (string, error) { //nolint:unused +func (bwp *bluetoothServiceLinux) readRobotPartKeyID() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -func (bwp *bluetoothService) readRobotPartKey() (string, error) { //nolint:unused +func (bwp *bluetoothServiceLinux) readRobotPartKey() (string, error) { //nolint:unused return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + " values from BLE peripheral characteristics") } -// NewBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func NewBluetoothService(ctx context.Context, logger logging.Logger, name string) (*bluetoothService, error) { - switch os := runtime.GOOS; os { - case "linux": - // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE - // to WiFi provisioning. - fallthrough - case "windows": - fallthrough - case "darwin": - fallthrough - default: - return nil, fmt.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) - } -} - /** Custom error type and miscellaneous utils that are helpful for managing low-level bluetooth on Linux **/ // emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. diff --git a/subsystems/networking/networking.go b/subsystems/networking/networking.go index 7313c1b..cd45e3c 100644 --- a/subsystems/networking/networking.go +++ b/subsystems/networking/networking.go @@ -50,9 +50,14 @@ type Networking struct { nets utils.AdditionalNetworks // portal - webServer *http.Server - grpcServer *grpc.Server - portalData *portalData + webServer *http.Server + grpcServer *grpc.Server + portalData *portalData + hotspotIsActive bool + + // bluetooth + bluetoothService bluetoothService + bluetoothIsActive bool pb.UnimplementedProvisioningServiceServer } diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 0437ceb..cbeda3f 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -3,15 +3,18 @@ package networking import ( "context" "errors" + "fmt" "os" "os/exec" "reflect" "sort" + "sync" "time" gnm "github.com/Otterverse/gonetworkmanager/v2" errw "github.com/pkg/errors" "github.com/viamrobotics/agent/utils" + goutils "go.viam.com/utils" ) func (n *Networking) warnIfMultiplePrimaryNetworks() { @@ -172,23 +175,72 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use n.opMu.Lock() defer n.opMu.Unlock() - n.logger.Info("Starting provisioning mode.") - _, err := n.addOrUpdateConnection(utils.NetworkDefinition{ - Type: NetworkTypeHotspot, - Interface: n.Config().HotspotInterface, - SSID: n.Config().HotspotSSID, - }) - if err != nil { - return err - } - if err := n.activateConnection(ctx, n.Config().HotspotInterface, n.Config().HotspotSSID); err != nil { - return errw.Wrap(err, "starting provisioning mode hotspot") - } + n.logger.Info("Starting provisioning mode. Attempting to set up both hotspot and bluetooth provisioning methods...") + + // Simultaneously start both the hotspot captive portal and the bluetooth service provisioning methods. + wg := sync.WaitGroup{} + + wg.Add(1) + var hotspotErr error + goutils.ManagedGo(func() { + _, err := n.addOrUpdateConnection(utils.NetworkDefinition{ + Type: NetworkTypeHotspot, + Interface: n.Config().HotspotInterface, + SSID: n.Config().HotspotSSID, + }) + if err != nil { + hotspotErr = err + return + } + if err := n.activateConnection(ctx, n.Config().HotspotInterface, n.Config().HotspotSSID); err != nil { + hotspotErr = errw.Wrap(err, "starting provisioning mode hotspot") + return + } - // start portal with ssid list and known connections - if err := n.startPortal(inputChan); err != nil { - err = errors.Join(err, n.deactivateConnection(n.Config().HotspotInterface, n.Config().HotspotSSID)) - return errw.Wrap(err, "starting web/grpc portal") + // start portal with ssid list and known connections + if err := n.startPortal(inputChan); err != nil { + err = errors.Join(err, n.deactivateConnection(n.Config().HotspotInterface, n.Config().HotspotSSID)) + hotspotErr = errw.Wrap(err, "starting web/grpc portal") + return + } + n.hotspotIsActive = true + }, wg.Done) + + wg.Add(1) + var bluetoothErr error + goutils.ManagedGo(func() { + if n.bluetoothService == nil { + bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) + if err != nil { + bluetoothErr = err + return + } + n.bluetoothService = bt + } + if err := n.bluetoothService.start(ctx); err != nil { + bluetoothErr = err + return + } + goutils.ManagedGo(func() { + userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. + if err != nil { + bluetoothErr = err + return + } + inputChan <- *userInput + }, nil) + n.bluetoothIsActive = true + }, wg.Done) + wg.Wait() + + switch { + case !n.hotspotIsActive && !n.bluetoothIsActive: + return fmt.Errorf("failed to set up provisioning: %w", errors.Join(hotspotErr, bluetoothErr)) + case !n.hotspotIsActive && n.bluetoothIsActive: + n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %w", hotspotErr) + case n.hotspotIsActive && !n.bluetoothIsActive: + n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %w", bluetoothErr) + default: } n.connState.setProvisioning(true) @@ -204,12 +256,44 @@ func (n *Networking) StopProvisioning() error { func (n *Networking) stopProvisioning() error { n.logger.Info("Stopping provisioning mode.") n.connState.setProvisioning(false) - err := n.stopPortal() - err2 := n.deactivateConnection(n.Config().HotspotInterface, n.Config().HotspotSSID) - if errors.Is(err2, ErrNoActiveConnectionFound) { - return err + + wg := sync.WaitGroup{} + var hotspotErr error + if n.hotspotIsActive { + wg.Add(1) + goutils.ManagedGo(func() { + err := n.stopPortal() + err2 := n.deactivateConnection(n.Config().HotspotInterface, n.Config().HotspotSSID) + if errors.Is(err2, ErrNoActiveConnectionFound) { + hotspotErr = err + return + } + if combinedErr := errors.Join(err, err2); combinedErr != nil { + hotspotErr = combinedErr + return + } + n.logger.Info("Stopped hotspot and captive portal.") + }, wg.Done) + } + var bluetoothErr error + if n.bluetoothIsActive { + wg.Add(1) + goutils.ManagedGo(func() { + if n.bluetoothService == nil { + bluetoothErr = errors.New("failure to stop bluetooth service " + + "expecting bluetooth service, but pointer is nil") + return + } + if err := n.bluetoothService.stop(); err != nil { + bluetoothErr = err + return + } + n.logger.Info("Stopped bluetooth service.") + }, wg.Done) } - return errors.Join(err, err2) + wg.Wait() + + return errors.Join(hotspotErr, bluetoothErr) } func (n *Networking) ActivateConnection(ctx context.Context, ifName, ssid string) error { From e8d43e6daa0c2abf697028b0ef31b7a20624882a Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 16:18:10 -0500 Subject: [PATCH 24/37] Add commits from other PRs. --- go.mod | 1 + go.sum | 2 + subsystems/networking/bluetooth.go | 581 ++++++++++++++++++++++++--- subsystems/networking/definitions.go | 12 +- 4 files changed, 544 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 025cc7d..61cea64 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 // indirect github.com/edaniels/zeroconf v1.0.10 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 93577e1..bd10dbf 100644 --- a/go.sum +++ b/go.sum @@ -185,6 +185,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= +github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 3e0bff9..90b3c54 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -1,13 +1,20 @@ package networking import ( + "bytes" "context" + "encoding/json" "errors" "fmt" + "os/exec" "runtime" + "strconv" + "strings" "sync" "time" + "github.com/godbus/dbus" + "github.com/google/uuid" "go.viam.com/rdk/logging" "go.viam.com/utils" ) @@ -16,53 +23,283 @@ import ( type bluetoothService interface { start(ctx context.Context) error stop() error - refreshAvailableNetworks(ctx context.Context, availableNetworks []NetworkInfo) error + refreshAvailableNetworks(ctx context.Context, availableNetworks []*NetworkInfo) error waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) } // newBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. func newBluetoothService(ctx context.Context, logger logging.Logger, name string) (bluetoothService, error) { - switch os := runtime.GOOS; os { - case "linux": - // TODO APP-7654: Implement initializer function for creating a BLE peripheral with the required set of characteristics for BLE - // to WiFi provisioning. - fallthrough - case "windows": - fallthrough - case "darwin": - fallthrough - default: - return nil, fmt.Errorf("failed to set up bluetooth-low-energy peripheral, %s is not yet supported", os) + if err := validateSystem(logger); err != nil { + return nil, fmt.Errorf("cannot initialize bluetooth peripheral, system requisites not met: %w", err) } + + adapter := bluetooth.DefaultAdapter + if err := adapter.Enable(); err != nil { + return nil, fmt.Errorf("failed to enable bluetooth adapter: %w", err) + } + + serviceUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x1111) + logger.Infof("serviceUUID: %s", serviceUUID.String()) + charSsidUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x2222) + logger.Infof("charSsidUUID: %s", charSsidUUID.String()) + charPskUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x3333) + logger.Infof("charPskUUID: %s", charPskUUID.String()) + charRobotPartKeyIDUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x4444) + logger.Infof("charRobotPartKeyIDUUID: %s", charRobotPartKeyIDUUID.String()) + charRobotPartKeyUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x5555) + logger.Infof("charRobotPartKeyUUID: %s", charRobotPartKeyUUID.String()) + charAppAddressUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x6666) + logger.Infof("charAppAddress: %s", charAppAddressUUID.String()) + charAvailableWiFiNetworksUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x7777) + logger.Infof("charAvailableWiFiNetworksUUID: %s", charAvailableWiFiNetworksUUID.String()) + + // Create abstracted characteristics which act as a buffer for reading data from bluetooth. + charSsid := &bluetoothCharacteristicLinux[*string]{ + UUID: charSsidUUID, + mu: &sync.Mutex{}, + currentValue: nil, + } + charPsk := &bluetoothCharacteristicLinux[*string]{ + UUID: charPskUUID, + mu: &sync.Mutex{}, + currentValue: nil, + } + charRobotPartKeyID := &bluetoothCharacteristicLinux[*string]{ + UUID: charRobotPartKeyIDUUID, + mu: &sync.Mutex{}, + currentValue: nil, + } + charRobotPartKey := &bluetoothCharacteristicLinux[*string]{ + UUID: charRobotPartKeyUUID, + mu: &sync.Mutex{}, + currentValue: nil, + } + charAppAddress := &bluetoothCharacteristicLinux[*string]{ + UUID: charAppAddressUUID, + mu: &sync.Mutex{}, + currentValue: nil, + } + + // Create write-only, locking characteristics (one per credential) for fields that are written to. + charConfigSsid := bluetooth.CharacteristicConfig{ + UUID: charSsidUUID, + Flags: bluetooth.CharacteristicWritePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + v := string(value) + logger.Infof("Received SSID: %s", v) + charSsid.mu.Lock() + defer charSsid.mu.Unlock() + charSsid.currentValue = &v + }, + } + charConfigPsk := bluetooth.CharacteristicConfig{ + UUID: charPskUUID, + Flags: bluetooth.CharacteristicWritePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + v := string(value) + logger.Infof("Received Passkey: %s", v) + charPsk.mu.Lock() + defer charPsk.mu.Unlock() + charPsk.currentValue = &v + }, + } + charConfigRobotPartKeyID := bluetooth.CharacteristicConfig{ + UUID: charRobotPartKeyIDUUID, + Flags: bluetooth.CharacteristicWritePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + v := string(value) + logger.Infof("Received Robot Part Key ID: %s", v) + charRobotPartKeyID.mu.Lock() + defer charRobotPartKeyID.mu.Unlock() + charRobotPartKeyID.currentValue = &v + }, + } + charConfigRobotPartKey := bluetooth.CharacteristicConfig{ + UUID: charRobotPartKeyUUID, + Flags: bluetooth.CharacteristicWritePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + v := string(value) + logger.Infof("Received Robot Part Key: %s", v) + charRobotPartKey.mu.Lock() + defer charRobotPartKey.mu.Unlock() + charRobotPartKey.currentValue = &v + }, + } + charConfigAppAddress := bluetooth.CharacteristicConfig{ + UUID: charAppAddressUUID, + Flags: bluetooth.CharacteristicWritePermission, + WriteEvent: func(client bluetooth.Connection, offset int, value []byte) { + v := string(value) + logger.Infof("Received App Address: %s", v) + charAppAddress.mu.Lock() + defer charAppAddress.mu.Unlock() + charAppAddress.currentValue = &v + }, + } + + // Create a read-only characteristic for broadcasting nearby, available WiFi networks. + charConfigAvailableWiFiNetworks := bluetooth.CharacteristicConfig{ + UUID: charAvailableWiFiNetworksUUID, + Flags: bluetooth.CharacteristicReadPermission, + Value: nil, // This will get filled in via calls to UpdateAvailableWiFiNetworks. + WriteEvent: nil, // This characteristic is read-only. + } + + // Channel will be written to by interface method UpdateAvailableWiFiNetworks and will be read by + // the following background goroutine + availableWiFiNetworksChannel := make(chan []*NetworkInfo, 1) + + // Read only channel used to listen for updates to the availableWiFiNetworks. + var availableWiFiNetworksChannelReadOnly <-chan []*NetworkInfo = availableWiFiNetworksChannel + utils.ManagedGo(func() { + defer close(availableWiFiNetworksChannel) + for { + if err := ctx.Err(); err != nil { + return + } + select { + case <-ctx.Done(): + return + case availableNetworks := <-availableWiFiNetworksChannelReadOnly: + var all [][]byte + for _, availableNetwork := range availableNetworks { + single, err := json.Marshal(availableNetwork) + if err != nil { + logger.Errorw("failed to convert available WiFi network to bytes", "err", err) + continue + } + all = append(all, single) + } + charConfigAvailableWiFiNetworks.Value = bytes.Join(all, []byte(",")) + logger.Infow("successfully updated available WiFi networks on bluetooth characteristic") + default: + time.Sleep(time.Second) + } + } + }, nil) + + // Create service which will advertise each of the above characteristics. + s := &bluetooth.Service{ + UUID: serviceUUID, + Characteristics: []bluetooth.CharacteristicConfig{ + charConfigSsid, + charConfigPsk, + charConfigRobotPartKeyID, + charConfigRobotPartKey, + charConfigAvailableWiFiNetworks, + }, + } + if err := adapter.AddService(s); err != nil { + return nil, fmt.Errorf("unable to add bluetooth service to default adapter: %w", err) + } + if err := adapter.Enable(); err != nil { + return nil, fmt.Errorf("failed to enable bluetooth adapter: %w", err) + } + defaultAdvertisement := adapter.DefaultAdvertisement() + if defaultAdvertisement == nil { + return nil, errors.New("default advertisement is nil") + } + if err := defaultAdvertisement.Configure( + bluetooth.AdvertisementOptions{ + LocalName: name, + ServiceUUIDs: []bluetooth.UUID{serviceUUID}, + }, + ); err != nil { + return nil, fmt.Errorf("failed to configure default advertisement: %w", err) + } + return &bluetoothServiceLinux{ + logger: logger, + mu: &sync.Mutex{}, + + adv: defaultAdvertisement, + advActive: false, + UUID: serviceUUID, + + availableWiFiNetworksChannelWriteOnly: availableWiFiNetworksChannel, + + characteristicSsid: charSsid, + characteristicPsk: charPsk, + characteristicRobotPartKeyID: charRobotPartKeyID, + characteristicRobotPartKey: charRobotPartKey, + characteristicAppAddress: charAppAddress, + }, nil +} + +// bluetoothCharacteristicLinux is used to read and write values to a bluetooh peripheral. +type bluetoothCharacteristicLinux[T any] struct { + UUID bluetooth.UUID + mu *sync.Mutex + + currentValue T } // bluetoothServiceLinux provides methods to retrieve cloud config and/or WiFi credentials for a robot over bluetooth. -type bluetoothServiceLinux struct{} +type bluetoothServiceLinux struct { + logger logging.Logger + mu *sync.Mutex + + adv *bluetooth.Advertisement + advActive bool + UUID bluetooth.UUID + + availableWiFiNetworksChannelWriteOnly chan<- []*NetworkInfo + + characteristicSsid *bluetoothCharacteristicLinux[*string] + characteristicPsk *bluetoothCharacteristicLinux[*string] + characteristicRobotPartKeyID *bluetoothCharacteristicLinux[*string] + characteristicRobotPartKey *bluetoothCharacteristicLinux[*string] + characteristicAppAddress *bluetoothCharacteristicLinux[*string] +} // Start begins advertising a bluetooth service that acccepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothServiceLinux) start(ctx context.Context) error { //nolint:unused - // TODO APP-7651: Implement helper methods to start/stop advertising BLE connection - bwp.enableAutoAcceptPairRequest() // Async goroutine (hence no error check) which auto-accepts pair requests on this device. - return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") +func (bsl *bluetoothServiceLinux) start(ctx context.Context) error { + bsl.mu.Lock() + defer bsl.mu.Unlock() + defer enableAutoAcceptPairRequest(bsl.logger) // Async (logs instead of error checks) to auto-accept pair requests on this device. + + if bsl.adv == nil { + return errors.New("advertisement is nil") + } + if bsl.advActive { + return errors.New("invalid request, advertising already active") + } + if err := bsl.adv.Start(); err != nil { + return fmt.Errorf("failed to start advertising: %w", err) + } + bsl.advActive = true + bsl.logger.Info("started advertising a BLE connection...") + return nil } // Stop stops advertising a bluetooth service which (when enabled) accepts WiFi and Viam cloud config credentials. -func (bwp *bluetoothServiceLinux) stop() error { //nolint:unused - return errors.New("TODO APP-7651: Implement helper methods to start/stop advertising BLE connection") +func (bsl *bluetoothServiceLinux) stop() error { + bsl.mu.Lock() + defer bsl.mu.Unlock() + + if bsl.adv == nil { + return errors.New("advertisement is nil") + } + if !bsl.advActive { + return errors.New("invalid request, advertising already inactive") + } + if err := bsl.adv.Stop(); err != nil { + return fmt.Errorf("failed to stop advertising: %w", err) + } + bsl.advActive = false + bsl.logger.Info("stopped advertising a BLE connection") + return nil } // Update updates the list of networks that are advertised via bluetooth as available. -func (bwp *bluetoothServiceLinux) refreshAvailableNetworks(ctx context.Context, awns []*NetworkInfo) error { //nolint:unused - return errors.New("TODO APP-7652: Implement helper method to write update WiFi networks to BLE peripheral characteristic") +func (bsl *bluetoothServiceLinux) refreshAvailableNetworks(ctx context.Context, availableNetworks []*NetworkInfo) error { + bsl.availableWiFiNetworksChannelWriteOnly <- availableNetworks + return nil } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. -func (bwp *bluetoothServiceLinux) waitForCredentials( //nolint:unused +func (bsl *bluetoothServiceLinux) waitForCredentials( ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, ) (*userInput, error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - if !requiresWiFiCredentials && !requiresCloudCredentials { return nil, errors.New("should be waiting for cloud credentials or WiFi credentials, or both") } @@ -85,14 +322,14 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( //nolint:unused if requiresWiFiCredentials { if ssid == "" { var e *emptyBluetoothCharacteristicError - ssid, ssidErr = bwp.readSsid() + ssid, ssidErr = bsl.readSsid() if ssidErr != nil && !errors.As(ssidErr, &e) { return } } if psk == "" { var e *emptyBluetoothCharacteristicError - psk, pskErr = bwp.readPsk() + psk, pskErr = bsl.readPsk() if pskErr != nil && !errors.As(pskErr, &e) { return } @@ -101,14 +338,14 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( //nolint:unused if requiresCloudCredentials { if robotPartKeyID == "" { var e *emptyBluetoothCharacteristicError - robotPartKeyID, robotPartKeyIDErr = bwp.readRobotPartKeyID() + robotPartKeyID, robotPartKeyIDErr = bsl.readRobotPartKeyID() if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, &e) { return } } if robotPartKey == "" { var e *emptyBluetoothCharacteristicError - robotPartKey, robotPartKeyErr = bwp.readRobotPartKey() + robotPartKey, robotPartKeyErr = bsl.readRobotPartKey() if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, &e) { return } @@ -138,37 +375,289 @@ func (bwp *bluetoothServiceLinux) waitForCredentials( //nolint:unused /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ -func (bwp *bluetoothServiceLinux) enableAutoAcceptPairRequest() { //nolint:unused - // TODO APP-7655: Implement method to auto-accept pairing requests to the BLE peripheral. -} +func (bsl *bluetoothServiceLinux) readSsid() (string, error) { + if bsl.characteristicSsid == nil { + return "", errors.New("characteristic ssid is nil") + } -func (bwp *bluetoothServiceLinux) readSsid() (string, error) { //nolint:unused - return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + - " values from BLE peripheral characteristics") + bsl.characteristicSsid.mu.Lock() + defer bsl.characteristicSsid.mu.Unlock() + + if bsl.characteristicSsid.currentValue == nil { + return "", newEmptyBluetoothCharacteristicError("ssid") + } + return *bsl.characteristicSsid.currentValue, nil } -func (bwp *bluetoothServiceLinux) readPsk() (string, error) { //nolint:unused - return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + - " values from BLE peripheral characteristics") +func (bsl *bluetoothServiceLinux) readPsk() (string, error) { + if bsl.characteristicPsk == nil { + return "", errors.New("characteristic psk is nil") + } + + bsl.characteristicPsk.mu.Lock() + defer bsl.characteristicPsk.mu.Unlock() + + if bsl.characteristicPsk.currentValue == nil { + return "", newEmptyBluetoothCharacteristicError("psk") + } + return *bsl.characteristicPsk.currentValue, nil } -func (bwp *bluetoothServiceLinux) readRobotPartKeyID() (string, error) { //nolint:unused - return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + - " values from BLE peripheral characteristics") +func (bsl *bluetoothServiceLinux) readRobotPartKeyID() (string, error) { + if bsl.characteristicRobotPartKeyID == nil { + return "", errors.New("characteristic robot part key ID is nil") + } + + bsl.characteristicRobotPartKeyID.mu.Lock() + defer bsl.characteristicRobotPartKeyID.mu.Unlock() + + if bsl.characteristicRobotPartKeyID.currentValue == nil { + return "", newEmptyBluetoothCharacteristicError("robot part key ID") + } + return *bsl.characteristicRobotPartKeyID.currentValue, nil } -func (bwp *bluetoothServiceLinux) readRobotPartKey() (string, error) { //nolint:unused - return "", errors.New("TODO APP-7653: Implement helper methods to read SSID, passkey, robot part key ID, and robot part key" + - " values from BLE peripheral characteristics") +func (bsl *bluetoothServiceLinux) readRobotPartKey() (string, error) { + if bsl.characteristicRobotPartKey == nil { + return "", errors.New("characteristic robot part key is nil") + } + + bsl.characteristicRobotPartKey.mu.Lock() + defer bsl.characteristicRobotPartKey.mu.Unlock() + + if bsl.characteristicRobotPartKey.currentValue == nil { + return "", newEmptyBluetoothCharacteristicError("robot part key") + } + return *bsl.characteristicRobotPartKey.currentValue, nil } /** Custom error type and miscellaneous utils that are helpful for managing low-level bluetooth on Linux **/ // emptyBluetoothCharacteristicError represents the error which is raised when we attempt to read from an empty BLE characteristic. -type emptyBluetoothCharacteristicError struct { //nolint:unused +type emptyBluetoothCharacteristicError struct { missingValue string } -func (e *emptyBluetoothCharacteristicError) Error() string { //nolint:unused +func (e *emptyBluetoothCharacteristicError) Error() string { return fmt.Sprintf("no value has been written to BLE characteristic for %s", e.missingValue) } + +func newEmptyBluetoothCharacteristicError(missingValue string) error { + return &emptyBluetoothCharacteristicError{ + missingValue: missingValue, + } +} + +const ( + BluezDBusService = "org.bluez" + BluezAgentPath = "/custom/agent" + BluezAgentManager = "org.bluez.AgentManager1" + BluezAgent = "org.bluez.Agent1" +) + +// checkOS verifies the system is running a Linux distribution. +func checkOS() error { + if runtime.GOOS != "linux" { + return fmt.Errorf("this program requires Linux, detected: %s", runtime.GOOS) + } + return nil +} + +// getBlueZVersion retrieves the installed BlueZ version and extracts the numeric value correctly. +func getBlueZVersion() (float64, error) { + // Try to get version from bluetoothctl first, fallback to bluetoothd + versionCmds := []string{"bluetoothctl --version", "bluetoothd --version"} + + var versionOutput bytes.Buffer + var err error + + for _, cmd := range versionCmds { + versionOutput.Reset() // Clear buffer + cmdParts := strings.Fields(cmd) + execCmd := exec.Command(cmdParts[0], cmdParts[1:]...) //nolint:gosec + execCmd.Stdout = &versionOutput + err = execCmd.Run() + if err == nil { + break // Found a valid command + } + } + + if err != nil { + return 0, fmt.Errorf("BlueZ is not installed or not accessible") + } + + // Extract only the numeric version + versionStr := strings.TrimSpace(versionOutput.String()) + parts := strings.Fields(versionStr) + + // Ensure we have at least one part before accessing it + if len(parts) == 0 { + return 0, fmt.Errorf("failed to parse BlueZ version: empty output") + } + + versionNum := parts[len(parts)-1] // Get the last word, which should be the version number + + // Convert to float + versionFloat, err := strconv.ParseFloat(versionNum, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse BlueZ version: %s", versionStr) + } + + return versionFloat, nil +} + +// validateSystem checks OS and BlueZ installation/version. +func validateSystem(logger logging.Logger) error { + // 1. Validate OS + if err := checkOS(); err != nil { + return err + } + logger.Info("✅ Running on a Linux system.") + + // 2. Check BlueZ version + blueZVersion, err := getBlueZVersion() + if err != nil { + return err + } + logger.Infof("✅ BlueZ detected, version: %.2f", blueZVersion) + + // 3. Validate BlueZ version is 5.66 or higher + if blueZVersion < 5.66 { + return fmt.Errorf("❌ BlueZ version is %.2f, but 5.66 or later is required", blueZVersion) + } + + logger.Info("✅ BlueZ version meets the requirement (5.66 or later).") + return nil +} + +// trustDevice sets the device as trusted and connects to it. +func trustDevice(logger logging.Logger, devicePath string) error { + conn, err := dbus.SystemBus() + if err != nil { + return fmt.Errorf("failed to connect to DBus: %w", err) + } + + obj := conn.Object(BluezDBusService, dbus.ObjectPath(devicePath)) + + // Set Trusted = true + call := obj.Call("org.freedesktop.DBus.Properties.Set", 0, + "org.bluez.Device1", "Trusted", dbus.MakeVariant(true)) + if call.Err != nil { + return fmt.Errorf("failed to set Trusted property: %w", call.Err) + } + logger.Info("device marked as trusted.") + + return nil +} + +// convertDBusPathToMAC converts a DBus object path to a Bluetooth MAC address. +func convertDBusPathToMAC(path string) string { + parts := strings.Split(path, "/") + if len(parts) < 4 { + return "" + } + + // Extract last part and convert underscores to colons + macPart := parts[len(parts)-1] + mac := strings.ReplaceAll(macPart, "_", ":") + return mac +} + +func enableAutoAcceptPairRequest(logger logging.Logger) { + var err error + utils.ManagedGo(func() { + conn, err := dbus.SystemBus() + if err != nil { + err = fmt.Errorf("failed to connect to system DBus: %w", err) + return + } + + // Export agent methods + reply := conn.Export(nil, BluezAgentPath, BluezAgent) + if reply != nil { + err = fmt.Errorf("failed to export Bluez agent: %w", reply) + return + } + + // Register the agent + obj := conn.Object(BluezDBusService, "/org/bluez") + call := obj.Call("org.bluez.AgentManager1.RegisterAgent", 0, dbus.ObjectPath(BluezAgentPath), "NoInputNoOutput") + if err := call.Err; err != nil { + err = fmt.Errorf("failed to register Bluez agent: %w", err) + return + } + + // Set as the default agent + call = obj.Call("org.bluez.AgentManager1.RequestDefaultAgent", 0, dbus.ObjectPath(BluezAgentPath)) + if err := call.Err; err != nil { + err = fmt.Errorf("failed to set default Bluez agent: %w", err) + return + } + + logger.Info("Bluez agent registered!") + + // Listen for properties changed events + signalChan := make(chan *dbus.Signal, 10) + conn.Signal(signalChan) + + // Add a match rule to listen for DBus property changes + matchRule := "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" + err = conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err + if err != nil { + err = fmt.Errorf("failed to add DBus match rule: %w", err) + return + } + + logger.Info("waiting for a BLE pairing request...") + + for signal := range signalChan { + // Check if the signal is from a BlueZ device + if len(signal.Body) < 3 { + continue + } + + iface, ok := signal.Body[0].(string) + if !ok || iface != "org.bluez.Device1" { + continue + } + + // Check if the "Paired" property is in the event + changedProps, ok := signal.Body[1].(map[string]dbus.Variant) + if !ok { + continue + } + + // TODO [APP-7613]: Pairing attempts from an iPhone connect first + // before pairing, so listen for a "Connected" event on the system + // D-Bus. This should be tested against Android. + connected, exists := changedProps["Connected"] + if !exists || connected.Value() != true { + continue + } + + // Extract device path from the signal sender + devicePath := string(signal.Path) + + // Convert DBus object path to MAC address + deviceMAC := convertDBusPathToMAC(devicePath) + if deviceMAC == "" { + continue + } + + logger.Infof("device %s initiated pairing!", deviceMAC) + + // Mark device as trusted + if err = trustDevice(logger, devicePath); err != nil { + err = fmt.Errorf("failed to trust device: %w", err) + return + } else { + logger.Info("device successfully trusted!") + } + } + }, nil) + if err != nil { + logger.Errorw( + "failed to listen for pairing request (will have to manually accept pairing request on device)", + "err", err) + } +} diff --git a/subsystems/networking/definitions.go b/subsystems/networking/definitions.go index 64906c8..451bb76 100644 --- a/subsystems/networking/definitions.go +++ b/subsystems/networking/definitions.go @@ -88,12 +88,12 @@ func (n *network) getInfo() NetworkInfo { } type NetworkInfo struct { - Type string - SSID string - Security string - Signal int32 - Connected bool - LastError string + Type string `json:"type"` + SSID string `json:"ssid"` + Security string `json:"security"` + Signal int32 `json:"signal"` + Connected bool `json:"connected"` + LastError string `json:"last_error"` } func NetworkInfoToProto(net *NetworkInfo) *pb.NetworkInfo { From cc5045b7d0e4b0a34c823dafc27e3f5c1c5fec38 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Thu, 20 Feb 2025 16:25:47 -0500 Subject: [PATCH 25/37] Fix linux-side bugs (moved from MacOS to Linux). --- go.mod | 11 ++++++++++- go.sum | 21 +++++++++++++++++++++ subsystems/networking/bluetooth.go | 2 ++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 61cea64..d379651 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/semver/v3 v3.3.0 github.com/Otterverse/gonetworkmanager/v2 v2.2.1 github.com/gabriel-vasile/mimetype v1.4.8 + github.com/godbus/dbus v4.1.0+incompatible github.com/google/uuid v1.6.0 github.com/jessevdk/go-flags v1.6.1 github.com/nightlyone/lockfile v1.0.0 @@ -21,6 +22,7 @@ require ( golang.org/x/sys v0.28.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 + tinygo.org/x/bluetooth v0.11.0 ) require ( @@ -32,8 +34,8 @@ require ( github.com/dgottlieb/smarty-assertions v1.2.6 // indirect github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 // indirect github.com/edaniels/zeroconf v1.0.10 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -70,8 +72,14 @@ require ( github.com/pion/turn/v2 v2.1.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect + github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 // indirect + github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect github.com/srikrsna/protoc-gen-gotag v0.6.2 // indirect github.com/stretchr/testify v1.9.0 // indirect + github.com/tinygo-org/cbgo v0.0.4 // indirect + github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect github.com/viamrobotics/webrtc/v3 v3.99.10 // indirect github.com/wlynxg/anet v0.0.3 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -84,6 +92,7 @@ require ( go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.31.0 // indirect + golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect diff --git a/go.sum b/go.sum index bd10dbf..581037f 100644 --- a/go.sum +++ b/go.sum @@ -162,6 +162,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -581,6 +583,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryancurrah/gomodguard v1.2.0/go.mod h1:rNqbC4TOIdUDcVMSIpNNAzTbzXAZa6W5lnUepvuMMgQ= github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik= +github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sanposhiho/wastedassign v0.1.3/go.mod h1:LGpq5Hsv74QaqM47WtIsRSF/ik9kqk07kchgv66tLVE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -594,15 +598,22 @@ github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOms github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/go-diff v0.6.1/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 h1:arwJFX1x5zq+wUp5ADGgudhMQEXKNMQOmTh+yYgkwzw= +github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE= +github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4= +github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= @@ -647,6 +658,10 @@ github.com/tetafro/godot v1.4.4/go.mod h1:FVDd4JuKliW3UgjswZfJfHq4vAx0bD/Jd5brJj github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc= github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +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.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ= +github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756/go.mod h1:yiFB6fFoV7saXirUGfuK+cPtUh4NX/Hf5y2WC2lehu0= @@ -760,6 +775,8 @@ golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxT golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= +golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -864,6 +881,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -890,6 +908,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1109,3 +1128,5 @@ nhooyr.io/websocket v1.8.9/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +tinygo.org/x/bluetooth v0.11.0 h1:32ludjNnqz6RyVRpmw2qgod7NvDePbBTWXkJm6jj4cg= +tinygo.org/x/bluetooth v0.11.0/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw= diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 90b3c54..741cb1f 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -17,6 +17,7 @@ import ( "github.com/google/uuid" "go.viam.com/rdk/logging" "go.viam.com/utils" + "tinygo.org/x/bluetooth" ) // bluetoothService provides an interface for retrieving cloud config and/or WiFi credentials for a robot over bluetooth. @@ -186,6 +187,7 @@ func newBluetoothService(ctx context.Context, logger logging.Logger, name string charConfigPsk, charConfigRobotPartKeyID, charConfigRobotPartKey, + charConfigAppAddress, charConfigAvailableWiFiNetworks, }, } From a4c226a6de80ca6a44765d193f0c797be10cbbec Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Mon, 24 Feb 2025 10:09:58 -0500 Subject: [PATCH 26/37] Add debugging log lines for provisioning defaults JSON error (and remember to revert this commit once the fix for rc0.14 is released). --- subsystems/networking/bluetooth.go | 2 +- subsystems/networking/generators.go | 2 + subsystems/networking/networkmanager.go | 63 ++++++++++++++----------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 741cb1f..66ad001 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -257,7 +257,7 @@ type bluetoothServiceLinux struct { func (bsl *bluetoothServiceLinux) start(ctx context.Context) error { bsl.mu.Lock() defer bsl.mu.Unlock() - defer enableAutoAcceptPairRequest(bsl.logger) // Async (logs instead of error checks) to auto-accept pair requests on this device. + // defer enableAutoAcceptPairRequest(bsl.logger) // Async (logs instead of error checks) to auto-accept pair requests on this device. if bsl.adv == nil { return errors.New("advertisement is nil") diff --git a/subsystems/networking/generators.go b/subsystems/networking/generators.go index db10931..ba7f7ce 100644 --- a/subsystems/networking/generators.go +++ b/subsystems/networking/generators.go @@ -11,11 +11,13 @@ import ( "github.com/google/uuid" errw "github.com/pkg/errors" "github.com/viamrobotics/agent/utils" + "go.viam.com/rdk/logging" ) // This file contains the wifi/hotspot setting generation functions. func generateHotspotSettings(id, ssid, psk, ifName string) gnm.ConnectionSettings { + logging.NewLogger("maxhorowitz").Infof("generateHotspotSettings(id=%s, ssid=%s, psk=%s, idName=%s)", id, ssid, psk, ifName) IPAsUint32, err := generateAddress(PortalBindAddr) if err != nil { // BindAddr is a const, so should only ever fail if code itself is changed/broken diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index cbeda3f..935c3bf 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -14,6 +14,7 @@ import ( gnm "github.com/Otterverse/gonetworkmanager/v2" errw "github.com/pkg/errors" "github.com/viamrobotics/agent/utils" + "go.viam.com/rdk/logging" goutils "go.viam.com/utils" ) @@ -172,6 +173,8 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use return errors.New("provisioning mode already started") } + logging.NewLogger("maxhorowitz").Infof("Networking: %+v", n) + n.opMu.Lock() defer n.opMu.Unlock() @@ -179,7 +182,6 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use // Simultaneously start both the hotspot captive portal and the bluetooth service provisioning methods. wg := sync.WaitGroup{} - wg.Add(1) var hotspotErr error goutils.ManagedGo(func() { @@ -206,40 +208,40 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use n.hotspotIsActive = true }, wg.Done) - wg.Add(1) + // wg.Add(1) var bluetoothErr error - goutils.ManagedGo(func() { - if n.bluetoothService == nil { - bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) - if err != nil { - bluetoothErr = err - return - } - n.bluetoothService = bt - } - if err := n.bluetoothService.start(ctx); err != nil { - bluetoothErr = err - return - } - goutils.ManagedGo(func() { - userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. - if err != nil { - bluetoothErr = err - return - } - inputChan <- *userInput - }, nil) - n.bluetoothIsActive = true - }, wg.Done) + // goutils.ManagedGo(func() { + // if n.bluetoothService == nil { + // bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) + // if err != nil { + // bluetoothErr = err + // return + // } + // n.bluetoothService = bt + // } + // if err := n.bluetoothService.start(ctx); err != nil { + // bluetoothErr = err + // return + // } + // goutils.ManagedGo(func() { + // userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. + // if err != nil { + // bluetoothErr = err + // return + // } + // inputChan <- *userInput + // }, nil) + // n.bluetoothIsActive = true + // }, wg.Done) wg.Wait() switch { case !n.hotspotIsActive && !n.bluetoothIsActive: - return fmt.Errorf("failed to set up provisioning: %w", errors.Join(hotspotErr, bluetoothErr)) + return fmt.Errorf("failed to set up provisioning: %v", errors.Join(hotspotErr, bluetoothErr)) case !n.hotspotIsActive && n.bluetoothIsActive: - n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %w", hotspotErr) + n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %v", hotspotErr) case n.hotspotIsActive && !n.bluetoothIsActive: - n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %w", bluetoothErr) + n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %v", bluetoothErr) default: } @@ -324,12 +326,16 @@ func (n *Networking) activateConnection(ctx context.Context, ifName, ssid string if nw.netType != NetworkTypeHotspot { nw.lastTried = now n.netState.SetLastSSID(ifName, ssid) + n.logger.Infof("net type is WiFi, ifname: %s, ssid: %s", ifName, ssid) + } else { + n.logger.Infof("net type is hotspot, ifname: %s, ssid: %s", ifName, ssid) } netDev = n.netState.WifiDevice(ifName) } else { // wired nw.lastTried = now netDev = n.netState.EthDevice(ifName) + n.logger.Infof("net type is wired, ifname: %s", ifName) } if netDev == nil { @@ -450,6 +456,7 @@ func (n *Networking) AddOrUpdateConnection(cfg utils.NetworkDefinition) (bool, e // returns true if network was new (added) and not updated. func (n *Networking) addOrUpdateConnection(cfg utils.NetworkDefinition) (bool, error) { + logging.NewLogger("maxhorowitz").Infof("cfg: %+v", cfg) var changesMade bool if cfg.Type != NetworkTypeWifi && cfg.Type != NetworkTypeHotspot && cfg.Type != NetworkTypeWired { From 63b42ea7c116702269ac8debf561faab5b99109d Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Mon, 24 Feb 2025 13:48:35 -0500 Subject: [PATCH 27/37] Revert "Add debugging log lines for provisioning defaults JSON error (and remember to revert this commit once the fix for rc0.14 is released)." This reverts commit a4c226a6de80ca6a44765d193f0c797be10cbbec. --- subsystems/networking/bluetooth.go | 2 +- subsystems/networking/generators.go | 2 - subsystems/networking/networkmanager.go | 63 +++++++++++-------------- 3 files changed, 29 insertions(+), 38 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 66ad001..741cb1f 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -257,7 +257,7 @@ type bluetoothServiceLinux struct { func (bsl *bluetoothServiceLinux) start(ctx context.Context) error { bsl.mu.Lock() defer bsl.mu.Unlock() - // defer enableAutoAcceptPairRequest(bsl.logger) // Async (logs instead of error checks) to auto-accept pair requests on this device. + defer enableAutoAcceptPairRequest(bsl.logger) // Async (logs instead of error checks) to auto-accept pair requests on this device. if bsl.adv == nil { return errors.New("advertisement is nil") diff --git a/subsystems/networking/generators.go b/subsystems/networking/generators.go index ba7f7ce..db10931 100644 --- a/subsystems/networking/generators.go +++ b/subsystems/networking/generators.go @@ -11,13 +11,11 @@ import ( "github.com/google/uuid" errw "github.com/pkg/errors" "github.com/viamrobotics/agent/utils" - "go.viam.com/rdk/logging" ) // This file contains the wifi/hotspot setting generation functions. func generateHotspotSettings(id, ssid, psk, ifName string) gnm.ConnectionSettings { - logging.NewLogger("maxhorowitz").Infof("generateHotspotSettings(id=%s, ssid=%s, psk=%s, idName=%s)", id, ssid, psk, ifName) IPAsUint32, err := generateAddress(PortalBindAddr) if err != nil { // BindAddr is a const, so should only ever fail if code itself is changed/broken diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 935c3bf..cbeda3f 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -14,7 +14,6 @@ import ( gnm "github.com/Otterverse/gonetworkmanager/v2" errw "github.com/pkg/errors" "github.com/viamrobotics/agent/utils" - "go.viam.com/rdk/logging" goutils "go.viam.com/utils" ) @@ -173,8 +172,6 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use return errors.New("provisioning mode already started") } - logging.NewLogger("maxhorowitz").Infof("Networking: %+v", n) - n.opMu.Lock() defer n.opMu.Unlock() @@ -182,6 +179,7 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use // Simultaneously start both the hotspot captive portal and the bluetooth service provisioning methods. wg := sync.WaitGroup{} + wg.Add(1) var hotspotErr error goutils.ManagedGo(func() { @@ -208,40 +206,40 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use n.hotspotIsActive = true }, wg.Done) - // wg.Add(1) + wg.Add(1) var bluetoothErr error - // goutils.ManagedGo(func() { - // if n.bluetoothService == nil { - // bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) - // if err != nil { - // bluetoothErr = err - // return - // } - // n.bluetoothService = bt - // } - // if err := n.bluetoothService.start(ctx); err != nil { - // bluetoothErr = err - // return - // } - // goutils.ManagedGo(func() { - // userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. - // if err != nil { - // bluetoothErr = err - // return - // } - // inputChan <- *userInput - // }, nil) - // n.bluetoothIsActive = true - // }, wg.Done) + goutils.ManagedGo(func() { + if n.bluetoothService == nil { + bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) + if err != nil { + bluetoothErr = err + return + } + n.bluetoothService = bt + } + if err := n.bluetoothService.start(ctx); err != nil { + bluetoothErr = err + return + } + goutils.ManagedGo(func() { + userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. + if err != nil { + bluetoothErr = err + return + } + inputChan <- *userInput + }, nil) + n.bluetoothIsActive = true + }, wg.Done) wg.Wait() switch { case !n.hotspotIsActive && !n.bluetoothIsActive: - return fmt.Errorf("failed to set up provisioning: %v", errors.Join(hotspotErr, bluetoothErr)) + return fmt.Errorf("failed to set up provisioning: %w", errors.Join(hotspotErr, bluetoothErr)) case !n.hotspotIsActive && n.bluetoothIsActive: - n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %v", hotspotErr) + n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %w", hotspotErr) case n.hotspotIsActive && !n.bluetoothIsActive: - n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %v", bluetoothErr) + n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %w", bluetoothErr) default: } @@ -326,16 +324,12 @@ func (n *Networking) activateConnection(ctx context.Context, ifName, ssid string if nw.netType != NetworkTypeHotspot { nw.lastTried = now n.netState.SetLastSSID(ifName, ssid) - n.logger.Infof("net type is WiFi, ifname: %s, ssid: %s", ifName, ssid) - } else { - n.logger.Infof("net type is hotspot, ifname: %s, ssid: %s", ifName, ssid) } netDev = n.netState.WifiDevice(ifName) } else { // wired nw.lastTried = now netDev = n.netState.EthDevice(ifName) - n.logger.Infof("net type is wired, ifname: %s", ifName) } if netDev == nil { @@ -456,7 +450,6 @@ func (n *Networking) AddOrUpdateConnection(cfg utils.NetworkDefinition) (bool, e // returns true if network was new (added) and not updated. func (n *Networking) addOrUpdateConnection(cfg utils.NetworkDefinition) (bool, error) { - logging.NewLogger("maxhorowitz").Infof("cfg: %+v", cfg) var changesMade bool if cfg.Type != NetworkTypeWifi && cfg.Type != NetworkTypeHotspot && cfg.Type != NetworkTypeWired { From 2d84f1814f33e49a16de0788246c50177321bb65 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Mon, 24 Feb 2025 16:30:12 -0500 Subject: [PATCH 28/37] Fix establishing available WiFi networks in BLE characteristic (confirmed with manual test). --- subsystems/networking/bluetooth.go | 119 ++++++++---------------- subsystems/networking/definitions.go | 10 +- subsystems/networking/networkmanager.go | 8 +- 3 files changed, 49 insertions(+), 88 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 741cb1f..5aa7be8 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -24,12 +24,12 @@ import ( type bluetoothService interface { start(ctx context.Context) error stop() error - refreshAvailableNetworks(ctx context.Context, availableNetworks []*NetworkInfo) error waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) } // newBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. -func newBluetoothService(ctx context.Context, logger logging.Logger, name string) (bluetoothService, error) { +func newBluetoothService(logger logging.Logger, name string, availableNetworks []NetworkInfo, +) (bluetoothService, error) { if err := validateSystem(logger); err != nil { return nil, fmt.Errorf("cannot initialize bluetooth peripheral, system requisites not met: %w", err) } @@ -40,19 +40,19 @@ func newBluetoothService(ctx context.Context, logger logging.Logger, name string } serviceUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x1111) - logger.Infof("serviceUUID: %s", serviceUUID.String()) + logger.Debugf("serviceUUID: %s", serviceUUID.String()) charSsidUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x2222) - logger.Infof("charSsidUUID: %s", charSsidUUID.String()) + logger.Debugf("charSsidUUID: %s", charSsidUUID.String()) charPskUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x3333) - logger.Infof("charPskUUID: %s", charPskUUID.String()) + logger.Debugf("charPskUUID: %s", charPskUUID.String()) charRobotPartKeyIDUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x4444) - logger.Infof("charRobotPartKeyIDUUID: %s", charRobotPartKeyIDUUID.String()) + logger.Debugf("charRobotPartKeyIDUUID: %s", charRobotPartKeyIDUUID.String()) charRobotPartKeyUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x5555) - logger.Infof("charRobotPartKeyUUID: %s", charRobotPartKeyUUID.String()) + logger.Debugf("charRobotPartKeyUUID: %s", charRobotPartKeyUUID.String()) charAppAddressUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x6666) - logger.Infof("charAppAddress: %s", charAppAddressUUID.String()) + logger.Debugf("charAppAddress: %s", charAppAddressUUID.String()) charAvailableWiFiNetworksUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x7777) - logger.Infof("charAvailableWiFiNetworksUUID: %s", charAvailableWiFiNetworksUUID.String()) + logger.Debugf("charAvailableWiFiNetworksUUID: %s", charAvailableWiFiNetworksUUID.String()) // Create abstracted characteristics which act as a buffer for reading data from bluetooth. charSsid := &bluetoothCharacteristicLinux[*string]{ @@ -139,46 +139,27 @@ func newBluetoothService(ctx context.Context, logger logging.Logger, name string } // Create a read-only characteristic for broadcasting nearby, available WiFi networks. + var availableNetworksBytes [][]byte + for _, availableNetwork := range availableNetworks { + an := NetworkInfo{ // Compress each network by taking only relevant fields. + SSID: availableNetwork.SSID, + Security: availableNetwork.Security, + Signal: availableNetwork.Signal, + } + bs, err := json.Marshal(an) + if err != nil { + logger.Warnf("failed to parse network info: %+v", err) + } + availableNetworksBytes = append(availableNetworksBytes, bs) + } charConfigAvailableWiFiNetworks := bluetooth.CharacteristicConfig{ - UUID: charAvailableWiFiNetworksUUID, - Flags: bluetooth.CharacteristicReadPermission, - Value: nil, // This will get filled in via calls to UpdateAvailableWiFiNetworks. - WriteEvent: nil, // This characteristic is read-only. + UUID: charAvailableWiFiNetworksUUID, + Flags: bluetooth.CharacteristicReadPermission, + Value: bytes.Join(availableNetworksBytes, []byte(",")), // Only 20 bytes maximum size, + // and anything over that gets cut off. This is a BLE characteristic default standard, + // so we pass in available networks sorted in descending order of signal strength. } - // Channel will be written to by interface method UpdateAvailableWiFiNetworks and will be read by - // the following background goroutine - availableWiFiNetworksChannel := make(chan []*NetworkInfo, 1) - - // Read only channel used to listen for updates to the availableWiFiNetworks. - var availableWiFiNetworksChannelReadOnly <-chan []*NetworkInfo = availableWiFiNetworksChannel - utils.ManagedGo(func() { - defer close(availableWiFiNetworksChannel) - for { - if err := ctx.Err(); err != nil { - return - } - select { - case <-ctx.Done(): - return - case availableNetworks := <-availableWiFiNetworksChannelReadOnly: - var all [][]byte - for _, availableNetwork := range availableNetworks { - single, err := json.Marshal(availableNetwork) - if err != nil { - logger.Errorw("failed to convert available WiFi network to bytes", "err", err) - continue - } - all = append(all, single) - } - charConfigAvailableWiFiNetworks.Value = bytes.Join(all, []byte(",")) - logger.Infow("successfully updated available WiFi networks on bluetooth characteristic") - default: - time.Sleep(time.Second) - } - } - }, nil) - // Create service which will advertise each of the above characteristics. s := &bluetooth.Service{ UUID: serviceUUID, @@ -217,8 +198,6 @@ func newBluetoothService(ctx context.Context, logger logging.Logger, name string advActive: false, UUID: serviceUUID, - availableWiFiNetworksChannelWriteOnly: availableWiFiNetworksChannel, - characteristicSsid: charSsid, characteristicPsk: charPsk, characteristicRobotPartKeyID: charRobotPartKeyID, @@ -244,8 +223,6 @@ type bluetoothServiceLinux struct { advActive bool UUID bluetooth.UUID - availableWiFiNetworksChannelWriteOnly chan<- []*NetworkInfo - characteristicSsid *bluetoothCharacteristicLinux[*string] characteristicPsk *bluetoothCharacteristicLinux[*string] characteristicRobotPartKeyID *bluetoothCharacteristicLinux[*string] @@ -292,12 +269,6 @@ func (bsl *bluetoothServiceLinux) stop() error { return nil } -// Update updates the list of networks that are advertised via bluetooth as available. -func (bsl *bluetoothServiceLinux) refreshAvailableNetworks(ctx context.Context, availableNetworks []*NetworkInfo) error { - bsl.availableWiFiNetworksChannelWriteOnly <- availableNetworks - return nil -} - // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. func (bsl *bluetoothServiceLinux) waitForCredentials( ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, @@ -514,21 +485,21 @@ func validateSystem(logger logging.Logger) error { if err := checkOS(); err != nil { return err } - logger.Info("✅ Running on a Linux system.") + logger.Debug("✅ Running on a Linux system.") // 2. Check BlueZ version blueZVersion, err := getBlueZVersion() if err != nil { return err } - logger.Infof("✅ BlueZ detected, version: %.2f", blueZVersion) + logger.Debugf("✅ BlueZ detected, version: %.2f", blueZVersion) // 3. Validate BlueZ version is 5.66 or higher if blueZVersion < 5.66 { return fmt.Errorf("❌ BlueZ version is %.2f, but 5.66 or later is required", blueZVersion) } - logger.Info("✅ BlueZ version meets the requirement (5.66 or later).") + logger.Debug("✅ BlueZ version meets the requirement (5.66 or later).") return nil } @@ -547,7 +518,7 @@ func trustDevice(logger logging.Logger, devicePath string) error { if call.Err != nil { return fmt.Errorf("failed to set Trusted property: %w", call.Err) } - logger.Info("device marked as trusted.") + logger.Debug("device marked as trusted.") return nil } @@ -566,18 +537,17 @@ func convertDBusPathToMAC(path string) string { } func enableAutoAcceptPairRequest(logger logging.Logger) { - var err error utils.ManagedGo(func() { conn, err := dbus.SystemBus() if err != nil { - err = fmt.Errorf("failed to connect to system DBus: %w", err) + logger.Error(fmt.Errorf("failed to connect to system DBus: %w", err)) return } // Export agent methods reply := conn.Export(nil, BluezAgentPath, BluezAgent) if reply != nil { - err = fmt.Errorf("failed to export Bluez agent: %w", reply) + logger.Error(fmt.Errorf("failed to export Bluez agent: %w", reply)) return } @@ -585,18 +555,18 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { obj := conn.Object(BluezDBusService, "/org/bluez") call := obj.Call("org.bluez.AgentManager1.RegisterAgent", 0, dbus.ObjectPath(BluezAgentPath), "NoInputNoOutput") if err := call.Err; err != nil { - err = fmt.Errorf("failed to register Bluez agent: %w", err) + logger.Error(fmt.Errorf("failed to register Bluez agent: %w", err)) return } // Set as the default agent call = obj.Call("org.bluez.AgentManager1.RequestDefaultAgent", 0, dbus.ObjectPath(BluezAgentPath)) if err := call.Err; err != nil { - err = fmt.Errorf("failed to set default Bluez agent: %w", err) + logger.Error(fmt.Errorf("failed to set default Bluez agent: %w", err)) return } - logger.Info("Bluez agent registered!") + logger.Debug("Bluez agent registered!") // Listen for properties changed events signalChan := make(chan *dbus.Signal, 10) @@ -606,11 +576,11 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { matchRule := "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" err = conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err if err != nil { - err = fmt.Errorf("failed to add DBus match rule: %w", err) + logger.Error(fmt.Errorf("failed to add DBus match rule: %w", err)) return } - logger.Info("waiting for a BLE pairing request...") + logger.Debug("waiting for a BLE pairing request...") for signal := range signalChan { // Check if the signal is from a BlueZ device @@ -645,21 +615,12 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { if deviceMAC == "" { continue } - - logger.Infof("device %s initiated pairing!", deviceMAC) - - // Mark device as trusted if err = trustDevice(logger, devicePath); err != nil { - err = fmt.Errorf("failed to trust device: %w", err) + logger.Error(fmt.Errorf("failed to trust device: %w", err)) return } else { - logger.Info("device successfully trusted!") + logger.Debug("device successfully trusted!") } } }, nil) - if err != nil { - logger.Errorw( - "failed to listen for pairing request (will have to manually accept pairing request on device)", - "err", err) - } } diff --git a/subsystems/networking/definitions.go b/subsystems/networking/definitions.go index 451bb76..e59c067 100644 --- a/subsystems/networking/definitions.go +++ b/subsystems/networking/definitions.go @@ -88,12 +88,12 @@ func (n *network) getInfo() NetworkInfo { } type NetworkInfo struct { - Type string `json:"type"` + Type string `json:"type,omitempty"` SSID string `json:"ssid"` - Security string `json:"security"` - Signal int32 `json:"signal"` - Connected bool `json:"connected"` - LastError string `json:"last_error"` + Security string `json:"sec"` + Signal int32 `json:"sig"` + Connected bool `json:"conn,omitempty"` + LastError string `json:"lastErr,omitempty"` } func NetworkInfoToProto(net *NetworkInfo) *pb.NetworkInfo { diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index cbeda3f..7121ec2 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -210,7 +210,7 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use var bluetoothErr error goutils.ManagedGo(func() { if n.bluetoothService == nil { - bt, err := newBluetoothService(ctx, n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID)) + bt, err := newBluetoothService(n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID), n.getVisibleNetworks()) if err != nil { bluetoothErr = err return @@ -224,7 +224,7 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use goutils.ManagedGo(func() { userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. if err != nil { - bluetoothErr = err + n.logger.Errorw("failed to wait for user input of credentials", "err", err) return } inputChan <- *userInput @@ -237,9 +237,9 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use case !n.hotspotIsActive && !n.bluetoothIsActive: return fmt.Errorf("failed to set up provisioning: %w", errors.Join(hotspotErr, bluetoothErr)) case !n.hotspotIsActive && n.bluetoothIsActive: - n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %w", hotspotErr) + n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %+v", hotspotErr) case n.hotspotIsActive && !n.bluetoothIsActive: - n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %w", bluetoothErr) + n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %+v", bluetoothErr) default: } From 88f771d340d4e994a3d6650de1233a9bc3d463a0 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Mon, 24 Feb 2025 16:35:29 -0500 Subject: [PATCH 29/37] Add suggestions from linter. --- subsystems/networking/bluetooth.go | 2 +- subsystems/networking/networkmanager.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 5aa7be8..bdd99f8 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -206,7 +206,7 @@ func newBluetoothService(logger logging.Logger, name string, availableNetworks [ }, nil } -// bluetoothCharacteristicLinux is used to read and write values to a bluetooh peripheral. +// bluetoothCharacteristicLinux is used to read and write values to a bluetooth peripheral. type bluetoothCharacteristicLinux[T any] struct { UUID bluetooth.UUID mu *sync.Mutex diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 7121ec2..574f88c 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -210,7 +210,8 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use var bluetoothErr error goutils.ManagedGo(func() { if n.bluetoothService == nil { - bt, err := newBluetoothService(n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID), n.getVisibleNetworks()) + bt, err := newBluetoothService( + n.logger, fmt.Sprintf("%s.%s.%s", n.cfg.Manufacturer, n.cfg.Model, n.cfg.FragmentID), n.getVisibleNetworks()) if err != nil { bluetoothErr = err return From d5c3c4e45ff1b51de370c9e1d2e2518ef18c982a Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 11:06:59 -0500 Subject: [PATCH 30/37] Pass write only channel into bluetooth service as interface requirement. --- subsystems/networking/bluetooth.go | 182 +++++++++++++----------- subsystems/networking/networkmanager.go | 12 +- 2 files changed, 100 insertions(+), 94 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index bdd99f8..9b058d1 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -24,7 +24,7 @@ import ( type bluetoothService interface { start(ctx context.Context) error stop() error - waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool) (*userInput, error) + waitForCredentials(ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, inputChan chan<- userInput) error } // newBluetoothService returns a service which accepts credentials over bluetooth to provision a robot and its WiFi connection. @@ -40,19 +40,19 @@ func newBluetoothService(logger logging.Logger, name string, availableNetworks [ } serviceUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x1111) - logger.Debugf("serviceUUID: %s", serviceUUID.String()) + logger.Debugf("Bluetooth peripheral service UUID: %s", serviceUUID.String()) charSsidUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x2222) - logger.Debugf("charSsidUUID: %s", charSsidUUID.String()) + logger.Debugf("WiFi SSID can be written to the following bluetooth characteristic: %s", charSsidUUID.String()) charPskUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x3333) - logger.Debugf("charPskUUID: %s", charPskUUID.String()) + logger.Debugf("WiFi passkey can be written to the following bluetooth characteristic: %s", charPskUUID.String()) charRobotPartKeyIDUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x4444) - logger.Debugf("charRobotPartKeyIDUUID: %s", charRobotPartKeyIDUUID.String()) + logger.Debugf("Robot part key ID can be written to the following bluetooth characteristic: %s", charRobotPartKeyIDUUID.String()) charRobotPartKeyUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x5555) - logger.Debugf("charRobotPartKeyUUID: %s", charRobotPartKeyUUID.String()) + logger.Debugf("Robot part key can be written to the following bluetooth characteristic: %s", charRobotPartKeyUUID.String()) charAppAddressUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x6666) - logger.Debugf("charAppAddress: %s", charAppAddressUUID.String()) + logger.Debugf("Viam app address can be written to the following bluetooth characteristic: %s", charAppAddressUUID.String()) charAvailableWiFiNetworksUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x7777) - logger.Debugf("charAvailableWiFiNetworksUUID: %s", charAvailableWiFiNetworksUUID.String()) + logger.Debugf("Available WiFi networks can be read from the following bluetooth characteristic: %s", charAvailableWiFiNetworksUUID.String()) // Create abstracted characteristics which act as a buffer for reading data from bluetooth. charSsid := &bluetoothCharacteristicLinux[*string]{ @@ -246,7 +246,7 @@ func (bsl *bluetoothServiceLinux) start(ctx context.Context) error { return fmt.Errorf("failed to start advertising: %w", err) } bsl.advActive = true - bsl.logger.Info("started advertising a BLE connection...") + bsl.logger.Debug("Started advertising a BLE connection") return nil } @@ -265,85 +265,90 @@ func (bsl *bluetoothServiceLinux) stop() error { return fmt.Errorf("failed to stop advertising: %w", err) } bsl.advActive = false - bsl.logger.Info("stopped advertising a BLE connection") + bsl.logger.Debug("Stopped advertising a BLE connection") return nil } // WaitForCredentials returns credentials, the minimum required information to provision a robot and/or its WiFi. func (bsl *bluetoothServiceLinux) waitForCredentials( - ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, -) (*userInput, error) { + ctx context.Context, requiresCloudCredentials, requiresWiFiCredentials bool, inputChan chan<- userInput, +) error { if !requiresWiFiCredentials && !requiresCloudCredentials { - return nil, errors.New("should be waiting for cloud credentials or WiFi credentials, or both") + return errors.New("should be waiting for cloud credentials or WiFi credentials, or both") } var ssid, psk, robotPartKeyID, robotPartKey string - var ctxErr, ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error - - wg := sync.WaitGroup{} - wg.Add(1) - - utils.ManagedGo(func() { - for { - if ctxErr = ctx.Err(); ctxErr != nil { - return - } - select { - case <-ctx.Done(): - ctxErr = ctx.Err() - return - default: - if requiresWiFiCredentials { - if ssid == "" { - var e *emptyBluetoothCharacteristicError - ssid, ssidErr = bsl.readSsid() - if ssidErr != nil && !errors.As(ssidErr, &e) { - return - } - } - if psk == "" { - var e *emptyBluetoothCharacteristicError - psk, pskErr = bsl.readPsk() - if pskErr != nil && !errors.As(pskErr, &e) { - return - } + var ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr error + for { + var shouldBreakOuterLoop bool + if ctx.Err() != nil { + break + } + select { + case <-ctx.Done(): + shouldBreakOuterLoop = true + default: + if requiresWiFiCredentials { + if ssid == "" { + var e *emptyBluetoothCharacteristicError + ssid, ssidErr = bsl.readSsid() + if ssidErr != nil && !errors.As(ssidErr, &e) { + shouldBreakOuterLoop = true + break } } - if requiresCloudCredentials { - if robotPartKeyID == "" { - var e *emptyBluetoothCharacteristicError - robotPartKeyID, robotPartKeyIDErr = bsl.readRobotPartKeyID() - if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, &e) { - return - } + if psk == "" { + var e *emptyBluetoothCharacteristicError + psk, pskErr = bsl.readPsk() + if pskErr != nil && !errors.As(pskErr, &e) { + shouldBreakOuterLoop = true + break } - if robotPartKey == "" { - var e *emptyBluetoothCharacteristicError - robotPartKey, robotPartKeyErr = bsl.readRobotPartKey() - if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, &e) { - return - } + } + } + if requiresCloudCredentials { + if robotPartKeyID == "" { + var e *emptyBluetoothCharacteristicError + robotPartKeyID, robotPartKeyIDErr = bsl.readRobotPartKeyID() + if robotPartKeyIDErr != nil && !errors.As(robotPartKeyIDErr, &e) { + shouldBreakOuterLoop = true + break } } - if requiresWiFiCredentials && requiresCloudCredentials && //nolint:gocritic - ssid != "" && psk != "" && robotPartKeyID != "" && robotPartKey != "" { - return - } else if requiresWiFiCredentials && ssid != "" && psk != "" { - return - } else if requiresCloudCredentials && robotPartKeyID != "" && robotPartKey != "" { - return + if robotPartKey == "" { + var e *emptyBluetoothCharacteristicError + robotPartKey, robotPartKeyErr = bsl.readRobotPartKey() + if robotPartKeyErr != nil && !errors.As(robotPartKeyErr, &e) { + shouldBreakOuterLoop = true + break + } } - - // Not ready to return (do not have the minimum required set of credentials), so sleep and try again. - time.Sleep(time.Second) } - } - }, wg.Done) - - wg.Wait() + if requiresWiFiCredentials && requiresCloudCredentials && //nolint:gocritic + ssid != "" && psk != "" && robotPartKeyID != "" && robotPartKey != "" { + shouldBreakOuterLoop = true + break + } else if requiresWiFiCredentials && ssid != "" && psk != "" { + shouldBreakOuterLoop = true + break + } else if requiresCloudCredentials && robotPartKeyID != "" && robotPartKey != "" { + shouldBreakOuterLoop = true + break + } - return &userInput{ + // Not ready to return (do not have the minimum required set of credentials), so sleep and try again. + time.Sleep(time.Second) + } + if shouldBreakOuterLoop { + break + } + } + if err := errors.Join(ctx.Err(), ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr); err != nil { + return err + } + inputChan <- userInput{ SSID: ssid, PSK: psk, PartID: robotPartKeyID, Secret: robotPartKey, - }, errors.Join(ctxErr, ssidErr, pskErr, robotPartKeyIDErr, robotPartKeyErr) + } + return nil } /** Helper methods for low-level system calls and read/write requests to/from bluetooth characteristics **/ @@ -485,21 +490,19 @@ func validateSystem(logger logging.Logger) error { if err := checkOS(); err != nil { return err } - logger.Debug("✅ Running on a Linux system.") // 2. Check BlueZ version blueZVersion, err := getBlueZVersion() if err != nil { return err } - logger.Debugf("✅ BlueZ detected, version: %.2f", blueZVersion) // 3. Validate BlueZ version is 5.66 or higher if blueZVersion < 5.66 { - return fmt.Errorf("❌ BlueZ version is %.2f, but 5.66 or later is required", blueZVersion) + return fmt.Errorf("BlueZ version is %.2f, but 5.66 or later is required", blueZVersion) } - logger.Debug("✅ BlueZ version meets the requirement (5.66 or later).") + logger.Debugf("BlueZ version (%.2f) meets the requirement (5.66 or later)", blueZVersion) return nil } @@ -518,7 +521,7 @@ func trustDevice(logger logging.Logger, devicePath string) error { if call.Err != nil { return fmt.Errorf("failed to set Trusted property: %w", call.Err) } - logger.Debug("device marked as trusted.") + logger.Debug("Device marked as trusted.") return nil } @@ -540,29 +543,36 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { utils.ManagedGo(func() { conn, err := dbus.SystemBus() if err != nil { - logger.Error(fmt.Errorf("failed to connect to system DBus: %w", err)) + logger.Errorf("Failed to connect to system DBus: %w", err) return } // Export agent methods reply := conn.Export(nil, BluezAgentPath, BluezAgent) if reply != nil { - logger.Error(fmt.Errorf("failed to export Bluez agent: %w", reply)) + logger.Errorf("Failed to export Bluez agent: %w", reply) return } - // Register the agent + // Register the agent (and defer call to unregister agent). obj := conn.Object(BluezDBusService, "/org/bluez") call := obj.Call("org.bluez.AgentManager1.RegisterAgent", 0, dbus.ObjectPath(BluezAgentPath), "NoInputNoOutput") if err := call.Err; err != nil { - logger.Error(fmt.Errorf("failed to register Bluez agent: %w", err)) + logger.Errorf("Failed to register Bluez agent: %w", err) return } + defer func() { + call := obj.Call("org.bluez.AgentManager1.UnregisterAgent", 0, dbus.ObjectPath(BluezAgentPath)) + if err := call.Err; err != nil { + logger.Errorf("Failed to unregister Bluez agent: %w", err) + return + } + }() // Set as the default agent call = obj.Call("org.bluez.AgentManager1.RequestDefaultAgent", 0, dbus.ObjectPath(BluezAgentPath)) if err := call.Err; err != nil { - logger.Error(fmt.Errorf("failed to set default Bluez agent: %w", err)) + logger.Errorf("Failed to set default Bluez agent: %w", err) return } @@ -576,11 +586,11 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { matchRule := "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'" err = conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err if err != nil { - logger.Error(fmt.Errorf("failed to add DBus match rule: %w", err)) + logger.Errorf("Failed to add DBus match rule: %w", err) return } - logger.Debug("waiting for a BLE pairing request...") + logger.Debug("Waiting for a BLE pairing request...") for signal := range signalChan { // Check if the signal is from a BlueZ device @@ -589,7 +599,7 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { } iface, ok := signal.Body[0].(string) - if !ok || iface != "org.bluez.Device1" { + if !ok || iface != "org.bleuez.Device1" { continue } @@ -616,10 +626,10 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { continue } if err = trustDevice(logger, devicePath); err != nil { - logger.Error(fmt.Errorf("failed to trust device: %w", err)) + logger.Errorf("Failed to trust device: %w", err) return } else { - logger.Debug("device successfully trusted!") + logger.Debug("Device successfully trusted!") } } }, nil) diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 574f88c..80ca9ab 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -222,14 +222,10 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use bluetoothErr = err return } - goutils.ManagedGo(func() { - userInput, err := n.bluetoothService.waitForCredentials(ctx, true, true) // Background goroutine ultimately cancelled by context. - if err != nil { - n.logger.Errorw("failed to wait for user input of credentials", "err", err) - return - } - inputChan <- *userInput - }, nil) + if err := n.bluetoothService.waitForCredentials(ctx, true, true, inputChan); err != nil { + bluetoothErr = err + return + } n.bluetoothIsActive = true }, wg.Done) wg.Wait() From 4ed618624f06536cd4b1591b73d7f6d23de8d870 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 11:10:17 -0500 Subject: [PATCH 31/37] Add changes from linter. --- subsystems/networking/bluetooth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 9b058d1..5e0530e 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -52,7 +52,8 @@ func newBluetoothService(logger logging.Logger, name string, availableNetworks [ charAppAddressUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x6666) logger.Debugf("Viam app address can be written to the following bluetooth characteristic: %s", charAppAddressUUID.String()) charAvailableWiFiNetworksUUID := bluetooth.NewUUID(uuid.New()).Replace16BitComponent(0x7777) - logger.Debugf("Available WiFi networks can be read from the following bluetooth characteristic: %s", charAvailableWiFiNetworksUUID.String()) + logger.Debugf("Available WiFi networks can be read from the following bluetooth characteristic: %s", + charAvailableWiFiNetworksUUID.String()) // Create abstracted characteristics which act as a buffer for reading data from bluetooth. charSsid := &bluetoothCharacteristicLinux[*string]{ From 83e1f29da58f2ddf986a6c981597302fcfcef129 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 12:55:24 -0500 Subject: [PATCH 32/37] Make waiting for credentials asynchronous. --- subsystems/networking/bluetooth.go | 1 + subsystems/networking/networkmanager.go | 5 +- subsystems/networking/networkmanager_test.go | 58 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 subsystems/networking/networkmanager_test.go diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 5e0530e..9c1468e 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -282,6 +282,7 @@ func (bsl *bluetoothServiceLinux) waitForCredentials( for { var shouldBreakOuterLoop bool if ctx.Err() != nil { + shouldBreakOuterLoop = true break } select { diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index eae63fc..836edfd 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -222,10 +222,7 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use bluetoothErr = err return } - if err := n.bluetoothService.waitForCredentials(ctx, true, true, inputChan); err != nil { - bluetoothErr = err - return - } + goutils.ManagedGo(func() { _ = n.bluetoothService.waitForCredentials(ctx, true, true, inputChan) }, nil) n.bluetoothIsActive = true }, wg.Done) wg.Wait() diff --git a/subsystems/networking/networkmanager_test.go b/subsystems/networking/networkmanager_test.go new file mode 100644 index 0000000..ab5056f --- /dev/null +++ b/subsystems/networking/networkmanager_test.go @@ -0,0 +1,58 @@ +package networking + +import ( + "context" + "testing" + + "github.com/viamrobotics/agent/utils" + "go.viam.com/rdk/logging" + "go.viam.com/test" +) + +func TestStartProvisioning(t *testing.T) { + ctx := context.Background() + logger := logging.NewTestLogger(t) + n := newNetworkingMock(t, ctx, logger) + test.That(t, n, test.ShouldNotBeNil) + inputChan := make(chan userInput, 1) + + /* + There is no variability in inputs passed to StartProvisioning, so + networking state validation should suffice for unit testing. + */ + + // Validate networking state from before provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldEqual, false) + test.That(t, len(n.nets), test.ShouldEqual, 0) + test.That(t, n.portalData, test.ShouldBeNil) + + err := n.StartProvisioning(ctx, inputChan) + test.That(t, err, test.ShouldBeNil) +} + +func TestStopProvisioning(t *testing.T) { + +} + +func newNetworkingMock(t *testing.T, ctx context.Context, logger logging.Logger) *Networking { + subsystem := NewSubsystem(ctx, logger, utils.DefaultConfig()) + networking, ok := subsystem.(*Networking) + test.That(t, ok, test.ShouldBeTrue) + networking.bluetoothService = &bluetoothServiceMock{} + return networking +} + +type bluetoothServiceMock struct { +} + +func (bsm *bluetoothServiceMock) start(_ context.Context) error { + return nil +} + +func (bsm *bluetoothServiceMock) stop() error { + return nil +} + +func (bsm *bluetoothServiceMock) waitForCredentials(_ context.Context, _, _ bool, _ chan<- userInput) error { + return nil +} From 35ad378a161887bc888ef67a9b259c27a3f342bd Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 13:52:38 -0500 Subject: [PATCH 33/37] Add logging of errors in waitForCredentials call. --- subsystems/networking/networkmanager.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 836edfd..69127e1 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -222,7 +222,14 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use bluetoothErr = err return } - goutils.ManagedGo(func() { _ = n.bluetoothService.waitForCredentials(ctx, true, true, inputChan) }, nil) + goutils.ManagedGo( // Listen for user input asynchronously. + func() { + if err := n.bluetoothService.waitForCredentials(ctx, true, true, inputChan); err != nil { + n.logger.Errorf("Failed to wait for user input of credentials over bluetooth: %+v", err) + } + + }, nil, + ) n.bluetoothIsActive = true }, wg.Done) wg.Wait() From a5d32b10141d4c420fcf9040fa389080f36df5f2 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 15:01:51 -0500 Subject: [PATCH 34/37] Add networkmanager_test.go file for StartProvisioning and StopProvisioning tests. --- subsystems/networking/networkmanager_test.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/subsystems/networking/networkmanager_test.go b/subsystems/networking/networkmanager_test.go index ab5056f..761dd46 100644 --- a/subsystems/networking/networkmanager_test.go +++ b/subsystems/networking/networkmanager_test.go @@ -22,12 +22,25 @@ func TestStartProvisioning(t *testing.T) { */ // Validate networking state from before provisioning flow. - test.That(t, n.connState.provisioningMode, test.ShouldEqual, false) + test.That(t, n.connState.provisioningMode, test.ShouldBeFalse) test.That(t, len(n.nets), test.ShouldEqual, 0) - test.That(t, n.portalData, test.ShouldBeNil) + test.That(t, n.portalData, test.ShouldResemble, &portalData{}) + test.That(t, n.hotspotIsActive, test.ShouldBeFalse) + test.That(t, n.bluetoothIsActive, test.ShouldBeFalse) err := n.StartProvisioning(ctx, inputChan) test.That(t, err, test.ShouldBeNil) + + // Validate networking state from after provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeTrue) + test.That(t, len(n.nets), test.ShouldEqual, 0) + var ui *userInput + test.That(t, n.portalData.input, test.ShouldResemble, ui) + test.That(t, n.hotspotIsActive, test.ShouldBeTrue) + test.That(t, n.bluetoothIsActive, test.ShouldBeTrue) + + // Validate passing user inputs works. + } func TestStopProvisioning(t *testing.T) { @@ -39,6 +52,7 @@ func newNetworkingMock(t *testing.T, ctx context.Context, logger logging.Logger) networking, ok := subsystem.(*Networking) test.That(t, ok, test.ShouldBeTrue) networking.bluetoothService = &bluetoothServiceMock{} + test.That(t, networking.init(ctx), test.ShouldBeNil) return networking } From 6c4c5fe063d76753d67e04b66d967c114daefe36 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 15:13:14 -0500 Subject: [PATCH 35/37] Fix typo in log lines. --- subsystems/networking/networkmanager.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index b456f04..7bc8ce7 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -238,9 +238,9 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use case !n.hotspotIsActive && !n.bluetoothIsActive: return fmt.Errorf("failed to set up provisioning: %w", errors.Join(hotspotErr, bluetoothErr)) case !n.hotspotIsActive && n.bluetoothIsActive: - n.logger.Infof("started bluetooth provisioning, but failed to set up hotspot provisioning: %+v", hotspotErr) + n.logger.Infof("Started bluetooth provisioning, but failed to set up hotspot provisioning: %+v", hotspotErr) case n.hotspotIsActive && !n.bluetoothIsActive: - n.logger.Infof("started hotspot provisioning, but failed to set up bluetooth provisioning: %+v", bluetoothErr) + n.logger.Infof("Started hotspot provisioning, but failed to set up bluetooth provisioning: %+v", bluetoothErr) default: } From f5f6fc96e2c0ad778b2bb8eec8f0cd21b0680073 Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Tue, 25 Feb 2025 17:29:05 -0500 Subject: [PATCH 36/37] Add passing unit tests. --- subsystems/networking/bluetooth.go | 12 +- subsystems/networking/networkmanager.go | 12 +- subsystems/networking/networkmanager_test.go | 122 +++++++++++++++---- 3 files changed, 111 insertions(+), 35 deletions(-) diff --git a/subsystems/networking/bluetooth.go b/subsystems/networking/bluetooth.go index 9c1468e..bc192b1 100644 --- a/subsystems/networking/bluetooth.go +++ b/subsystems/networking/bluetooth.go @@ -282,7 +282,6 @@ func (bsl *bluetoothServiceLinux) waitForCredentials( for { var shouldBreakOuterLoop bool if ctx.Err() != nil { - shouldBreakOuterLoop = true break } select { @@ -556,20 +555,13 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { return } - // Register the agent (and defer call to unregister agent). + // Register the agent. obj := conn.Object(BluezDBusService, "/org/bluez") call := obj.Call("org.bluez.AgentManager1.RegisterAgent", 0, dbus.ObjectPath(BluezAgentPath), "NoInputNoOutput") if err := call.Err; err != nil { logger.Errorf("Failed to register Bluez agent: %w", err) return } - defer func() { - call := obj.Call("org.bluez.AgentManager1.UnregisterAgent", 0, dbus.ObjectPath(BluezAgentPath)) - if err := call.Err; err != nil { - logger.Errorf("Failed to unregister Bluez agent: %w", err) - return - } - }() // Set as the default agent call = obj.Call("org.bluez.AgentManager1.RequestDefaultAgent", 0, dbus.ObjectPath(BluezAgentPath)) @@ -601,7 +593,7 @@ func enableAutoAcceptPairRequest(logger logging.Logger) { } iface, ok := signal.Body[0].(string) - if !ok || iface != "org.bleuez.Device1" { + if !ok || iface != "org.bluez.Device1" { continue } diff --git a/subsystems/networking/networkmanager.go b/subsystems/networking/networkmanager.go index 7bc8ce7..2b0aaf1 100644 --- a/subsystems/networking/networkmanager.go +++ b/subsystems/networking/networkmanager.go @@ -222,12 +222,11 @@ func (n *Networking) StartProvisioning(ctx context.Context, inputChan chan<- use bluetoothErr = err return } - goutils.ManagedGo( // Listen for user input asynchronously. + goutils.ManagedGo( // Listen for user input asynchronously. How should we be handling errors here? func() { if err := n.bluetoothService.waitForCredentials(ctx, true, true, inputChan); err != nil { n.logger.Errorf("Failed to wait for user input of credentials over bluetooth: %+v", err) } - }, nil, ) n.bluetoothIsActive = true @@ -256,7 +255,6 @@ func (n *Networking) StopProvisioning() error { func (n *Networking) stopProvisioning() error { n.logger.Info("Stopping provisioning mode.") - n.connState.setProvisioning(false) wg := sync.WaitGroup{} var hotspotErr error @@ -273,6 +271,7 @@ func (n *Networking) stopProvisioning() error { hotspotErr = combinedErr return } + n.hotspotIsActive = false n.logger.Info("Stopped hotspot and captive portal.") }, wg.Done) } @@ -289,12 +288,17 @@ func (n *Networking) stopProvisioning() error { bluetoothErr = err return } + n.bluetoothIsActive = false n.logger.Info("Stopped bluetooth service.") }, wg.Done) } wg.Wait() - return errors.Join(hotspotErr, bluetoothErr) + if err := errors.Join(hotspotErr, bluetoothErr); err != nil { + return err + } + n.connState.setProvisioning(false) + return nil } func (n *Networking) ActivateConnection(ctx context.Context, ifName, ssid string) error { diff --git a/subsystems/networking/networkmanager_test.go b/subsystems/networking/networkmanager_test.go index 761dd46..333367b 100644 --- a/subsystems/networking/networkmanager_test.go +++ b/subsystems/networking/networkmanager_test.go @@ -2,6 +2,7 @@ package networking import ( "context" + "errors" "testing" "github.com/viamrobotics/agent/utils" @@ -9,64 +10,143 @@ import ( "go.viam.com/test" ) -func TestStartProvisioning(t *testing.T) { +func TestProvisioningOverBluetooth(t *testing.T) { ctx := context.Background() logger := logging.NewTestLogger(t) - n := newNetworkingMock(t, ctx, logger) + n, bsm := newNetworkingWithBluetoothServiceMock(t, ctx, logger) test.That(t, n, test.ShouldNotBeNil) inputChan := make(chan userInput, 1) /* - There is no variability in inputs passed to StartProvisioning, so - networking state validation should suffice for unit testing. + There is no variability in inputs passed to either StartProvisioning or + StopProvisioning, so provisioning state validation should suffice for + unit testing. */ - // Validate networking state from before provisioning flow. + /* + Case 1: Successfully start, asynch wait for credentials, and stop bluetooth provisioning. + */ + bsm.shouldFailToStart = false + bsm.shouldFailToWaitForCredentials = false + bsm.shouldFailToStop = false + + // Validate networking state from before starting provisioning flow. test.That(t, n.connState.provisioningMode, test.ShouldBeFalse) - test.That(t, len(n.nets), test.ShouldEqual, 0) - test.That(t, n.portalData, test.ShouldResemble, &portalData{}) - test.That(t, n.hotspotIsActive, test.ShouldBeFalse) test.That(t, n.bluetoothIsActive, test.ShouldBeFalse) err := n.StartProvisioning(ctx, inputChan) test.That(t, err, test.ShouldBeNil) - // Validate networking state from after provisioning flow. + // Validate networking state from after starting provisioning flow. test.That(t, n.connState.provisioningMode, test.ShouldBeTrue) - test.That(t, len(n.nets), test.ShouldEqual, 0) - var ui *userInput - test.That(t, n.portalData.input, test.ShouldResemble, ui) - test.That(t, n.hotspotIsActive, test.ShouldBeTrue) test.That(t, n.bluetoothIsActive, test.ShouldBeTrue) - // Validate passing user inputs works. + err = n.StopProvisioning() + test.That(t, err, test.ShouldBeNil) -} + // Validate networking state from after stopping provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeFalse) + test.That(t, n.bluetoothIsActive, test.ShouldBeFalse) + + /* + Case 2: Fail to start bluetooth provisioning. + */ + bsm.shouldFailToStart = true + + err = n.StartProvisioning(ctx, inputChan) + test.That(t, err, test.ShouldNotBeNil) + + // Validate networking state from after failing to start the provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeFalse) + test.That(t, n.bluetoothIsActive, test.ShouldBeFalse) + + /* + Case 3: Fail to wait for credentials after starting bluetooth provisioning. + */ + bsm.shouldFailToStart = false + bsm.shouldFailToWaitForCredentials = true + + err = n.StartProvisioning(ctx, inputChan) + test.That(t, err, test.ShouldBeNil) // Desired behavior is up to discussion. + + // Validate networking state from after failing to wait for credentials. + test.That(t, n.connState.provisioningMode, test.ShouldBeTrue) + test.That(t, n.bluetoothIsActive, test.ShouldBeTrue) -func TestStopProvisioning(t *testing.T) { + err = n.StopProvisioning() // Need to clean up because it is technically still active. + test.That(t, err, test.ShouldBeNil) + + /* + Case 4: Fail to stop bluetooth provisioning. + */ + bsm.shouldFailToStart = false + bsm.shouldFailToWaitForCredentials = false + bsm.shouldFailToStop = true + + err = n.StartProvisioning(ctx, inputChan) + test.That(t, err, test.ShouldBeNil) + + // Validate networking state from after starting provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeTrue) + test.That(t, n.bluetoothIsActive, test.ShouldBeTrue) + + err = n.StopProvisioning() + test.That(t, err, test.ShouldNotBeNil) + + // Validate networking state from after failing to stop the provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeTrue) + test.That(t, n.bluetoothIsActive, test.ShouldBeTrue) + + /* + Case 5: Successfully stop bluetooth provisioning. + */ + bsm.shouldFailToStop = false + + err = n.StopProvisioning() + test.That(t, err, test.ShouldBeNil) + + // Validate networking state from after failing to stop the provisioning flow. + test.That(t, n.connState.provisioningMode, test.ShouldBeFalse) + test.That(t, n.bluetoothIsActive, test.ShouldBeFalse) + // Need to add a way of restoring WiFi connection to existing before exiting from this test suite. } -func newNetworkingMock(t *testing.T, ctx context.Context, logger logging.Logger) *Networking { - subsystem := NewSubsystem(ctx, logger, utils.DefaultConfig()) +func newNetworkingWithBluetoothServiceMock(t *testing.T, ctx context.Context, logger logging.Logger) (*Networking, *bluetoothServiceMock) { + cfg := utils.DefaultConfig() + cfg.NetworkConfiguration.HotspotSSID = "viam-setup" + subsystem := NewSubsystem(ctx, logger, cfg) networking, ok := subsystem.(*Networking) test.That(t, ok, test.ShouldBeTrue) - networking.bluetoothService = &bluetoothServiceMock{} + bsm := &bluetoothServiceMock{} + networking.bluetoothService = bsm test.That(t, networking.init(ctx), test.ShouldBeNil) - return networking + return networking, bsm } type bluetoothServiceMock struct { + shouldFailToStart bool + shouldFailToWaitForCredentials bool + shouldFailToStop bool } func (bsm *bluetoothServiceMock) start(_ context.Context) error { + if bsm.shouldFailToStart { + return errors.New("mock error: fail to start") + } return nil } func (bsm *bluetoothServiceMock) stop() error { + if bsm.shouldFailToStop { + return errors.New("mock error: fail to stop") + } return nil } -func (bsm *bluetoothServiceMock) waitForCredentials(_ context.Context, _, _ bool, _ chan<- userInput) error { +func (bsm *bluetoothServiceMock) waitForCredentials(ctx context.Context, _, _ bool, _ chan<- userInput) error { + if bsm.shouldFailToWaitForCredentials { + return errors.New("mock error: fail to wait for credentials") + } return nil } From deb212c676d7c20d0fee9dd6bbe8fc30337754db Mon Sep 17 00:00:00 2001 From: Max Horowitz Date: Wed, 26 Feb 2025 10:12:22 -0500 Subject: [PATCH 37/37] Update subsystems/networking/networkmanager_test.go --- subsystems/networking/networkmanager_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/subsystems/networking/networkmanager_test.go b/subsystems/networking/networkmanager_test.go index 333367b..f692e9a 100644 --- a/subsystems/networking/networkmanager_test.go +++ b/subsystems/networking/networkmanager_test.go @@ -16,6 +16,7 @@ func TestProvisioningOverBluetooth(t *testing.T) { n, bsm := newNetworkingWithBluetoothServiceMock(t, ctx, logger) test.That(t, n, test.ShouldNotBeNil) inputChan := make(chan userInput, 1) + defer close(inputChan) /* There is no variability in inputs passed to either StartProvisioning or