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

Fetch limited vehicle info over BLE #263

Merged
merged 2 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion cmd/tesla-control/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/keys"
"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/vcsec"
"github.com/teslamotors/vehicle-command/pkg/vehicle"
"google.golang.org/protobuf/encoding/protojson"
)

var ErrCommandLineArgs = errors.New("invalid command line arguments")
Expand All @@ -34,6 +35,7 @@ type Command struct {
args []Argument
optional []Argument
handler Handler
domain protocol.Domain
}

// configureAndVerifyFlags verifies that c contains all the information required to execute a command.
Expand All @@ -43,6 +45,9 @@ func configureFlags(c *cli.Config, commandName string, forceBLE bool) error {
return ErrUnknownCommand
}
c.Flags = cli.FlagBLE
if info.domain != protocol.DomainNone {
c.Domains = cli.DomainList{info.domain}
}
bleWake := forceBLE && commandName == "wake"
if bleWake || info.requiresAuth {
// Wake commands are special. When sending a wake command over the Internet, infotainment
Expand All @@ -55,7 +60,7 @@ func configureFlags(c *cli.Config, commandName string, forceBLE bool) error {
// One handshake with VCSEC, one handshake with infotainment. However, if we're sending a
// BLE wake command, then infotainment is (presumably) asleep, and so we should only try to
// handshake with VCSEC.
c.DomainNames = []string{"VCSEC"}
c.Domains = cli.DomainList{protocol.DomainVCSEC}
}
if !info.requiresFleetAPI {
c.Flags |= cli.FlagVIN
Expand Down Expand Up @@ -794,4 +799,24 @@ var commands = map[string]*Command{
return car.CloseWindows(ctx)
},
},
"body-controller-state": &Command{
help: "Fetch limited vehicle state information. Works over BLE when infotainment is asleep.",
domain: protocol.DomainVCSEC,
requiresAuth: false,
requiresFleetAPI: false,
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
info, err := car.BodyControllerState(ctx)
if err != nil {
return err
}
options := protojson.MarshalOptions{
Indent: "\t",
UseEnumNumbers: false,
EmitUnpopulated: false,
EmitDefaultValues: true,
}
fmt.Println(options.Format(info))
return nil
},
},
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
golang.org/x/term v0.5.0
google.golang.org/protobuf v1.28.1
google.golang.org/protobuf v1.34.2
)

