Skip to content

Tesla BLE vehicle-command integration (Phases 0-4) - #58

Merged
wpmed92 merged 5 commits into
softwiredtech:mainfrom
dkneeland:feat/tesla-ble
Aug 24, 2026
Merged

wpmed92 merged 5 commits into
softwiredtech:mainfrom
dkneeland:feat/tesla-ble

Conversation

@dkneeland

@dkneeland dkneeland commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Full Tesla BLE (vehicle-command) integration, Phases 0-4 of the implementation plan, developed and on-car validated on the dkneeland fork. This PR brings the whole line into the official repo so others can flash and test the BLE onboarding flow.

What's included

  • Phase 0: components/tesla-protocol/ - vendored Apache-2.0 Tesla protos (pinned in protos/VERSION) + nanopb runtime + crypto/session layers ported to mbedTLS 3.6, validated against protocol.md known-answer vectors (61/61 host tests + session/enrollment suites).
  • Phase 2: Central connect, GATT discovery (svc 0211, write 0212, indicate 0213), VCSEC handshake, GET_STATUS poll; response anti-replay + GCM decrypt per review.
  • Phase 3: Present-key enrollment (card tap + touchscreen confirm), role CHARGING_MANAGER, NVS persistence - on-car validated 2026-08-19.
  • Phase 4: App channel - new app-channel GATT service (ble_appchan.c, CADA02xx) with app-triggered-only pairing. The phone discovers + provisions the car's VIN/MAC via opcode 0x04; 0x01 starts enrollment, 0x02 factory-resets, 0x03 cancels. Includes staged-car state and fault reporting. The firmware never self-arms pairing and ships no BLE observer (Phase 1 was cut in the scope audit - the app stages the car instead of the firmware scanning).
  • Follow-up fixes: persistent-poll client model with debounced not-connected reporting; link-only tap-window keepalive; fast connect-failure; KeyIdentifier.publicKeySHA1 as dynamic bytes (real cars send truncated 4-byte ids); whitelist-result classifier; pairing-window/enrolled report timing aligned with real car state.

Validation

  • All host suites green in CI: crypto 61/61, session + enrollment suites; check_proto_sync.sh fails on vendored-proto or binding drift (nanopb pinned).
  • On-car: enrollment (tap + confirm), authenticated handshake, live decrypted status (presence/lock/sleep), Android app end-to-end onboarding.

Notes for reviewers / testers

  • CONFIG_DASHKIT_TESLA_BLE defaults to y: every build now includes the Tesla central role and the tesla-protocol component. Harmless with no car provisioned (the client idles; nothing scans or self-stages without app input), but call it out for flashing.
  • Testers need the matching DashPilot Android app (Phase 4 branch) to exercise onboarding - the wire format (status frame with link_state byte) is new. Provisioning is app-driven: the phone scans for the car and sends VIN + BLE address via opcode 0x04.
  • Known limitations, acceptable for this merge: sleeping-car poll logs GET_STATUS send failed on retries (Phase 6 robustness item); private key stored in plaintext NVS (flagged release-blocker-candidate, hardening deferred); role is CHARGING_MANAGER only (DRIVER opt-in is Phase 5); app-channel commands are authenticated only by BLE link-layer encryption (no app-level secret - acceptable for the car-trim threat model, but not to be treated as strong auth).
  • Reconciled upstream gitattributes: force LF for shell scripts #54/Keep climate on when driver leaves the car #55/Release v0.1.3 #56 before opening.

Update 2026-08-23 - scope cut, review remediation, app-driven onboarding

This PR has been substantially revised since the initial push. The branch was rebuilt as feat/tesla-ble-scope-cut (now pushed here) containing all original content plus fixes found in vehicle validation and an internal audit. Headline changes vs the description above:

