Skip to content
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
12 changes: 12 additions & 0 deletions .changeset/navigation-supercharger-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@teslemetry/tesla-protocol": minor
---

Add `CarServer.NavigationSuperchargerRequest.id` (`int64`, tag 1), the supercharger
location the request navigates to - the same identifier `CarServer.Superchargers.id`
carries in a `GetNearbyChargingSites` reply. Tag 1 was previously reserved and unmodelled,
so a supercharger-nav command could only carry a trip order with no supercharger named.

Wire-compatible addition: nothing is renamed or renumbered, and `"order"` stays reserved.
Callers that want the vehicle to act on the request must set `id` alongside
`remoteNavTripOrder` (TypeScript) / `remote_nav_trip_order` (Python).
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- Adding a wholly new proto directory needs registering in three places: the `buf.yaml` module list, `GROUPS_LIST` in `scripts/generate.sh`, and the layout table in `README.md`. TypeScript/Python package exports are wildcarded, so no per-group registration is needed there.
- `scripts/build_catalog.py` regenerates the descriptor-based protobuf surface catalog (`catalog/catalog.json` + `catalog/SUMMARY.md`) from `proto/`: every group, file, package, message, enum, field/tag/type, oneof, reservation, import, and generated TS/Python target. CI fails on any diff or on inconsistent group registration across `buf.yaml`/`GROUPS_LIST`/README, so run it after any proto or group-registration change. `energy_device/google/rpc/status.proto` is catalogued but excluded from message/enum/field totals since it's never a compiled top-level input (only reachable via import).
- Some `energy_device.v1` services (`SMAPI`, `WMSAAPI`) have no modeled request/response bodies, only the routing enum. A few `energy_device.v1` messages carry a field whose type lives in a sibling, not-yet-published package (`energy_registration.v1`) - left unmodeled with a short inline comment rather than guessed at or vendored in.
- `buf breaking`'s tag-renumbering forbidding (see `.changeset/README.md`) is a hard CI gate, not a guideline: it fires even when you have hard evidence an already-published field's tag is wrong, and reserving just the number isn't enough - `FIELD_NO_DELETE` also requires reserving the field's name, which then collides with reusing that name at the corrected number. Don't attempt an in-place renumber; that always needs a maintainer decision. The sanctioned fix (once approved) is reserve the old number and name, then add a differently-named field at the correct number - a breaking rename, not a wire-compatible change, so it needs a major changeset. `NavigationSuperchargerRequest` (`reserved 1`/`"order"`, `remote_nav_trip_order` at tag 2) is a live example.
- `buf breaking`'s tag-renumbering forbidding (see `.changeset/README.md`) is a hard CI gate, not a guideline: it fires even when you have hard evidence an already-published field's tag is wrong, and reserving just the number isn't enough - `FIELD_NO_DELETE` also requires reserving the field's name, which then collides with reusing that name at the corrected number. Don't attempt an in-place renumber; that always needs a maintainer decision. The sanctioned fix (once approved) is reserve the old number and name, then add a differently-named field at the correct number - a breaking rename, not a wire-compatible change, so it needs a major changeset. `NavigationSuperchargerRequest` is a live example: `"order"` stays reserved by name, with `id` at tag 1 and `remote_nav_trip_order` at tag 2.
- Cross-package wire-format confirmation lives in `fixtures/golden/*.json`: language-neutral hex-encoded cases, each consumed by both `packages/typescript/test/*.test.ts` (`node --test`) and `packages/python/tests/test_*.py` (`unittest`), asserting both encoders produce identical bytes and both decoders round-trip them. CI runs both suites. Add a new fixture file per confirmed command/message rather than growing one file unboundedly.

## Maintaining this file
Expand Down
4 changes: 2 additions & 2 deletions catalog/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ Generated by `scripts/build_catalog.py` from `proto/`. Do not edit by hand;
regenerate with `.venv/bin/python scripts/build_catalog.py` and commit the diff.
See `catalog/catalog.json` for the full descriptor-based inventory.

**Totals:** 38 source files, 7 groups, 868 messages, 237 enums, 2804 fields.
**Totals:** 38 source files, 7 groups, 868 messages, 237 enums, 2805 fields.

| Group | Source files | Compiled top-level inputs | Messages | Enums | Fields |
|---|---:|---:|---:|---:|---:|
| `charging` | 1 | 1 | 6 | 0 | 31 |
| `command` | 10 | 10 | 315 | 97 | 1420 |
| `command` | 10 | 10 | 315 | 97 | 1421 |
| `dashcam` | 1 | 1 | 1 | 2 | 16 |
| `energy_command` | 1 | 1 | 0 | 1 | 0 |
| `energy_device` | 19 | 18 | 435 | 89 | 1014 |
Expand Down
19 changes: 11 additions & 8 deletions catalog/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@
},
"messages": 315,
"enums": 97,
"fields": 1420,
"fields": 1421,
"files": [
{
"package": "CarServer",
Expand Down Expand Up @@ -4686,6 +4686,14 @@
"fqn": ".CarServer.NavigationSuperchargerRequest",
"name": "NavigationSuperchargerRequest",
"fields": [
{
"number": 1,
"name": "id",
"type": "TYPE_INT64",
"type_name": null,
"label": "LABEL_OPTIONAL",
"oneof": null
},
{
"number": 2,
"name": "remote_nav_trip_order",
Expand All @@ -4696,12 +4704,7 @@
}
],
"oneofs": [],
"reserved_ranges": [
[
1,
1
]
],
"reserved_ranges": [],
"reserved_names": [
"order"
],
Expand Down Expand Up @@ -41947,7 +41950,7 @@
"groups": 7,
"messages": 868,
"enums": 237,
"fields": 2804
"fields": 2805
},
"registration": {
"consistent": true,
Expand Down
31 changes: 31 additions & 0 deletions fixtures/golden/navigation_supercharger_id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"description": "Golden wire-format fixtures for CarServer.Action.vehicleAction (navigation supercharger request, supercharger location id). Pins NavigationSuperchargerRequest.id as an int64 varint at tag 1 alongside remoteNavTripOrder at tag 2. Ids below are synthetic. Consumed identically by the TypeScript and Python test suites: both encoders must produce these exact bytes, and both decoders must round-trip them back to the same field values.",
"outer_message": "CarServer.Action",
"outer_field": { "name": "vehicleAction", "tag": 2, "type": "CarServer.VehicleAction" },
"cases": [
{
"name": "navigation_supercharger_id_only",
"vehicle_action_field": { "name": "navigationSuperchargerRequest", "tag": 22, "type": "CarServer.NavigationSuperchargerRequest" },
"payload": { "id": 123456, "remote_nav_trip_order": "REMOTE_NAV_TRIP_ORDER_UNKNOWN", "remote_nav_trip_order_number": 0 },
"hex": "1207b2010408c0c407"
},
{
"name": "navigation_supercharger_id_with_replace",
"vehicle_action_field": { "name": "navigationSuperchargerRequest", "tag": 22, "type": "CarServer.NavigationSuperchargerRequest" },
"payload": { "id": 123456, "remote_nav_trip_order": "REMOTE_NAV_TRIP_ORDER_REPLACE", "remote_nav_trip_order_number": 1 },
"hex": "1209b2010608c0c4071001"
},
{
"name": "navigation_supercharger_id_with_prepend",
"vehicle_action_field": { "name": "navigationSuperchargerRequest", "tag": 22, "type": "CarServer.NavigationSuperchargerRequest" },
"payload": { "id": 987654321, "remote_nav_trip_order": "REMOTE_NAV_TRIP_ORDER_PREPEND", "remote_nav_trip_order_number": 2 },
"hex": "120bb2010808b1d1f9d6031002"
},
{
"name": "navigation_supercharger_id_with_append",
"vehicle_action_field": { "name": "navigationSuperchargerRequest", "tag": 22, "type": "CarServer.NavigationSuperchargerRequest" },
"payload": { "id": 1, "remote_nav_trip_order": "REMOTE_NAV_TRIP_ORDER_APPEND", "remote_nav_trip_order_number": 3 },
"hex": "1207b2010408011003"
}
]
}
416 changes: 208 additions & 208 deletions packages/python/tesla_protocol/command/car_server_pb2.py

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions packages/python/tesla_protocol/command/car_server_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1497,7 +1497,7 @@ class NavigationRequest(_message.Message):
...

class NavigationSuperchargerRequest(_message.Message):
__slots__ = ('remote_nav_trip_order',)
__slots__ = ('id', 'remote_nav_trip_order')

class RemoteNavTripOrder(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
Expand All @@ -1509,10 +1509,12 @@ class NavigationSuperchargerRequest(_message.Message):
REMOTE_NAV_TRIP_ORDER_REPLACE: NavigationSuperchargerRequest.RemoteNavTripOrder
REMOTE_NAV_TRIP_ORDER_PREPEND: NavigationSuperchargerRequest.RemoteNavTripOrder
REMOTE_NAV_TRIP_ORDER_APPEND: NavigationSuperchargerRequest.RemoteNavTripOrder
ID_FIELD_NUMBER: _ClassVar[int]
REMOTE_NAV_TRIP_ORDER_FIELD_NUMBER: _ClassVar[int]
id: int
remote_nav_trip_order: NavigationSuperchargerRequest.RemoteNavTripOrder

def __init__(self, remote_nav_trip_order: _Optional[_Union[NavigationSuperchargerRequest.RemoteNavTripOrder, str]]=...) -> None:
def __init__(self, id: _Optional[int]=..., remote_nav_trip_order: _Optional[_Union[NavigationSuperchargerRequest.RemoteNavTripOrder, str]]=...) -> None:
...

class UiSetUpcomingCalendarEntries(_message.Message):
Expand Down
48 changes: 48 additions & 0 deletions packages/python/tests/test_navigation_supercharger_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import json
import unittest
from pathlib import Path

from tesla_protocol.command.car_server_pb2 import Action, NavigationSuperchargerRequest

FIXTURE = json.loads(
(
Path(__file__).parents[3] / "fixtures" / "golden" / "navigation_supercharger_id.json"
).read_text()
)


class GoldenFixtureTests(unittest.TestCase):
def test_outer_vehicle_action_wrapper_is_tag_2(self):
self.assertEqual(FIXTURE["outer_field"]["tag"], 2)
self.assertEqual(FIXTURE["outer_field"]["name"], "vehicleAction")

def test_fixtures_round_trip_to_golden_bytes(self):
for case in FIXTURE["cases"]:
with self.subTest(case=case["name"]):
action = Action(
vehicleAction={
"navigationSuperchargerRequest": NavigationSuperchargerRequest(
id=case["payload"]["id"],
remote_nav_trip_order=case["payload"][
"remote_nav_trip_order_number"
],
)
}
)

encoded = action.SerializeToString()
self.assertEqual(encoded.hex(), case["hex"])

decoded = Action.FromString(encoded)
self.assertEqual(
decoded.vehicleAction.navigationSuperchargerRequest.id,
case["payload"]["id"],
)
self.assertEqual(
decoded.vehicleAction.navigationSuperchargerRequest.remote_nav_trip_order,
case["payload"]["remote_nav_trip_order_number"],
)


if __name__ == "__main__":
unittest.main()
20 changes: 19 additions & 1 deletion packages/typescript/src/command/car_server.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions packages/typescript/test/navigation-supercharger-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

import { Action } from "../dist/command/car_server.mjs";

const fixturePath = fileURLToPath(
new URL("../../../fixtures/golden/navigation_supercharger_id.json", import.meta.url),
);
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));