require (
Expand Down
8 changes: 2 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633 h1:ZrzoZQz1CF33SPHLkjRp
github.com/go-ble/ble v0.0.0-20220207185428-60d1eecf2633/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
Expand Down Expand Up @@ -66,10 +64,8 @@ golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
60 changes: 30 additions & 30 deletions pkg/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,34 +67,39 @@ import (
"github.com/99designs/keyring"
)

// domainNames is used to translate domains provided at the command line into native protocol.Domain
// values.
type domainNames []string
var DomainsByName = map[string]protocol.Domain{
"VCSEC": protocol.DomainVCSEC,
"INFOTAINMENT": protocol.DomainInfotainment,
}

func (d *domainNames) Set(value string) error {
*d = append(*d, value)
return nil
var DomainNames = map[protocol.Domain]string{
protocol.DomainVCSEC: "VCSEC",
protocol.DomainInfotainment: "INFOTAINMENT",
}

func (d *domainNames) String() string {
return strings.Join(*d, ",")
// DomainList is used to translate domains provided at the command line into native protocol.Domain
// values.
type DomainList []protocol.Domain

// Set updates a DomainList from a command-line argument.
func (d *DomainList) Set(value string) error {
canonicalName := strings.ToUpper(value)
if domain, ok := DomainsByName[canonicalName]; ok {
*d = append(*d, domain)
} else {
return fmt.Errorf("unknown domain '%s'", value)
}
return nil
}

func (d *domainNames) ToDomains() ([]protocol.Domain, error) {
mapping := map[string]protocol.Domain{
"VCSEC": protocol.DomainVCSEC,
"INFOTAINMENT": protocol.DomainInfotainment,
}
var domains []protocol.Domain
for _, name := range *d {
canonicalName := strings.ToUpper(name)
if domain, ok := mapping[canonicalName]; ok {
domains = append(domains, domain)
} else {
return nil, fmt.Errorf("unknown domain '%s'", name)
func (d *DomainList) String() string {
var names []string
for _, domain := range *d {
if name, ok := DomainNames[domain]; ok {
names = append(names, name)
}
}
return domains, nil
return strings.Join(names, ",")
}

// Environment variable names used are used by [Config.ReadFromEnvironment] to set common parameters.
Expand Down Expand Up @@ -145,9 +150,9 @@ type Config struct {
BackendType backendType
Debug bool // Enable keyring debug messages

// DomainNames can limit a vehicle connection to relevant subsystems, which can reduce
// Domains can limit a vehicle connection to relevant subsystems, which can reduce
// connection latency and avoid waking up the infotainment system unnecessarily.
DomainNames domainNames
Domains DomainList

password *string
sessions *cache.SessionCache
Expand Down Expand Up @@ -183,7 +188,7 @@ func (c *Config) RegisterCommandLineFlags() {
flag.StringVar(&c.CacheFilename, "session-cache", "", "Load session info cache from `file`. Defaults to $TESLA_CACHE_FILE.")
flag.StringVar(&c.KeyringKeyName, "key-name", "", "System keyring `name` for private key. Defaults to $TESLA_KEY_NAME.")
flag.StringVar(&c.KeyFilename, "key-file", "", "A `file` containing private key. Defaults to $TESLA_KEY_FILE.")
flag.Var(&c.DomainNames, "domain", "Domains to connect to (can be repeated; omit for all)")
flag.Var(&c.Domains, "domain", "Domains to connect to (can be repeated; omit for all)")
}
if c.Flags.isSet(FlagOAuth) {
flag.StringVar(&c.KeyringTokenName, "token-name", "", "System keyring `name` for OAuth token. Defaults to $TESLA_TOKEN_NAME.")
Expand Down Expand Up @@ -366,12 +371,7 @@ func (c *Config) Connect(ctx context.Context) (acct *account.Account, car *vehic
}
if skey != nil {
log.Info("Securing connection...")
domains, err := c.DomainNames.ToDomains()
if err != nil {
return nil, nil, err
}

if err := car.StartSession(ctx, domains); err != nil {
if err := car.StartSession(ctx, c.Domains); err != nil {
return nil, nil, err
}
}
Expand Down
24 changes: 24 additions & 0 deletions pkg/cli/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cli_test

import (
"github.com/teslamotors/vehicle-command/pkg/cli"
"testing"
)

func TestDomainCLI(t *testing.T) {
var d cli.DomainList
if d.Set("DoesNotExist") == nil {
t.Error("Expected error when parsing invalid domain name")
}
// Uppercase
if err := d.Set("VCSEC"); err != nil {
t.Errorf("Unexpected error when parsing VCSEC: %s", err)
}
// Mixed case
if err := d.Set("infoTainMenT"); err != nil {
t.Errorf("Unexpected error when parsing mixed-case domain name: %s", err)
}
if s := d.String(); s != "VCSEC,INFOTAINMENT" {
t.Errorf("Unexpected string conversion result: %s", s)
}
}
1 change: 1 addition & 0 deletions pkg/protocol/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
type Domain = universal.Domain

const (
DomainNone = universal.Domain_DOMAIN_BROADCAST
// DomainVCSEC handles (un)lock, remote start drive, keychain management commands.
DomainVCSEC = universal.Domain_DOMAIN_VEHICLE_SECURITY
// DomainInfotainment handles commands that terminate on the vehicle's infotainment system.
Expand Down
54 changes: 54 additions & 0 deletions pkg/protocol/protobuf/vcsec.proto
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,63 @@ message UnsignedMessage {
}
}

enum ClosureState_E {
CLOSURESTATE_CLOSED = 0;
CLOSURESTATE_OPEN = 1;
CLOSURESTATE_AJAR = 2;
CLOSURESTATE_UNKNOWN = 3;
CLOSURESTATE_FAILED_UNLATCH = 4;
CLOSURESTATE_OPENING = 5;
CLOSURESTATE_CLOSING = 6;
}

enum VehicleLockState_E {
VEHICLELOCKSTATE_UNLOCKED = 0;
VEHICLELOCKSTATE_LOCKED = 1;
VEHICLELOCKSTATE_INTERNAL_LOCKED = 2;
VEHICLELOCKSTATE_SELECTIVE_UNLOCKED = 3;
}

message ClosureStatuses
{
ClosureState_E frontDriverDoor = 1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: can we make 0 as UNKNOWN?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This isn't an enum type; those are field numbers, not enum values. :)

ClosureState_E frontPassengerDoor = 2;
ClosureState_E rearDriverDoor = 3;
ClosureState_E rearPassengerDoor = 4;
ClosureState_E rearTrunk = 5;
ClosureState_E frontTrunk = 6;
ClosureState_E chargePort = 7;
ClosureState_E tonneau = 8;
}

enum VehicleSleepStatus_E {
VEHICLE_SLEEP_STATUS_UNKNOWN = 0;
VEHICLE_SLEEP_STATUS_AWAKE = 1;
VEHICLE_SLEEP_STATUS_ASLEEP = 2;
}

message DetailedClosureStatus {
uint32 tonneauPercentOpen = 1;
}

enum UserPresence_E {
VEHICLE_USER_PRESENCE_UNKNOWN = 0;
VEHICLE_USER_PRESENCE_NOT_PRESENT = 1;
VEHICLE_USER_PRESENCE_PRESENT = 2;
}

message VehicleStatus {
ClosureStatuses closureStatuses = 1;
VehicleLockState_E vehicleLockState = 2;
VehicleSleepStatus_E vehicleSleepStatus = 3;
UserPresence_E userPresence = 4;
DetailedClosureStatus detailedClosureStatus = 5;
}

message FromVCSECMessage {
reserved 6 to 10;
oneof sub_message {
VehicleStatus vehicleStatus = 1;
CommandStatus commandStatus = 4;
WhitelistInfo whitelistInfo = 16;
WhitelistEntryInfo whitelistEntryInfo = 17;
Expand Down
Loading
Loading