Scope cut (firmware gets smaller, not bigger)

  • The NimBLE observer / advert-name matching is gone - the phone discovers the car instead (tesla_advert_name.*, its test suite, and tools/fake_tesla_beacon/ are deleted).
  • Infotainment protos (car_server/common/managed_charging/vehicle.proto + bindings) are removed; only the VCSEC/vehicle-command schemas we actually use remain vendored.
  • Net ~ -3,000 lines against the previously pushed head, while adding the fixes below.

Fixes from vehicle validation

  • KeyIdentifier.publicKeySHA1 regenerated as dynamic-length bytes: real cars send truncated 4-byte ids inside signerOfOperation, and a fixed-length binding rejected those whole frames (the tap result silently vanished). Host regression test pins the captured frame.
  • Tap-window keepalive replaced: polling GET_STATUS mid-window made the car stop responding entirely. It is now a link-only ATT touch (CCCD read) that resets supervision without touching the VCSEC stack.
  • Discovery-phase disconnects now fail connect() immediately instead of riding out the 20 s timeout.
  • Unsupported status-frame versions are rejected; new interim link state 0x06 Connecting keeps the app informed between staging and the tap window.
  • CI pins nanopb==0.4.9.1; check_proto_sync.sh fails on vendored-proto or binding drift.

Reviewer items (see comment thread): proto pinning/sync OK, generated-bindings policy OK, Kconfig scoping OK, NimBLE capacity bumps OK, storage-header verbosity OK, advert-name question moot after scope cut OK.

@wpmed92 wpmed92 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I like the PR, but left some comments, let's discuss them, and apply changes where needed.

Comment thread components/tesla-protocol/protos/vehicle.proto Outdated
Comment thread components/tesla-protocol/generated/errors.pb.c
Comment thread main/tesla/tesla_ble_storage.h
Comment thread main/Kconfig.projbuild
Comment thread tools/test/run_tesla_advert_name_test.sh Outdated
@wpmed92

wpmed92 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

@dkneeland What I forgot in the review comments, so adding separately, is that now Tesla - if paired, will opccupy another BLE slot, I see you bumped to CONFIG_BT_NIMBLE_MAX_CONNECTIONS=4 -> Can you check what's the max supported is in our stack, and bump it to that, or near that value? Because I think I will occupy all three because I have 3 devices, then will add Tesla, and might do some Cabana work on my Mac, so it will be not enough. It's not tightly coupled to this PR, but since this already touches it, would make sense to bump it here.

dkneeland added a commit to dkneeland/dashkit-firmware that referenced this pull request Aug 23, 2026
Reviewer (wpmed92) comments plus follow-ups, applied on top of the
already-merged Tesla BLE work:

- BLE slot/bond/activity config: BT_NIMBLE_MAX_CONNECTIONS 4->6,
  BT_NIMBLE_MAX_BONDS 3->8, BT_NIMBLE_MAX_CCCDS 8->16, and
  BT_CTRL_BLE_MAX_ACT 6->8. Corrected the sdkconfig.defaults note that
  claimed ESP32-S3 "does not expose" the controller activity limit (the S3
  controller Kconfig sources the C3 one, so it IS exposed and is the real
  ceiling). Cost: ~2.9 KB RAM, ~2.5 KB of the 24 KB NVS partition at saturation.
- Proto-sync guarantee: new tools/check_proto_sync.sh + CI step that diffs the
  vendored protos against the pinned teslamotors/vehicle-command commit and
  regenerates the nanopb bindings asserting no drift. Verified locally: protos
  match upstream at f97fa1e..., generated bindings have zero content drift.
- Comment-verbosity pass across the whole stack: stripped "Phase N"/"plan
  §N"/internal review-reference tokens from sources, build files, READMEs, CI
  step names and test suite labels; compressed over-long comments; kept the
  security and protocol-quirk notes.
- VIN privacy: removed the developer's real VIN and its derived advertisement
  name everywhere; replaced with a synthetic, check-digit-valid VIN
  (5YJ3E1EB8TF024681 -> legacy advert name S1481f4f405d98dfeC). The observer
  auto-provision now matches the placeholder name only; app-driven enrollment
  covers any VIN (Kconfig-izing the target is the follow-up).
