Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor BLE connecting to allow scanning for vehicle presence #353

Merged
merged 6 commits into from
Jan 28, 2025

Conversation

Lenart12
Copy link
Contributor

@Lenart12 Lenart12 commented Jan 18, 2025

Description

This PR splits the ble.tryToConnect method in to two parts, by factoring out its first step in which we scan for a suitable tesla BLE advertisement (beacon). This is achieved by putting the code in a new ble.scanVehicleBeacon method along with other smaller refactorings. This allows us to expose the new method with ble.ScanVehicleBeacon.

The end user can then use a different (shorter) timeout to check for the presence of the vehicle before deciding if it wants to try to connect, since if a beacon got detected it would make sense to use a longer timeout to connect, or even do multiple retries, and if it wasn't detected allow for faster error handling.

Example

// Old way
conn, err := ble.NewConnection(ctx, vin)

// New way

// Either try to connect immediatly
conn, err := ble.NewConnection(ctx, vin)
// Or
scanCtx, scanCancel = context.withTimeout(ctx, 1*time.Second)
defer scanCancel()
beacon, err := ble.ScanVehicleBeacon(scanCtx, vin)
if err != nil {
  handleErr(err)
}
conn, err := ble.NewConnectionToBleTarget(ctx, vin, beacon)

Type of change

Please select all options that apply to this change:

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Bug fix (non-breaking change which fixes an issue)
  • Documentation update

Checklist:

Confirm you have completed the following steps:

  • My code follows the style of this project.
  • I have performed a self-review of my code.
  • I have made corresponding updates to the documentation.
  • I have added/updated unit tests to cover my changes.

go func() {
select {
case <-ctx.Done():
cancel()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ctx2 is created from ctx, ctx2 will be terminated automatically when ctx is done.

ch := make(chan Advertisement)
fn := func(a Advertisement) {
ln := a.LocalName()
if ln != localName {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this this is copy/pasted, but we should just if a.LocalName() != localName.

return
}
cancel()
ch <- a
Copy link
Collaborator

@sethterashima sethterashima Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit worried about creating something like go-ble/ble#118 here. I don't believe the interface defines whether or not blocking behavior in fn is allowed. If device.Scan blocks on fn, then there's a deadlock because ch is unbuffered and nothing reads from it until device.Scan finishes. On the other hand, if fn runs asynchronously then we could leak a go routine if two BLE devices with localName respond with advertisements, because the second will block indefinitely while waiting for ch to clear.

Edit: For context, go-ble/ble@c1142dc changed the behavior of Scan on MacOS from non-blocking to blocking, so being agnostic to this requirement would be great, and would probably allow us to uprev the ble package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need help with this change 😅. I'm new to Go and also don't have a Mac to test.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based this on your changes: Lenart12#1

Tested on Linux and MacOS.

var lastError error
for {
conn, err := tryToConnect(ctx, vin)
conn, err := tryToConnect(ctx, vin, target)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Results in redundant calls to VehicleLocalName. Call it once in NewConnectionToBleTarget and pass the result to tryToConnect instead of the VIN.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only way its called twice is when user is calling first ScanVehicleBeacon and then NewConnectionToBleTarget. If calling NewConnection (or NewConnectionToBleTarget(ctx, vin, nil)) it is only generated once in tryToConnect and passed to scanVehicleBeacon. I think avoiding double generation can be solved only by requiring the user to generate the local name and passing it to both the calls, replacing the vin argument in both ScanVehicleBeacon and TryToConnectToBleTarget. Did you mean this?

return nil, ErrMaxConnectionsExceeded
}

log.Debug("Connecting to BLE beacon %s...", client.Addr())
log.Debug("Dialing to %s (%s)...", target.Addr(), localName)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is connecting with target.Addr() significantly faster than scanning? If so, then it might be worth modifying the API to accept target.Addr() instead of target. This could allow clients to cache target.Addr() to use for future connections.

We'd need to consider being able to recover in cases where target.Addr() changes due to a VCSEC replacement, as well as detecting ErrMaxConnectionsExceeded. But optmizing for the common case could be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, but If we would count on "longer term" address caching it would make sense to also seperate this part of the tryToConnect method to its own function, since it adds back the same pain point this PR is trying to address. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine punting that improvement to a different PR. The reason I proposed it because implementing it as part of this one would help limit API churn.

var lastError error
for {
conn, err := tryToConnect(ctx, vin)
conn, err := tryToConnect(ctx, vin, target)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If target.Connectable() is false, we're just going to fail in a tight loop. Should return immediately.

@@ -146,61 +230,51 @@ func NewConnection(ctx context.Context, vin string) (*Connection, error) {
}
}

func tryToConnect(ctx context.Context, vin string) (*Connection, error) {
func tryToConnect(ctx context.Context, vin string, target Advertisement) (*Connection, error, bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function fails the recently merged linter checks because error isn't the final return value. Please fix and rebase off of the latest version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed linters

@sethterashima
Copy link
Collaborator

sethterashima commented Jan 24, 2025

This likely has the side effect of fixing #272 by removing the direct call to device.Connect.

Lenart12 and others added 6 commits January 27, 2025 22:40
The MacOS BLE Scan() method blocks until all queued instances of the
callback function finish. This created a deadlock, since the callback
function would try to write a matching advertisement to an unbuffered
channel, and the unbuffered channel would not be read until Scan()
completed.

To fix the problem, this commit makes the channel buffered, allowing the
callback function to succeed on the first match. The callback handles
subsequent matches by detecting that the scan context has been
canceled and returning.

This commit also uprevs the BLE package. This was previously impossible
because we relied on the device.Connect method, which is broken
upstream on MacOS.
@sethterashima sethterashima merged commit 142e78e into teslamotors:main Jan 28, 2025
1 check passed
@sethterashima
Copy link
Collaborator

Thanks, @Lenart12 ! I think we ended up killing a couple of very annoying birds in the process with this one.

@pkuehnel
Copy link

pkuehnel commented Jan 28, 2025

Is updating the tesla-control CLI docs planned? I am not sure how I should check for the presence of the car. Also, how to check change the timeout and what is the default timeout?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants