diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fd0deb3..a09f4119 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.23.0 + go-version: 1.24.3 - name: No formatting changes run: | @@ -23,9 +23,9 @@ jobs: git diff --exit-code - name: Setup golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v7 with: - version: v1.61.0 + version: v2.1.5 - name: Linter passes run: make linters diff --git a/.golangci.yml b/.golangci.yml index acb44955..6ba9425c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,59 +1,83 @@ ---- -run: - timeout: 4m - -issues: - exclude-dirs: - - temp - exclude: - - "Error return value of `w.Write` is not checked" - - "Error return value of `fmt.Fprintf` is not checked" - - "Error return value of `fmt.Fprintln` is not checked" - - "Error return value of `file.Close` is not checked" - - "Error return value of `os.Setenv` is not checked" - # ECDH code needs to support a Go 1.21 toolchain - - "elliptic.([A-Za-z]+) has been deprecated since Go 1.21: for ECDH" +version: "2" linters: enable: - errcheck - - gofmt - - gosimple - govet - misspell - revive - unused -linters-settings: - revive: + settings: + revive: + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + arguments: + - allowedPackages: + - github.com/onsi/ginkgo + - github.com/onsi/ginkgo/v2 + - github.com/onsi/gomega + - name: empty-block + - name: error-naming + - name: error-return + - name: error-strings + - name: errorf + - name: increment-decrement + - name: indent-error-flow + - name: package-comments + disabled: true + - name: range + - name: receiver-naming + - name: redefines-builtin-id + - name: superfluous-else + - name: time-naming + - name: unexported-return + - name: unreachable-code + - name: unused-parameter + - name: var-declaration + - name: var-naming + - name: exported + + staticcheck: + checks: + - "-QF1008" + + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling rules: - - name: blank-imports - - name: context-as-argument - - name: context-keys-type - - name: dot-imports - arguments: - - allowedPackages: - - "github.com/onsi/ginkgo" - - "github.com/onsi/ginkgo/v2" - - "github.com/onsi/gomega" - - name: empty-block - - name: error-naming - - name: error-return - - name: error-strings - - name: errorf - # Uncomment when all issues are fixed - # - name: exported - - name: increment-decrement - - name: indent-error-flow - - name: package-comments - disabled: true - - name: range - - name: receiver-naming - - name: redefines-builtin-id - - name: superfluous-else - - name: time-naming - - name: unexported-return - - name: unreachable-code - - name: unused-parameter - - name: var-declaration - - name: var-naming + - path: (.+)\.go$ + text: Error return value of `w.Write` is not checked + - path: (.+)\.go$ + text: Error return value of `fmt.Fprintf` is not checked + - path: (.+)\.go$ + text: Error return value of `fmt.Fprintln` is not checked + - path: (.+)\.go$ + text: Error return value of `file.Close` is not checked + - path: (.+)\.go$ + text: Error return value of `os.Setenv` is not checked + - path: (.+)\.go$ # ECDH code needs to support a Go 1.21 toolchain + text: 'elliptic.([A-Za-z]+) has been deprecated since Go 1.21: for ECDH' + paths: + - temp + - third_party$ + - builtin$ + - examples$ + +formatters: + enable: + - gofmt + exclusions: + generated: lax + paths: + - temp + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index f977d93a..20568d1a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -LINTER = golangci-lint run -v $(LINTER_FLAGS) --exclude-use-default=false --timeout $(LINTER_DEADLINE) +LINTER = golangci-lint run -v $(LINTER_FLAGS) --timeout $(LINTER_DEADLINE) LINTER_DEADLINE = 30s LINTER_FLAGS ?= diff --git a/README.md b/README.md index 6021f8b6..c158a223 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ curl --cacert cert.pem \ Requirements: * You've [installed Golang](https://go.dev/doc/install). The package was - tested with Go 1.23.0. + tested with Go 1.24.3. * You're using macOS or Linux. (Everything except BLE should run on Windows, but Windows is not officially supported). diff --git a/cmd/tesla-control/commands.go b/cmd/tesla-control/commands.go index 098b042c..b9f35383 100644 --- a/cmd/tesla-control/commands.go +++ b/cmd/tesla-control/commands.go @@ -168,8 +168,8 @@ func configureFlags(c *cli.Config, commandName string, forceBLE bool) error { } // Verify all required parameters are present. - havePrivateKey := !(c.KeyringKeyName == "" && c.KeyFilename == "") - haveOAuth := !(c.KeyringTokenName == "" && c.TokenFilename == "") + havePrivateKey := c.KeyringKeyName != "" || c.KeyFilename != "" + haveOAuth := c.KeyringTokenName != "" || c.TokenFilename != "" haveVIN := c.VIN != "" _, err := checkReadiness(commandName, havePrivateKey, haveOAuth, haveVIN) return err @@ -1052,9 +1052,10 @@ var commands = map[string]*Command{ return errors.New("expected numeric ID") } return car.RemoveChargeSchedule(ctx, id) - } else { - return errors.New("missing schedule ID") } + + return errors.New("missing schedule ID") + case "HOME": home = true case "WORK": @@ -1156,9 +1157,8 @@ var commands = map[string]*Command{ return errors.New("expected numeric ID") } return car.RemovePreconditionSchedule(ctx, id) - } else { - return errors.New("missing schedule ID") } + return errors.New("missing schedule ID") case "HOME": home = true case "WORK": diff --git a/cmd/tesla-control/main.go b/cmd/tesla-control/main.go index 046ad6e8..c47aee4b 100644 --- a/cmd/tesla-control/main.go +++ b/cmd/tesla-control/main.go @@ -16,7 +16,7 @@ import ( "github.com/teslamotors/vehicle-command/internal/log" "github.com/teslamotors/vehicle-command/pkg/account" "github.com/teslamotors/vehicle-command/pkg/cli" - "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/goble" "github.com/teslamotors/vehicle-command/pkg/protocol" "github.com/teslamotors/vehicle-command/pkg/vehicle" ) @@ -164,8 +164,8 @@ func main() { acct, car, err := config.Connect(ctx) if err != nil { - if ble.IsAdapterError(err) { - writeErr("%s", ble.AdapterErrorHelpMessage(err)) + if goble.IsAdapterError(err) { + writeErr("%s", goble.AdapterErrorHelpMessage(err)) } else { writeErr("Error: %s", err) } diff --git a/examples/ble/main.go b/examples/ble/main.go index 60466445..18bc7a9b 100644 --- a/examples/ble/main.go +++ b/examples/ble/main.go @@ -15,6 +15,8 @@ import ( debugger "github.com/teslamotors/vehicle-command/internal/log" "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/goble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/tinygo" "github.com/teslamotors/vehicle-command/pkg/protocol" "github.com/teslamotors/vehicle-command/pkg/vehicle" ) @@ -23,6 +25,7 @@ func main() { logger := log.New(os.Stderr, "", 0) status := 1 debug := false + useTinyGo := false defer func() { os.Exit(status) }() @@ -38,6 +41,7 @@ func main() { flag.StringVar(&privateKeyFile, "key", "", "Private key `file` for authorizing commands (PEM PKCS8 NIST-P256)") flag.StringVar(&vin, "vin", "", "Vehicle Identification Number (`VIN`) of the car") flag.BoolVar(&debug, "debug", false, "Enable debugging of TX/RX BLE packets") + flag.BoolVar(&useTinyGo, "tinygo", false, "Use tinygo BLE implementation instead of go-ble") if runtime.GOOS == "linux" { flag.StringVar(&btAdapter, "bt-adapter", "", "Optional ID of Bluetooth adapter to use") } @@ -48,13 +52,21 @@ func main() { debugger.SetLevel(debugger.LevelDebug) } - err := ble.InitAdapterWithID(btAdapter) - if err != nil { - if ble.IsAdapterError(err) { - logger.Print(ble.AdapterErrorHelpMessage(err)) - } else { - logger.Printf("Failed to initialize BLE adapter: %s", err) + var adapter ble.Adapter + var err error + + if useTinyGo { + adapter, err = tinygo.NewAdapter(btAdapter) + } else { + adapter, err = goble.NewAdapter(btAdapter) + if err != nil && goble.IsAdapterError(err) { + logger.Print(goble.AdapterErrorHelpMessage(err)) + return } + } + + if err != nil { + logger.Printf("Failed to initialize BLE adapter: %s", err) return } @@ -68,7 +80,7 @@ func main() { defer cancel() doneChan := make(chan struct{}) go func() { - _, err := ble.ScanVehicleBeacon(ctx, vin) + _, err := ble.ScanVehicleBeacon(ctx, vin, adapter) if err != nil && ctx.Err() == nil { logger.Printf("Scan failed: %s", err) } else if ctx.Err() == nil { @@ -105,14 +117,14 @@ func main() { } } - scan, err := ble.ScanVehicleBeacon(ctx, vin) + beacon, err := ble.ScanVehicleBeacon(ctx, vin, adapter) if err != nil { logger.Println(err) return } - logger.Printf("Found vehicle: %s (%s) %ddBm", scan.LocalName, scan.Address, scan.RSSI) + logger.Printf("Found vehicle: %s (%s) %ddBm", beacon.LocalName, beacon.Address, beacon.RSSI) - conn, err := ble.NewConnectionFromScanResult(ctx, vin, scan) + conn, err := ble.NewConnectionFromBeacon(ctx, vin, beacon, adapter) if err != nil { logger.Printf("Failed to connect to vehicle: %s", err) return diff --git a/go.mod b/go.mod index 74d64c2a..2b7db2d5 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,39 @@ module github.com/teslamotors/vehicle-command -go 1.23.0 +go 1.24 require ( github.com/99designs/keyring v1.2.2 github.com/cronokirby/saferith v0.33.0 - github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - golang.org/x/term v0.5.0 - google.golang.org/protobuf v1.34.2 + github.com/zlymeda/go-ble v0.0.0-20250503163836-ac689226ee0c + golang.org/x/term v0.32.0 + google.golang.org/protobuf v1.36.6 + tinygo.org/x/bluetooth v0.12.0 ) require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/JuulLabs-OSS/cbgo v0.0.1 // indirect - github.com/danieljoos/wincred v1.2.0 // indirect - github.com/dvsekhvalnov/jose2go v1.6.0 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect + github.com/dvsekhvalnov/jose2go v1.8.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.6 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect github.com/mtibben/percent v0.2.1 // indirect - github.com/pkg/errors v0.8.1 // indirect - github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 // indirect - github.com/sirupsen/logrus v1.5.0 // indirect - golang.org/x/sys v0.8.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/raff/goble v0.0.0-20200327175727-d63360dcfd80 // indirect + github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af // indirect + github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect + github.com/tinygo-org/cbgo v0.0.4 // indirect + github.com/tinygo-org/pio v0.2.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/sys v0.33.0 // indirect ) - -replace github.com/JuulLabs-OSS/cbgo => github.com/tinygo-org/cbgo v0.0.4 diff --git a/go.sum b/go.sum index 78cd8bef..e0425d4e 100644 --- a/go.sum +++ b/go.sum @@ -2,79 +2,90 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo= github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= -github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= -github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333 h1:bQK6D51cNzMSTyAf0HtM30V2IbljHTDam7jru9JNlJA= -github.com/go-ble/ble v0.0.0-20240122180141-8c5522f54333/go.mod h1:fFJl/jD/uyILGBeD5iQ8tYHrPlJafyqCJzAyTHNJ1Uk= +github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA= +github.com/dvsekhvalnov/jose2go v1.8.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY= github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99 h1:JtoVdxWJ3tgyqtnPq3r4hJ9aULcIDDnPXBWxZsdmqWU= -github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/raff/goble v0.0.0-20200327175727-d63360dcfd80 h1:IZkjNgPZXcE4USkGzmJQyHco3KFLmhcLyFdxCOiY6cQ= +github.com/raff/goble v0.0.0-20200327175727-d63360dcfd80/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4= +github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff h1:cCYo/NzsEvK9MedoaqkVY8kCp4g1QMyKOYlA/uJwO7g= +github.com/saltosystems/winrt-go v0.0.0-20241223121953-98e32661f6ff/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af h1:ZfFq94aH/BCSWWKd9RPUgdHOdgGKCnfl2VdvU9UksTA= +github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af/go.mod h1:MUaGO5m6X7xrkHrPDmnaxCEcuCCFN/0ZFh9oie+exbU= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA= +github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU= +github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= +github.com/zlymeda/go-ble v0.0.0-20250503163836-ac689226ee0c h1:hP1oGamm7+qjPQdKbJsAZcrqD65zJxpJd7I99FPle2k= +github.com/zlymeda/go-ble v0.0.0-20250503163836-ac689226ee0c/go.mod h1:XNq6FUR/SjcD8bfj5Oxz9ziq5Rgn79jNspDz3gbwFG8= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +tinygo.org/x/bluetooth v0.12.0 h1:ztrLZfhcZsmzdpir7lBKNz+Q5Wbd6ZdUB98sYLhXWhw= +tinygo.org/x/bluetooth v0.12.0/go.mod h1:6+y5kVUN6tU7wtJj+qrcFJEVhas4/bIDhGNqvENmT74= diff --git a/internal/dispatcher/dispatcher_test.go b/internal/dispatcher/dispatcher_test.go index 37d868cf..8baec20e 100644 --- a/internal/dispatcher/dispatcher_test.go +++ b/internal/dispatcher/dispatcher_test.go @@ -254,14 +254,18 @@ func (d *dummyConnector) Send(_ context.Context, buffer []byte) error { if !d.AckRequests { return errTimeout } + + d.lock.Lock() + defer d.lock.Unlock() + if len(d.errorQueue) > 0 { - d.lock.Lock() err := d.errorQueue[0] d.errorQueue = d.errorQueue[1:] - d.lock.Unlock() - if err == errDropMessage { + + if errors.Is(err, errDropMessage) { return nil - } else if err != nil { + } + if err != nil { return err } } diff --git a/pkg/cache/example_test.go b/pkg/cache/example_test.go index 49b78e5e..6e76678a 100644 --- a/pkg/cache/example_test.go +++ b/pkg/cache/example_test.go @@ -6,6 +6,7 @@ import ( "github.com/teslamotors/vehicle-command/pkg/cache" "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/goble" "github.com/teslamotors/vehicle-command/pkg/protocol" "github.com/teslamotors/vehicle-command/pkg/vehicle" ) @@ -14,7 +15,12 @@ func Example() { const cacheFilename = "my_cache.json" const privateKeyFilename = "private_key.pem" - conn, err := ble.NewConnection(context.Background(), "myvin123") + adapter, err := goble.NewAdapter("") + if err != nil { + panic(err) + } + + conn, err := ble.NewConnection(context.Background(), "myvin123", adapter) if err != nil { panic(err) } diff --git a/pkg/cli/config.go b/pkg/cli/config.go index 1b22f647..10f20a78 100644 --- a/pkg/cli/config.go +++ b/pkg/cli/config.go @@ -58,14 +58,15 @@ import ( "sort" "strings" + "github.com/99designs/keyring" "github.com/teslamotors/vehicle-command/internal/log" "github.com/teslamotors/vehicle-command/pkg/account" "github.com/teslamotors/vehicle-command/pkg/cache" "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/goble" + "github.com/teslamotors/vehicle-command/pkg/connector/ble/tinygo" "github.com/teslamotors/vehicle-command/pkg/protocol" "github.com/teslamotors/vehicle-command/pkg/vehicle" - - "github.com/99designs/keyring" ) var DomainsByName = map[string]protocol.Domain{ @@ -103,6 +104,36 @@ func (d *DomainList) String() string { return strings.Join(names, ",") } +type BleImpl int + +const ( + Goble BleImpl = iota + TinyGo +) + +func (b *BleImpl) String() string { + switch *b { + case Goble: + return "goble" + case TinyGo: + return "tinygo" + default: + return "unknown" + } +} + +func (b *BleImpl) Set(value string) error { + switch value { + case "goble": + *b = Goble + case "tinygo": + *b = TinyGo + default: + return fmt.Errorf("invalid BLE adapter: %s (valid options: goble, tinygo)", value) + } + return nil +} + // Environment variable names used are used by [Config.ReadFromEnvironment] to set common parameters. const ( EnvTeslaKeyName = "TESLA_KEY_NAME" @@ -145,6 +176,7 @@ type Config struct { KeyringTokenName string // Username for OAuth token in system keyring VIN string BtAdapterID string // ID of Bluetooth adapter to use (Linux only) + BtImpl BleImpl TokenFilename string KeyFilename string CacheFilename string @@ -208,6 +240,9 @@ func (c *Config) RegisterCommandLineFlags() { flag.StringVar(&c.Backend.FileDir, "keyring-file-dir", keyringDirectory, "keyring `directory` for file-backed keyring types") flag.BoolVar(&c.Debug, "keyring-debug", false, "Enable keyring debug logging") } + if c.Flags.isSet(FlagBLE) { + flag.Var(&c.BtImpl, "bt-impl", "BLE implementation to use. Allowed values are \"tinygo\" and \"goble\" (default)") + } c.registerCommandLineFlagsOsSpecific() } @@ -347,7 +382,7 @@ func (c *Config) Connect(ctx context.Context) (acct *account.Account, car *vehic // provided ones. var skey protocol.ECDHPrivateKey skey, err = c.PrivateKey() - if err != nil && err != ErrNoKeySpecified { + if err != nil && !errors.Is(err, ErrNoKeySpecified) { return nil, nil, err } @@ -470,20 +505,27 @@ func (c *Config) ConnectRemote(ctx context.Context, skey protocol.ECDHPrivateKey } // ConnectLocal connects to a vehicle over BLE. -func (c *Config) ConnectLocal(ctx context.Context, skey protocol.ECDHPrivateKey) (car *vehicle.Vehicle, err error) { - err = ble.InitAdapterWithID(c.BtAdapterID) - if err != nil { - return nil, err +func (c *Config) ConnectLocal(ctx context.Context, skey protocol.ECDHPrivateKey) (*vehicle.Vehicle, error) { + var adapter ble.Adapter + var err error + + switch c.BtImpl { + case Goble: + adapter, err = goble.NewAdapter(c.BtAdapterID) + case TinyGo: + adapter, err = tinygo.NewAdapter(c.BtAdapterID) + default: + return nil, fmt.Errorf("unsupported BLE impl: %s", c.BtImpl.String()) } - conn, err := ble.NewConnection(ctx, c.VIN) if err != nil { return nil, err } - car, err = vehicle.NewVehicle(conn, skey, c.sessions) + conn, err := ble.NewConnection(ctx, c.VIN, adapter) if err != nil { return nil, err } - return + + return vehicle.NewVehicle(conn, skey, c.sessions) } diff --git a/pkg/connector/ble/ble.go b/pkg/connector/ble/ble.go index 8d94575f..7d5b2c80 100644 --- a/pkg/connector/ble/ble.go +++ b/pkg/connector/ble/ble.go @@ -1,111 +1,139 @@ -// Package ble implements the vehicle.Connector interface using BLE. - package ble import ( "context" "crypto/sha1" - "errors" "fmt" "sync" "time" - "github.com/go-ble/ble" "github.com/teslamotors/vehicle-command/internal/log" "github.com/teslamotors/vehicle-command/pkg/connector" "github.com/teslamotors/vehicle-command/pkg/protocol" ) -const ( - maxBLEMTUSize = ble.MaxMTU // Max MTU size accepted by the client (this library) - maxBLEMessageSize = 1024 +var ( + ErrMaxConnectionsExceeded = protocol.NewError("the vehicle is already connected to the maximum number of BLE devices", false, false) ) -var ErrAdapterInvalidID = protocol.NewError("the bluetooth adapter ID is invalid", false, false) -var ErrMaxConnectionsExceeded = protocol.NewError("the vehicle is already connected to the maximum number of BLE devices", false, false) +const ( + defaultMTU = 23 + maxBLEMTUSize = 512 + 3 + maxBLEMessageSize = 1024 -var ( rxTimeout = time.Second // Timeout interval between receiving chunks of a mesasge maxLatency = 4 * time.Second // Max allowed error when syncing vehicle clock ) -var ( - vehicleServiceUUID = ble.MustParse("00000211-b2d1-43f0-9b88-960cebf8b91e") - toVehicleUUID = ble.MustParse("00000212-b2d1-43f0-9b88-960cebf8b91e") - fromVehicleUUID = ble.MustParse("00000213-b2d1-43f0-9b88-960cebf8b91e") +const ( + vehicleServiceUUID = "00000211-b2d1-43f0-9b88-960cebf8b91e" + toVehicleUUID = "00000212-b2d1-43f0-9b88-960cebf8b91e" + fromVehicleUUID = "00000213-b2d1-43f0-9b88-960cebf8b91e" ) -var ( - device ble.Device - mu sync.Mutex -) +func VehicleLocalName(vin string) string { + vinBytes := []byte(vin) + digest := sha1.Sum(vinBytes) + return fmt.Sprintf("S%02xC", digest[:8]) +} type Connection struct { - vin string - inbox chan []byte - txChar *ble.Characteristic + vin string + inbox chan []byte + device Device + writer Writer + blockLength int - rxChar *ble.Characteristic inputBuffer []byte - client ble.Client lastRx time.Time lock sync.Mutex } -func (c *Connection) PreferredAuthMethod() connector.AuthMethod { - return connector.AuthMethodGCM +func ScanVehicleBeacon(ctx context.Context, vin string, adapter Adapter) (*Beacon, error) { + return adapter.ScanBeacon(ctx, VehicleLocalName(vin)) } -func (c *Connection) RetryInterval() time.Duration { - return time.Second +func NewConnection(ctx context.Context, vin string, adapter Adapter) (*Connection, error) { + beacon, err := adapter.ScanBeacon(ctx, VehicleLocalName(vin)) + if err != nil { + return nil, err + } + return NewConnectionFromBeacon(ctx, vin, beacon, adapter) } -func (c *Connection) Receive() <-chan []byte { - return c.inbox -} +func NewConnectionFromBeacon(ctx context.Context, vin string, beacon *Beacon, adapter Adapter) (*Connection, error) { + var lastError error -func (c *Connection) flush() bool { - if len(c.inputBuffer) >= 2 { - msgLength := 256*int(c.inputBuffer[0]) + int(c.inputBuffer[1]) - if msgLength > maxBLEMessageSize { - c.inputBuffer = []byte{} - return false + if beacon.LocalName != VehicleLocalName(vin) { + return nil, fmt.Errorf("ble: beacon with unexpected local name: '%s'", beacon.LocalName) + } + + if !beacon.Connectable { + return nil, ErrMaxConnectionsExceeded + } + + for { + conn, err := tryToConnect(ctx, vin, beacon, adapter) + if err == nil { + return conn, nil } - if len(c.inputBuffer) >= 2+msgLength { - buffer := c.inputBuffer[2 : 2+msgLength] - log.Debug("RX: %02x", buffer) - c.inputBuffer = c.inputBuffer[2+msgLength:] - select { - case c.inbox <- buffer: - default: - return false + + log.Warning("BLE connection attempt failed: %+v", err) + if err := ctx.Err(); err != nil { + if lastError != nil { + return nil, lastError } - return true + return nil, err } + lastError = err } - return false } -func (c *Connection) Close() { - _ = c.client.ClearSubscriptions() - _ = c.client.CancelConnection() -} +func tryToConnect(ctx context.Context, vin string, beacon *Beacon, adapter Adapter) (*Connection, error) { + device, err := adapter.Connect(ctx, beacon) + if err != nil { + return nil, err + } -func (c *Connection) AllowedLatency() time.Duration { - return maxLatency -} + service, err := device.Service(ctx, vehicleServiceUUID) + if err != nil { + return nil, err + } -func (c *Connection) rx(p []byte) { - if time.Since(c.lastRx) > rxTimeout { - c.inputBuffer = []byte{} + writer, err := service.Tx(toVehicleUUID) + if err != nil { + return nil, err } - c.lastRx = time.Now() - c.inputBuffer = append(c.inputBuffer, p...) - for c.flush() { + + txMtu, err := writer.MTU(maxBLEMTUSize) + if err != nil { + txMtu = defaultMTU - 3 // Fallback to default MTU size + } else { + txMtu = min(txMtu, maxBLEMessageSize) - 3 // 3 bytes for header } + + conn := &Connection{ + vin: vin, + inbox: make(chan []byte, 5), + device: device, + writer: writer, + + blockLength: txMtu, + } + + err = service.Rx(fromVehicleUUID, conn.rx) + if err != nil { + return nil, err + } + + return conn, nil +} + +func (c *Connection) Receive() <-chan []byte { + return c.inbox } -func (c *Connection) Send(_ context.Context, buffer []byte) error { +func (c *Connection) Send(ctx context.Context, buffer []byte) error { c.lock.Lock() defer c.lock.Unlock() @@ -115,12 +143,21 @@ func (c *Connection) Send(_ context.Context, buffer []byte) error { out = append(out, buffer...) blockLength := c.blockLength for len(out) > 0 { + if ctx.Err() != nil { + return ctx.Err() + } + if blockLength > len(out) { blockLength = len(out) } - if err := c.client.WriteCharacteristic(c.txChar, out[:blockLength], false); err != nil { + + n, err := c.writer.Write(out[:blockLength]) + if err != nil { return err + } else if n != blockLength { + return fmt.Errorf("ble: failed to write %d bytes", blockLength) } + out = out[blockLength:] } return nil @@ -130,231 +167,52 @@ func (c *Connection) VIN() string { return c.vin } -func VehicleLocalName(vin string) string { - vinBytes := []byte(vin) - digest := sha1.Sum(vinBytes) - return fmt.Sprintf("S%02xC", digest[:8]) -} - -// InitAdapterWithID initializes the BLE adapter with the given ID. -// Currently this is only supported on Linux. It is not necessary to -// call this function if using the default adapter, but if not, it -// must be called before making any other BLE calls. -// Linux: -// - id is in the form "hciX" where X is the number of the adapter. -func InitAdapterWithID(id string) error { - mu.Lock() - defer mu.Unlock() - return initAdapter(&id) -} - -// CloseAdapter unsets the BLE adapter so that a new one can be created -// on the next call to InitAdapter. This does not disconnect any existing -// connections or stop any ongoing scans and must be done separately. -func CloseAdapter() error { - mu.Lock() - defer mu.Unlock() - if device != nil { - if err := device.Stop(); err != nil { - return fmt.Errorf("ble: failed to stop device: %s", err) - } - device = nil - log.Debug("Closed BLE adapter") - } - return nil -} - -func initAdapter(id *string) error { - var err error - // We don't want concurrent calls to NewConnection that would defeat - // the point of reusing the existing BLE device. Note that this is not - // an issue on MacOS, but multiple calls to newDevice() on Linux leads to failures. - if device != nil { - log.Debug("Reusing existing BLE device") - } else { - log.Debug("Creating new BLE adapter") - device, err = newAdapter(id) - if err != nil { - return fmt.Errorf("ble: failed to enable device: %s", err) - } +func (c *Connection) Close() { + if err := c.device.Close(); err != nil { + log.Warning("ble: failed to close device: %s", err) } - return nil } -type ScanResult struct { - Address string - LocalName string - RSSI int16 - Connectable bool +func (c *Connection) PreferredAuthMethod() connector.AuthMethod { + return connector.AuthMethodGCM } -func advertisementToScanResult(a ble.Advertisement) *ScanResult { - return &ScanResult{ - Address: a.Addr().String(), - LocalName: a.LocalName(), - RSSI: int16(a.RSSI()), - Connectable: a.Connectable(), - } +func (c *Connection) RetryInterval() time.Duration { + return time.Second } -func ScanVehicleBeacon(ctx context.Context, vin string) (*ScanResult, error) { - mu.Lock() - defer mu.Unlock() - - if err := initAdapter(nil); err != nil { - return nil, err - } - - a, err := scanVehicleBeacon(ctx, VehicleLocalName(vin)) - if err != nil { - return nil, fmt.Errorf("ble: failed to scan for %s: %s", vin, err) - } - return a, nil +func (c *Connection) AllowedLatency() time.Duration { + return maxLatency } -func scanVehicleBeacon(ctx context.Context, localName string) (*ScanResult, error) { - var err error - ctx2, cancel := context.WithCancel(ctx) - defer cancel() - - ch := make(chan ble.Advertisement, 1) - fn := func(a ble.Advertisement) { - if a.LocalName() != localName { - return - } - select { - case ch <- a: - cancel() // Notify device.Scan() that we found a match - case <-ctx2.Done(): - // Another goroutine already found a matching advertisement. We need to return so that - // the MacOS implementation of device.Scan(...) unblocks. - } - } - - if err = device.Scan(ctx2, false, fn); !errors.Is(err, context.Canceled) { - // If ctx rather than ctx2 was canceled, we'll pick that error up below. This is a bit - // hacky, but unfortunately device.Scan() _always_ returns an error on MacOS because it does - // not terminate until the provided context is canceled. - return nil, err +func (c *Connection) rx(p []byte) { + if time.Since(c.lastRx) > rxTimeout { + c.inputBuffer = []byte{} } - - select { - case a, ok := <-ch: - if !ok { - // This should never happen, but just in case - return nil, fmt.Errorf("scan channel closed") - } - return advertisementToScanResult(a), nil - case <-ctx.Done(): - return nil, ctx.Err() + c.lastRx = time.Now() + c.inputBuffer = append(c.inputBuffer, p...) + for c.flush() { } } -func NewConnection(ctx context.Context, vin string) (*Connection, error) { - return NewConnectionFromScanResult(ctx, vin, nil) -} - -// NewConnectionFromScanResult creates a new BLE connection to the given target. -// If target is nil, the vehicle will be scanned for. -func NewConnectionFromScanResult(ctx context.Context, vin string, target *ScanResult) (*Connection, error) { - var lastError error - for { - conn, retry, err := tryToConnect(ctx, vin, target) - if err == nil { - return conn, nil - } - if !retry || IsAdapterError(err) { - return nil, err +func (c *Connection) flush() bool { + if len(c.inputBuffer) >= 2 { + msgLength := 256*int(c.inputBuffer[0]) + int(c.inputBuffer[1]) + if msgLength > maxBLEMessageSize { + c.inputBuffer = []byte{} + return false } - log.Warning("BLE connection attempt failed: %s", err) - if err := ctx.Err(); err != nil { - if lastError != nil { - return nil, lastError + if len(c.inputBuffer) >= 2+msgLength { + buffer := c.inputBuffer[2 : 2+msgLength] + log.Debug("RX: %02x", buffer) + c.inputBuffer = c.inputBuffer[2+msgLength:] + select { + case c.inbox <- buffer: + default: + return false } - return nil, err - } - lastError = err - } -} - -func tryToConnect(ctx context.Context, vin string, target *ScanResult) (*Connection, bool, error) { - var err error - mu.Lock() - defer mu.Unlock() - - if err = initAdapter(nil); err != nil { - return nil, false, err - } - - localName := VehicleLocalName(vin) - - if target == nil { - target, err = scanVehicleBeacon(ctx, localName) - if err != nil { - return nil, true, fmt.Errorf("ble: failed to scan for %s: %s", vin, err) - } - } - - if target.LocalName != localName { - return nil, false, fmt.Errorf("ble: beacon with unexpected local name: '%s'", target.LocalName) - } - - if !target.Connectable { - return nil, false, ErrMaxConnectionsExceeded - } - - log.Debug("Dialing to %s (%s)...", target.Address, localName) - - client, err := device.Dial(ctx, ble.NewAddr(target.Address)) - if err != nil { - return nil, true, fmt.Errorf("ble: failed to dial for %s (%s): %s", vin, localName, err) - } - - log.Debug("Discovering services %s...", client.Addr()) - services, err := client.DiscoverServices([]ble.UUID{vehicleServiceUUID}) - if err != nil { - return nil, true, fmt.Errorf("ble: failed to enumerate device services: %s", err) - } - if len(services) == 0 { - return nil, true, fmt.Errorf("ble: failed to discover service") - } - - characteristics, err := client.DiscoverCharacteristics([]ble.UUID{toVehicleUUID, fromVehicleUUID}, services[0]) - if err != nil { - return nil, true, fmt.Errorf("ble: failed to discover service characteristics: %s", err) - } - - conn := Connection{ - vin: vin, - client: client, - inbox: make(chan []byte, 5), - } - for _, characteristic := range characteristics { - if characteristic.UUID.Equal(toVehicleUUID) { - conn.txChar = characteristic - } else if characteristic.UUID.Equal(fromVehicleUUID) { - conn.rxChar = characteristic - } - if _, err := client.DiscoverDescriptors(nil, characteristic); err != nil { - return nil, true, fmt.Errorf("ble: couldn't fetch descriptors: %s", err) + return true } } - if conn.txChar == nil || conn.rxChar == nil { - return nil, true, fmt.Errorf("ble: failed to find required characteristics") - } - if err := client.Subscribe(conn.rxChar, true, conn.rx); err != nil { - return nil, true, fmt.Errorf("ble: failed to subscribe to RX: %s", err) - } - - txMtu, err := client.ExchangeMTU(maxBLEMTUSize) - if err != nil { - log.Warning("ble: failed to exchange MTU: %s", err) - conn.blockLength = ble.DefaultMTU - 3 // Fallback to default MTU size - } else { - conn.blockLength = min(txMtu, maxBLEMessageSize) - 3 // 3 bytes for header - log.Debug("MTU size: %d", txMtu) - } - - log.Info("Connected to vehicle BLE") - return &conn, false, nil + return false } diff --git a/pkg/connector/ble/goble/adapter.go b/pkg/connector/ble/goble/adapter.go new file mode 100644 index 00000000..51088577 --- /dev/null +++ b/pkg/connector/ble/goble/adapter.go @@ -0,0 +1,77 @@ +package goble + +import ( + "context" + + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/protocol" + goble "github.com/zlymeda/go-ble" +) + +var ErrAdapterInvalidID = protocol.NewError("the bluetooth adapter ID is invalid", false, false) + +func NewAdapter(id string) (ble.Adapter, error) { + device, err := newAdapter(id) + if err != nil { + return nil, err + } + + return &adapter{ + device: device, + }, nil +} + +type adapter struct { + device goble.Device +} + +func (s *adapter) ScanBeacon(ctx context.Context, name string) (*ble.Beacon, error) { + scanCtx, cancel := context.WithCancel(ctx) + defer cancel() + + var result *ble.Beacon + + fn := func(a goble.Advertisement) { + if name != a.LocalName() { + return + } + + result = advertisementToBeacon(a) + cancel() + } + + err := s.device.Scan(scanCtx, false, fn) + if err != nil && result == nil { + return nil, err + } + + return result, nil +} + +func (s *adapter) Connect(ctx context.Context, beacon *ble.Beacon) (ble.Device, error) { + client, err := s.device.Dial(ctx, goble.NewAddr(beacon.Address)) + if err != nil { + return nil, err + } + + return &device{client: client}, nil +} + +func (s *adapter) Close() error { + if s.device == nil { + return nil + } + + device := s.device + s.device = nil + return device.Stop() +} + +func advertisementToBeacon(a goble.Advertisement) *ble.Beacon { + return &ble.Beacon{ + Address: a.Addr().String(), + LocalName: a.LocalName(), + RSSI: int16(a.RSSI()), + Connectable: a.Connectable(), + } +} diff --git a/pkg/connector/ble/goble/device.go b/pkg/connector/ble/goble/device.go new file mode 100644 index 00000000..2d4479d3 --- /dev/null +++ b/pkg/connector/ble/goble/device.go @@ -0,0 +1,40 @@ +package goble + +import ( + "context" + "errors" + "fmt" + + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + goble "github.com/zlymeda/go-ble" +) + +type device struct { + client goble.Client +} + +func (c *device) Service(_ context.Context, uuid string) (ble.Service, error) { + services, err := c.client.DiscoverServices([]goble.UUID{goble.MustParse(uuid)}) + if err != nil { + return nil, fmt.Errorf("ble: failed to enumerate device services: %s", err) + } + if len(services) == 0 { + return nil, fmt.Errorf("ble: failed to discover service") + } + + return &service{client: c.client, service: services[0]}, nil +} + +func (c *device) Close() error { + if c.client == nil { + return nil + } + + client := c.client + c.client = nil + + err1 := client.ClearSubscriptions() + err2 := client.CancelConnection() + + return errors.Join(err1, err2) +} diff --git a/pkg/connector/ble/device_darwin.go b/pkg/connector/ble/goble/device_darwin.go similarity index 74% rename from pkg/connector/ble/device_darwin.go rename to pkg/connector/ble/goble/device_darwin.go index 53fdedba..1d03c973 100644 --- a/pkg/connector/ble/device_darwin.go +++ b/pkg/connector/ble/goble/device_darwin.go @@ -1,9 +1,9 @@ -package ble +package goble import ( - "github.com/go-ble/ble" - "github.com/go-ble/ble/darwin" "github.com/teslamotors/vehicle-command/internal/log" + "github.com/zlymeda/go-ble" + "github.com/zlymeda/go-ble/darwin" ) func IsAdapterError(_ error) bool { @@ -15,8 +15,8 @@ func AdapterErrorHelpMessage(err error) string { return err.Error() } -func newAdapter(id *string) (ble.Device, error) { - if id != nil && *id != "" { +func newAdapter(id string) (ble.Device, error) { + if id != "" { log.Warning("Darwin does not support specifying a Bluetooth adapter ID") return nil, ErrAdapterInvalidID } diff --git a/pkg/connector/ble/device_linux.go b/pkg/connector/ble/goble/device_linux.go similarity index 84% rename from pkg/connector/ble/device_linux.go rename to pkg/connector/ble/goble/device_linux.go index 61cd508a..ccabc85f 100644 --- a/pkg/connector/ble/device_linux.go +++ b/pkg/connector/ble/goble/device_linux.go @@ -1,4 +1,4 @@ -package ble +package goble import ( "os" @@ -6,9 +6,9 @@ import ( "strings" "time" - "github.com/go-ble/ble" - "github.com/go-ble/ble/linux" - "github.com/go-ble/ble/linux/hci/cmd" + "github.com/zlymeda/go-ble" + "github.com/zlymeda/go-ble/linux" + "github.com/zlymeda/go-ble/linux/hci/cmd" ) func IsAdapterError(err error) bool { @@ -35,17 +35,17 @@ var scanParams = cmd.LESetScanParameters{ ScanningFilterPolicy: 2, // Basic filtered } -func newAdapter(id *string) (ble.Device, error) { +func newAdapter(id string) (ble.Device, error) { opts := []ble.Option{ ble.OptDialerTimeout(bleTimeout), ble.OptListenerTimeout(bleTimeout), ble.OptScanParams(scanParams), } - if id != nil && *id != "" { - if !strings.HasPrefix(*id, "hci") { + if id != "" { + if !strings.HasPrefix(id, "hci") { return nil, ErrAdapterInvalidID } - hciStr := strings.TrimPrefix(*id, "hci") + hciStr := strings.TrimPrefix(id, "hci") hciID, err := strconv.Atoi(hciStr) if err != nil || hciID < 0 || hciID > 15 { return nil, ErrAdapterInvalidID diff --git a/pkg/connector/ble/device_windows.go b/pkg/connector/ble/goble/device_windows.go similarity index 72% rename from pkg/connector/ble/device_windows.go rename to pkg/connector/ble/goble/device_windows.go index ad1d1ec5..8b85196c 100644 --- a/pkg/connector/ble/device_windows.go +++ b/pkg/connector/ble/goble/device_windows.go @@ -1,9 +1,9 @@ -package ble +package goble import ( "errors" - "github.com/go-ble/ble" + "github.com/zlymeda/go-ble" ) func IsAdapterError(_ error) bool { @@ -15,6 +15,6 @@ func AdapterErrorHelpMessage(err error) string { return err.Error() } -func newAdapter(_ *string) (ble.Device, error) { +func newAdapter(_ string) (ble.Device, error) { return nil, errors.New("not supported on Windows") } diff --git a/pkg/connector/ble/goble/service.go b/pkg/connector/ble/goble/service.go new file mode 100644 index 00000000..66a0755a --- /dev/null +++ b/pkg/connector/ble/goble/service.go @@ -0,0 +1,65 @@ +package goble + +import ( + "bytes" + "fmt" + + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + goble "github.com/zlymeda/go-ble" +) + +type service struct { + client goble.Client + service *goble.Service +} + +func (s *service) Rx(uuid string, callback func(buf []byte)) error { + characteristic, err := s.discover(uuid) + if err != nil { + return err + } + + if err := s.client.Subscribe(characteristic, true, callback); err != nil { + return fmt.Errorf("ble: failed to subscribe to RX: %s", err) + } + + return nil +} + +func (s *service) Tx(uuid string) (ble.Writer, error) { + characteristic, err := s.discover(uuid) + if err != nil { + return nil, err + } + + return &writer{ + characteristic: characteristic, + client: s.client, + }, nil +} + +func (s *service) discover(uuidStr string) (*goble.Characteristic, error) { + uuid := goble.MustParse(uuidStr) + characteristics, err := s.client.DiscoverCharacteristics([]goble.UUID{uuid}, s.service) + if err != nil { + return nil, fmt.Errorf("ble: failed to discover service characteristics: %s", err) + } + + var characteristic *goble.Characteristic + for _, char := range characteristics { + if bytes.Equal(char.UUID, uuid) { + characteristic = char + break + } + } + + if characteristic == nil { + return nil, fmt.Errorf("ble: failed to discover service characteristics: %s", err) + } + + if _, err := s.client.DiscoverDescriptors(nil, characteristic); err != nil { + return nil, fmt.Errorf("ble: couldn't fetch descriptors: %s", err) + } + + return characteristic, nil +} diff --git a/pkg/connector/ble/goble/writer.go b/pkg/connector/ble/goble/writer.go new file mode 100644 index 00000000..650497fe --- /dev/null +++ b/pkg/connector/ble/goble/writer.go @@ -0,0 +1,21 @@ +package goble + +import "github.com/zlymeda/go-ble" + +type writer struct { + characteristic *ble.Characteristic + client ble.Client +} + +func (w *writer) Write(bytes []byte) (int, error) { + err := w.client.WriteCharacteristic(w.characteristic, bytes, false) + if err != nil { + return 0, err + } + + return len(bytes), err +} + +func (w *writer) MTU(rxMTU int) (txMTU int, err error) { + return w.client.ExchangeMTU(rxMTU) +} diff --git a/pkg/connector/ble/iface.go b/pkg/connector/ble/iface.go new file mode 100644 index 00000000..c5e6e66a --- /dev/null +++ b/pkg/connector/ble/iface.go @@ -0,0 +1,34 @@ +package ble + +import ( + "context" + "io" +) + +type Beacon struct { + Address string + LocalName string + RSSI int16 + Connectable bool +} + +type Adapter interface { + ScanBeacon(ctx context.Context, name string) (*Beacon, error) + Connect(ctx context.Context, beacon *Beacon) (Device, error) + Close() error +} + +type Device interface { + Service(ctx context.Context, uuid string) (Service, error) + Close() error +} + +type Service interface { + Rx(uuid string, callback func(buf []byte)) error + Tx(uuid string) (Writer, error) +} + +type Writer interface { + io.Writer + MTU(rxMTU int) (txMTU int, err error) +} diff --git a/pkg/connector/ble/tinygo/adapter.go b/pkg/connector/ble/tinygo/adapter.go new file mode 100644 index 00000000..0ea90181 --- /dev/null +++ b/pkg/connector/ble/tinygo/adapter.go @@ -0,0 +1,112 @@ +package tinygo + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/teslamotors/vehicle-command/internal/log" + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "github.com/teslamotors/vehicle-command/pkg/protocol" + "tinygo.org/x/bluetooth" +) + +var ErrAdapterInvalidID = protocol.NewError("the bluetooth adapter ID is invalid", false, false) + +func NewAdapter(id string) (ble.Adapter, error) { + device, err := newAdapter(id) + if err != nil { + return nil, fmt.Errorf("ble: failed to create device: %s", err) + } + if err = device.Enable(); err != nil { + return nil, fmt.Errorf("ble: failed to enable device: %s", err) + } + + return &adapter{ + device: device, + }, nil +} + +type adapter struct { + device *bluetooth.Adapter +} + +func (s *adapter) ScanBeacon(ctx context.Context, name string) (*ble.Beacon, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + stopScan := func() { + err := s.device.StopScan() + if err != nil { + if strings.Contains(err.Error(), "no scan in progress") { + return + } + log.Warning("ble: failed to stop scan: %+v", err) + } + } + + scanCtx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + <-scanCtx.Done() + stopScan() + }() + + var result *ble.Beacon + err := s.device.Scan(func(_ *bluetooth.Adapter, a bluetooth.ScanResult) { + if a.LocalName() == name { + result = advertisementToBeacon(a) + stopScan() + } + }) + + if err != nil { + return nil, err + } + + if result == nil { + return nil, scanCtx.Err() + } + + return result, err +} + +func (s *adapter) Connect(ctx context.Context, beacon *ble.Beacon) (ble.Device, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + params := bluetooth.ConnectionParams{} + if deadline, ok := ctx.Deadline(); ok { + params.ConnectionTimeout = bluetooth.NewDuration(time.Until(deadline)) + } + + addr, err := parseAddress(beacon.Address) + if err != nil { + return nil, err + } + + client, err := s.device.Connect(addr, params) + if err != nil { + return nil, err + } + + return &device{client: &client}, nil +} + +func (s *adapter) Close() error { + s.device = nil + return nil +} + +func advertisementToBeacon(result bluetooth.ScanResult) *ble.Beacon { + return &ble.Beacon{ + Address: result.Address.String(), + LocalName: result.LocalName(), + RSSI: result.RSSI, + Connectable: true, + } +} diff --git a/pkg/connector/ble/tinygo/device.go b/pkg/connector/ble/tinygo/device.go new file mode 100644 index 00000000..ed906d20 --- /dev/null +++ b/pkg/connector/ble/tinygo/device.go @@ -0,0 +1,35 @@ +package tinygo + +import ( + "context" + "fmt" + + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "tinygo.org/x/bluetooth" +) + +type device struct { + client *bluetooth.Device +} + +func (c *device) Service(_ context.Context, uuid string) (ble.Service, error) { + services, err := c.client.DiscoverServices([]bluetooth.UUID{mustParseUUID(uuid)}) + if err != nil { + return nil, fmt.Errorf("ble: failed to enumerate device services: %s", err) + } + if len(services) != 1 { + return nil, fmt.Errorf("ble: failed to discover service") + } + + return &service{client: c.client, service: services[0]}, nil +} + +func (c *device) Close() error { + if c.client == nil { + return nil + } + + client := c.client + c.client = nil + return client.Disconnect() +} diff --git a/pkg/connector/ble/tinygo/device_darwin.go b/pkg/connector/ble/tinygo/device_darwin.go new file mode 100644 index 00000000..00df5457 --- /dev/null +++ b/pkg/connector/ble/tinygo/device_darwin.go @@ -0,0 +1,39 @@ +package tinygo + +import ( + "fmt" + "tinygo.org/x/bluetooth" +) + +func IsAdapterError(_ error) bool { + // TODO: Add check for Darwin + return false +} + +func AdapterErrorHelpMessage(err error) string { + return err.Error() +} + +func newAdapter(id string) (*bluetooth.Adapter, error) { + if id != "" { + // TODO: Add support for Darwin + return nil, ErrAdapterInvalidID + } + + return bluetooth.DefaultAdapter, nil +} + +var ( + deviceCharacteristicWrite = bluetooth.DeviceCharacteristic.Write +) + +func parseAddress(address string) (bluetooth.Address, error) { + uuid, err := bluetooth.ParseUUID(address) + if err != nil { + return bluetooth.Address{}, fmt.Errorf("ble: failed to parse MAC address: %s", err) + } + + return bluetooth.Address{ + UUID: uuid, + }, nil +} diff --git a/pkg/connector/ble/tinygo/device_linux.go b/pkg/connector/ble/tinygo/device_linux.go new file mode 100644 index 00000000..cfc9a987 --- /dev/null +++ b/pkg/connector/ble/tinygo/device_linux.go @@ -0,0 +1,51 @@ +package tinygo + +import ( + "fmt" + "strings" + + "tinygo.org/x/bluetooth" +) + +func IsAdapterError(err error) bool { + // D-Bus not found + if strings.Contains(err.Error(), "dbus") && strings.HasSuffix(err.Error(), "no such file or directory") { + return true + } + // D-Bus is running but org.bluez is not found + if strings.Contains(err.Error(), "The name org.bluez was not provided by any .service files") { + return true + } + return false +} + +func AdapterErrorHelpMessage(err error) string { + return "Failed to initialize BLE adapter: \n\t" + err.Error() + "\n" + + "Make sure bluez and dbus are installed and running.\n" + + "If running in a container, make sure the container has access to the host's D-Bus socket. (e.g. -v /var/run/dbus:/var/run/dbus)" +} + +func newAdapter(id string) (*bluetooth.Adapter, error) { + if id != "" { + return bluetooth.NewAdapter(id), nil + } + + return bluetooth.DefaultAdapter, nil +} + +var ( + deviceCharacteristicWrite = bluetooth.DeviceCharacteristic.WriteWithoutResponse +) + +func parseAddress(address string) (bluetooth.Address, error) { + mac, err := bluetooth.ParseMAC(address) + if err != nil { + return bluetooth.Address{}, fmt.Errorf("ble: failed to parse MAC address: %s", err) + } + + return bluetooth.Address{ + MACAddress: bluetooth.MACAddress{ + MAC: mac, + }, + }, nil +} diff --git a/pkg/connector/ble/tinygo/device_windows.go b/pkg/connector/ble/tinygo/device_windows.go new file mode 100644 index 00000000..edf496a5 --- /dev/null +++ b/pkg/connector/ble/tinygo/device_windows.go @@ -0,0 +1,40 @@ +package tinygo + +import ( + "tinygo.org/x/bluetooth" +) + +func IsAdapterError(_ error) bool { + // TODO: Add check for Windows + return false +} + +func AdapterErrorHelpMessage(err error) string { + return err.Error() +} + +func newAdapter(id string) (*bluetooth.Adapter, error) { + if id != "" { + // TODO: Add support for Windows + return nil, ErrAdapterInvalidID + } + + return bluetooth.DefaultAdapter, nil +} + +var ( + deviceCharacteristicWrite = bluetooth.DeviceCharacteristic.WriteWithoutResponse +) + +func parseAddress(address string) (bluetooth.Address, error) { + mac, err := bluetooth.ParseMAC(address) + if err != nil { + return bluetooth.Address{}, fmt.Errorf("ble: failed to parse MAC address: %s", err) + } + + return bluetooth.Address{ + MACAddress: bluetooth.MACAddress{ + MAC: mac, + }, + }, nil +} diff --git a/pkg/connector/ble/tinygo/service.go b/pkg/connector/ble/tinygo/service.go new file mode 100644 index 00000000..a10e2dd9 --- /dev/null +++ b/pkg/connector/ble/tinygo/service.go @@ -0,0 +1,60 @@ +package tinygo + +import ( + "fmt" + + "github.com/teslamotors/vehicle-command/pkg/connector/ble" + "tinygo.org/x/bluetooth" +) + +type service struct { + client *bluetooth.Device + service bluetooth.DeviceService +} + +func (s *service) Rx(uuid string, callback func(buf []byte)) error { + characteristic, err := s.discover(uuid) + if err != nil { + return err + } + + if err := characteristic.EnableNotifications(callback); err != nil { + return fmt.Errorf("ble: failed to subscribe to RX: %s", err) + } + + return nil +} + +func (s *service) Tx(uuid string) (ble.Writer, error) { + characteristic, err := s.discover(uuid) + if err != nil { + return nil, err + } + + return &writer{ + characteristic: characteristic, + client: s.client, + }, nil +} + +func (s *service) discover(uuid string) (bluetooth.DeviceCharacteristic, error) { + characteristics, err := s.service.DiscoverCharacteristics([]bluetooth.UUID{mustParseUUID(uuid)}) + if err != nil { + return bluetooth.DeviceCharacteristic{}, fmt.Errorf("ble: failed to discover service characteristics: %s", err) + } + + if len(characteristics) == 0 { + return bluetooth.DeviceCharacteristic{}, fmt.Errorf("ble: failed to discover service characteristics: %s", err) + } + + return characteristics[0], nil +} + +func mustParseUUID(uuid string) bluetooth.UUID { + uuidParsed, err := bluetooth.ParseUUID(uuid) + if err != nil { + panic(err) + } + + return uuidParsed +} diff --git a/pkg/connector/ble/tinygo/writer.go b/pkg/connector/ble/tinygo/writer.go new file mode 100644 index 00000000..8e1d8d6f --- /dev/null +++ b/pkg/connector/ble/tinygo/writer.go @@ -0,0 +1,19 @@ +package tinygo + +import ( + "tinygo.org/x/bluetooth" +) + +type writer struct { + characteristic bluetooth.DeviceCharacteristic + client *bluetooth.Device +} + +func (w *writer) Write(bytes []byte) (int, error) { + return deviceCharacteristicWrite(w.characteristic, bytes) +} + +func (w *writer) MTU(_ int) (txMTU int, err error) { + mtu, err := w.characteristic.GetMTU() + return int(mtu), err +} diff --git a/pkg/protocol/key.go b/pkg/protocol/key.go index eb3f65e4..1761a8c4 100644 --- a/pkg/protocol/key.go +++ b/pkg/protocol/key.go @@ -89,27 +89,29 @@ func LoadPublicKey(filename string) (*ecdh.PublicKey, error) { if err != nil { return nil, err } - if ecdsaPrivateKey, ok := skey.(*ecdsa.PrivateKey); !ok { + ecdsaPrivateKey, ok := skey.(*ecdsa.PrivateKey) + if !ok { return nil, ErrInvalidPublicKey - } else { - pkey, err = ecdsaPrivateKey.PublicKey.ECDH() - if err != nil { - return nil, err - } } + pkey, err = ecdsaPrivateKey.PublicKey.ECDH() + if err != nil { + return nil, err + } + case "PUBLIC KEY": publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, err } - if ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey); !ok { + ecdsaPublicKey, ok := publicKey.(*ecdsa.PublicKey) + if !ok { return nil, ErrInvalidPublicKey - } else { - pkey, err = ecdsaPublicKey.ECDH() - if err != nil { - return nil, err - } } + pkey, err = ecdsaPublicKey.ECDH() + if err != nil { + return nil, err + } + default: return nil, fmt.Errorf("unrecognized PEM block type %s", block.Type) } diff --git a/pkg/sign/sign.go b/pkg/sign/sign.go index a466a7b3..2318bdd4 100644 --- a/pkg/sign/sign.go +++ b/pkg/sign/sign.go @@ -1,3 +1,4 @@ +//revive:disable:exported package sign import (