- On-device verification (COM3): boots clean at the new limits; live on-car
  handshake + decrypted GET_STATUS still work (sleeping-car retry pattern
  unchanged).

Functional changes are limited to the config bumps and the inert observer
target placeholder; the rest is comments/docs/test-label cleanup.
dkneeland added a commit to dkneeland/dashkit-firmware that referenced this pull request Aug 23, 2026
Implements the 2026-08-21 PR softwiredtech#58 audit recommendations:

- Remove the NimBLE observer entirely (scan task, discovery handler,
  advert-name matcher + its test, fake-beacon project). The phone scans for
  the car instead; the firmware never scans. Drops the hardcoded target VIN
  (the PR previously only staged one specific car) and the OBSERVER role
  requirement (#error guard, Kconfig select, sdkconfig default).
- New app-channel opcode 0x04 TESLA_CMD_PROVISION (25 bytes: opcode + 17-byte
  VIN + BLE address type + 6 MAC bytes in ble_addr_t.val order) stages the
  car via tesla_pairing_configure(); enrollment still starts only on 0x01.
- Dedup identical status frames in ble_appchan_report_status(): the pairing
  task's staged/never-enrolled loops were notifying the same frame at 5 Hz /
  1 Hz to a connected phone.
- Drop the 4 unused vendored protos (car_server/vehicle/managed_charging/
  common - Infotainment-only, imported by nothing we generate); update
  protos/README, gen_proto.py and check_proto_sync.sh comments.
- Delete dead tesla_authenticated_command_key() (+ its openssl vector).
- Hoist duplicated RX_FRAME_MAX -> TESLA_RX_FRAME_MAX (adapter.h) and the two
  identical hw_rng wrappers -> tesla_rand.c.
- Consolidate the four test runners' copy-pasted mbedTLS download/build block
  into tools/test/tesla_host_env.sh; drop the advert-name CI step.

Verified: host suites green (crypto/session/enrollment), idf.py build clean
with both a stale sdkconfig and a fresh reconfigure (OBSERVER=n); binary size
unchanged at 69% OTA free.
dkneeland added a commit to dkneeland/dashkit-firmware that referenced this pull request Aug 23, 2026
…ope-cut rework

The fork head carried the original Phase 0-4 commits. The scope-cut
branch reimplements that content with review fixes, the app-driven
provisioning flow, and the audit remediation (dynamic-bytes binding,
link-only keepalive, fast-fail connect). Taking our tree wholesale;
original commits preserved as ancestry.
@dkneeland

Copy link
Copy Markdown
Contributor Author

Thanks @wpmed92 — all five inline comments plus your NimBLE capacity note are addressed, alongside a scope revision and several vehicle-validation fixes. Pushed to feat/tesla-ble (68eb540).

Your review points

Comment Resolution
vehicle.proto: protos in sync with teslamotors/vehicle-command? Vendored protos are pinned to upstream commit f97fa1e4 (protos/VERSION), and a new CI step (tools/check_proto_sync.sh) fails when vendored protos drift or when committed nanopb bindings don't match regeneration
gitignore generated/, regen as part of build? Kept committed so a clean checkout builds without a protobuf toolchain — but drift can no longer land silently thanks to that check; generator pinned (nanopb==0.4.9.1) so comparisons are like-for-like
tesla_ble_storage.h: less verbose comments Trimmed; same verbosity pass applied across the Tesla stack
Kconfig.projbuild: whole project or part? Project-level menu "DashKit" containing a single gate, DASHKIT_TESLA_BLE; disabling removes the tesla/ modules and the BLE central role from the build
advert-name tests — needed? Moot after this update's scope cut: the observer and advert-name matching are removed entirely (see below), tests included

Separate issue comment (NimBLE slots): bumped well past 4 — CONFIG_BT_NIMBLE_MAX_CONNECTIONS=6 (host ceiling 9), plus GATT_MAX_PROCS=8, MAX_BONDS=8, MAX_CCCDS=16, preferred MTU 512.

Scope change since you reviewed: the firmware-side observer / advert-name matching, the infotainment protos, and tools/fake_tesla_beacon/ are removed — the phone now discovers the car and provisions VIN+MAC over the app channel, so firmware owns only VCSEC/BLE (≈ −3,000 lines vs the previously pushed head).

Vehicle-validation fixes riding along: regenerated KeyIdentifier as dynamic-length bytes (real cars send truncated 4-byte ids in signerOfOperation — fixed-length rejected entire frames), tap-window keepalive switched from GET_STATUS writes to a link-only CCCD read, discovery-phase disconnects fail connect() fast instead of stalling, and an interim Connecting link state keeps the app informed. Happy to walk through any of it.

… layers, mbedTLS 3.6

Vendor the Apache-2.0 Tesla vehicle-command .proto schemas (pinned in
protos/VERSION at upstream f97fa1e4) and the nanopb 0.4.9.1 runtime, and add
the crypto/session layers ported to the mbedTLS 3.6 API used by ESP-IDF 5.4.1:
P-256 ECDH, K = SHA1(X)[:16], HMAC-SHA256 session/auth subkeys, AES-128-GCM
sign + decrypt (AAD = SHA256(metadata)), metadata TLV sort/build, session-info
HMAC auth, request-hash, and the committed nanopb VCSEC bindings plus the
protobuf message builders. tools/gen_proto.py regenerates the bindings;
tools/check_proto_sync.sh fails on vendored-proto or binding drift.
The protocol layer has no BLE dependency (Phase 0 scope).
…-framed RX/TX, link keepalive

NimBLE central adapter for the vehicle-command GATT service: connect with a
generation-token that rejects stale/late callbacks, exchange MTU, discover
service 00000211 / write 0212 / indicate 0213, subscribe notify+indicate,
2-byte BE length-prefixed write framing + RX reassembly, and a link-only
keepalive (CCCD read) that resets supervision without touching the VCSEC
stack. Idle-disconnect + abort/cleanup reconcile the async transport.

The central GAP path is kept strictly separate from the peripheral server's
gap_event_handler (per ADR 0001 note 2) and never feeds ble_server's slot table.
…esent-key enrollment

Phases 2-4 of the plan. ble_appchan (CADA02xx) is the app-triggered pairing
channel (opcodes 0x01 start / 0x02 reset / 0x03 cancel / 0x04 provision VIN+MAC)
with status-notify + read of a link_state frame; the firmware never self-arms
pairing and ships no BLE observer. tesla_ble_client runs the persistent
VCSEC poll (handshake -> GET_STATUS, anti-replay + GCM decrypt, debounced
not-connected reporting). tesla_ble_storage persists the keypair/VIN/car
address in NVS. tesla_pairing does present-key enrollment (CHARGING_MANAGER,
card tap + touchscreen confirm) with handshake-verified persistence.

Review fixes folded in: status dedup centralized in ble_appchan_report_status
(single full-frame memcmp); status notify documented as targeting only the
single active connection.
…anary

DASHKIT_TESLA_BLE gate (default y) adds the tesla-protocol component, the
main/tesla sources, the app-channel service, and the BLE central role; disabling
drops them from the build. sdkconfig.defaults enables the central role and bumps
MAX_CONNECTIONS. main.c gains the central #error guard and a boot canary.
… drift check

Host-side (no ESP-IDF) unit tests over the crypto/session/protobuf layers,
validated against Tesla's protocol.md known-answer vectors: crypto 61/61,
session round-trip (handshake, GET_STATUS sign/decrypt, anti-replay, whitelist
state machine), and enrollment (keygen + present-key message). A CI 'test' job
builds a pinned mbedTLS 3.6.2 and runs the suites plus check_proto_sync.sh.
@wpmed92
wpmed92 merged commit 90577ac into softwiredtech:main Aug 24, 2026
2 checks passed
@dkneeland
dkneeland deleted the feat/tesla-ble branch August 24, 2026 19:33
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.

2 participants