function hex(bytes: Uint8Array): string {
return Buffer.from(bytes).toString("hex");
}

test("Action.vehicleAction outer wrapper is tag 2", () => {
assert.equal(fixture.outer_field.tag, 2);
assert.equal(fixture.outer_field.name, "vehicleAction");
});

for (const c of fixture.cases) {
test(`golden fixture: ${c.name}`, () => {
const action = Action.fromJSON({
vehicleAction: {
navigationSuperchargerRequest: {
id: c.payload.id,
remoteNavTripOrder: c.payload.remote_nav_trip_order_number,
},
},
});

const encoded = Action.encode(action).finish();
assert.equal(hex(encoded), c.hex, `encode mismatch for ${c.name}`);

const decoded = Action.decode(encoded);
assert.equal(
decoded.vehicleAction?.navigationSuperchargerRequest?.id ?? 0,
c.payload.id,
);
assert.equal(
decoded.vehicleAction?.navigationSuperchargerRequest?.remoteNavTripOrder ?? 0,
c.payload.remote_nav_trip_order_number,
);
});
}
5 changes: 3 additions & 2 deletions proto/command/car_server.proto
Original file line number Diff line number Diff line change
Expand Up @@ -773,15 +773,16 @@ message NavigationRequest {
}

message NavigationSuperchargerRequest {
// order was misdeclared at tag 1; reserved to block reuse, see AGENTS.md.
reserved 1;
// The trip order lives at tag 2, so the name stays blocked here, see AGENTS.md.
reserved "order";
enum RemoteNavTripOrder {
REMOTE_NAV_TRIP_ORDER_UNKNOWN = 0;
REMOTE_NAV_TRIP_ORDER_REPLACE = 1;
REMOTE_NAV_TRIP_ORDER_PREPEND = 2;
REMOTE_NAV_TRIP_ORDER_APPEND = 3;
}
// Supercharger location, identified as in Superchargers.id.
int64 id = 1;
RemoteNavTripOrder remote_nav_trip_order = 2;
}

Expand Down
7 changes: 3 additions & 4 deletions scripts/test_build_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,14 @@ def test_files_within_each_group_are_sorted_by_path(self):
self.assertEqual(paths, sorted(paths))

def test_baseline_totals(self):
# Reproduces the E4.1 breakdown report's baseline counts at main 1df2336b.
# If proto/ has changed since, the current tree is authoritative - this
# assertion documents that baseline, it doesn't freeze it.
# proto/ is authoritative: these document the current surface size so an
# unintended change shows up, they don't freeze it. Update on purpose.
totals = self.catalog["totals"]
self.assertEqual(totals["source_files"], 38)
self.assertEqual(totals["groups"], 7)
self.assertEqual(totals["messages"], 868)
self.assertEqual(totals["enums"], 237)
self.assertEqual(totals["fields"], 2804)
self.assertEqual(totals["fields"], 2805)

def test_rebuild_is_deterministic(self):
second = build_catalog.build_catalog()
Expand Down
Loading