mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-11 23:24:40 +08:00
Compare commits
91 Commits
better-exp
...
screensave
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acb4bb848b | ||
|
|
0cd913d7ac | ||
|
|
7fd1fea3b0 | ||
|
|
e79c3243b2 | ||
|
|
a936c311d7 | ||
|
|
8e53fe176d | ||
|
|
22a12aec35 | ||
|
|
426c2abd61 | ||
|
|
1a84559cec | ||
|
|
65b61fd965 | ||
|
|
a01449822f | ||
|
|
f52db76444 | ||
|
|
b61f095ef2 | ||
|
|
59e5fd7fe6 | ||
|
|
607811b6ad | ||
|
|
2414fe287d | ||
|
|
f7a4dbc47f | ||
|
|
fd0a5f806c | ||
|
|
de62240e97 | ||
|
|
adba29da1b | ||
|
|
6d6a91c318 | ||
|
|
7259687578 | ||
|
|
83a07e4bfa | ||
|
|
cb65777186 | ||
|
|
282b867fcb | ||
|
|
3fd3c31966 | ||
|
|
f3c3581472 | ||
|
|
465daf323c | ||
|
|
3fe8e155b6 | ||
|
|
c0bffa2a8c | ||
|
|
c9edcaa1a6 | ||
|
|
76101207cb | ||
|
|
62b01d3799 | ||
|
|
a3be1edde7 | ||
|
|
7869542683 | ||
|
|
981aab0500 | ||
|
|
9c82592906 | ||
|
|
5d3f95d420 | ||
|
|
f8d19fe9dd | ||
|
|
9d711350c2 | ||
|
|
45c853c87a | ||
|
|
e8ab9d812d | ||
|
|
ed775185f2 | ||
|
|
7bbbc6588e | ||
|
|
e68c65d15d | ||
|
|
0db8722221 | ||
|
|
a33497ed19 | ||
|
|
91f2bf3459 | ||
|
|
7fad2fc189 | ||
|
|
e74252bdf5 | ||
|
|
6b795ab513 | ||
|
|
3b1b4ef90f | ||
|
|
48d33e98e7 | ||
|
|
2717d97350 | ||
|
|
64232397ed | ||
|
|
0613442ac9 | ||
|
|
93ab6ee06b | ||
|
|
4baa170cfa | ||
|
|
2be95225b1 | ||
|
|
f8e8c59dd5 | ||
|
|
dd9d4bedf4 | ||
|
|
ada449989b | ||
|
|
68c7f69439 | ||
|
|
1db71aff57 | ||
|
|
942ccb90dd | ||
|
|
e6f5aae246 | ||
|
|
7032e4a972 | ||
|
|
ffa78eabaa | ||
|
|
5b03369a8f | ||
|
|
1e0564b484 | ||
|
|
eb94abaa14 | ||
|
|
4da32cc009 | ||
|
|
4820265268 | ||
|
|
01aa6c4204 | ||
|
|
21beea51ec | ||
|
|
98c479830c | ||
|
|
5369b6880f | ||
|
|
a9e57f0a76 | ||
|
|
712a358c94 | ||
|
|
423a7d2ed0 | ||
|
|
e4e10d4b87 | ||
|
|
632e9d13b2 | ||
|
|
362e9ce04b | ||
|
|
51d0666c85 | ||
|
|
deda1329a2 | ||
|
|
4110749cb0 | ||
|
|
3946e643f6 | ||
|
|
0c37a38596 | ||
|
|
9c5acf61c0 | ||
|
|
121b304fe0 | ||
|
|
47d848293b |
10
.github/workflows/cereal_validation.yaml
vendored
10
.github/workflows/cereal_validation.yaml
vendored
@@ -33,15 +33,15 @@ jobs:
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
run: scons -j$(nproc) cereal
|
||||
- name: Dump sunnypilot schema
|
||||
- name: Generate the log file
|
||||
run: |
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema.json
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema_instances.bin
|
||||
- name: 'Prepare artifact'
|
||||
run: |
|
||||
mkdir -p "cereal/messaging/tests/cereal_validations"
|
||||
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
|
||||
cp schema.json "cereal/messaging/tests/cereal_validations/schema.json"
|
||||
cp schema_instances.bin "cereal/messaging/tests/cereal_validations/schema_instances.bin"
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -71,8 +71,8 @@ jobs:
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: openpilot/cereal/messaging/tests/cereal_validations
|
||||
- name: 'Validate sunnypilot schema against upstream'
|
||||
- name: 'Run the validation'
|
||||
run: |
|
||||
export PYTHONPATH=${{ github.workspace }}/openpilot
|
||||
chmod +x openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
|
||||
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema.json
|
||||
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema_instances.bin
|
||||
|
||||
6
.github/workflows/docs.yaml
vendored
6
.github/workflows/docs.yaml
vendored
@@ -29,9 +29,9 @@ jobs:
|
||||
# Build
|
||||
- name: Build docs
|
||||
run: |
|
||||
git lfs pull
|
||||
pip install zensical
|
||||
python scripts/docs.py build
|
||||
# TODO: can we install just the "docs" dependency group without the normal deps?
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,7 +46,6 @@ compile_commands.json
|
||||
compare_runtime*.html
|
||||
|
||||
# build artifacts
|
||||
docs_site/
|
||||
selfdrive/pandad/pandad
|
||||
cereal/services.h
|
||||
cereal/gen
|
||||
|
||||
@@ -342,7 +342,6 @@ struct OnroadEventSP @0xda96579883444c35 {
|
||||
speedLimitChanged @21;
|
||||
speedLimitPending @22;
|
||||
e2eChime @23;
|
||||
chillModeSwitched @24;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -558,8 +558,8 @@ struct PandaState @0xa7649e2575e4591e {
|
||||
|
||||
# these fields are not used by openpilot, but they're
|
||||
# reserved for forks building alternate experiences.
|
||||
controlsAllowedLateral @38 :Bool;
|
||||
controlsAllowedLongitudinal @39 :Bool;
|
||||
controlsAllowedRESERVED1 @38 :Bool;
|
||||
controlsAllowedRESERVED2 @39 :Bool;
|
||||
|
||||
enum FaultStatus {
|
||||
none @0;
|
||||
|
||||
@@ -1,251 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Schema-level cereal compat check between sunnypilot and upstream openpilot.
|
||||
|
||||
Rules (per struct matched across sides by typeId):
|
||||
R1 shared ordinal must reference the same type.
|
||||
R2 sunnypilot-only ordinal in a union -> FAIL (unknown discriminant upstream).
|
||||
R3 sunnypilot-only ordinal on a regular field -> OK (additive struct evolution).
|
||||
R4 upstream-only ordinal -> OK.
|
||||
R5 sunnypilot-only struct referenced via an upstream-shared field -> FAIL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
NO_DISCRIMINANT = 0xFFFF
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def hex_id(value: int) -> str:
|
||||
return f"0x{value:016x}"
|
||||
def print_debug(string: str) -> None:
|
||||
if DEBUG:
|
||||
print(string)
|
||||
|
||||
|
||||
def encode_type(type_node: Any) -> dict:
|
||||
which = type_node.which()
|
||||
if which == "struct":
|
||||
return {"kind": "struct", "typeId": hex_id(type_node.struct.typeId)}
|
||||
if which == "enum":
|
||||
return {"kind": "enum", "typeId": hex_id(type_node.enum.typeId)}
|
||||
if which == "interface":
|
||||
return {"kind": "interface", "typeId": hex_id(type_node.interface.typeId)}
|
||||
if which == "list":
|
||||
return {"kind": "list", "element": encode_type(type_node.list.elementType)}
|
||||
if which == "anyPointer":
|
||||
return {"kind": "anyPointer"}
|
||||
return {"kind": which}
|
||||
def create_schema_instance(struct: Any, prop: Tuple[str, Any]) -> Any:
|
||||
"""
|
||||
Create a new instance of a schema type, handling different field types.
|
||||
|
||||
Args:
|
||||
struct: The Cap'n Proto schema structure
|
||||
prop: A tuple containing the field name and field metadata
|
||||
|
||||
def encode_field(name: str, field: Any) -> dict:
|
||||
proto = field.proto
|
||||
ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None
|
||||
discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None
|
||||
|
||||
if proto.which() == "group":
|
||||
type_desc = {"kind": "group", "typeId": hex_id(proto.group.typeId)}
|
||||
else:
|
||||
type_desc = encode_type(proto.slot.type)
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"ordinal": ordinal,
|
||||
"discriminant": discriminant,
|
||||
"type": type_desc,
|
||||
}
|
||||
|
||||
|
||||
def encode_struct(schema: Any) -> dict:
|
||||
node = schema.node
|
||||
return {
|
||||
"typeId": hex_id(node.id),
|
||||
"displayName": node.displayName,
|
||||
"hasUnion": node.struct.discriminantCount > 0,
|
||||
"fields": [encode_field(name, field) for name, field in schema.fields.items()],
|
||||
}
|
||||
|
||||
|
||||
def _child_struct_schema(field: Any) -> Any:
|
||||
proto = field.proto
|
||||
if proto.which() == "group":
|
||||
return field.schema
|
||||
type_node = proto.slot.type
|
||||
which = type_node.which()
|
||||
if which == "struct":
|
||||
return field.schema
|
||||
if which == "list":
|
||||
container = field.schema
|
||||
element_type = type_node.list.elementType
|
||||
while element_type.which() == "list":
|
||||
container = container.elementType
|
||||
element_type = element_type.list.elementType
|
||||
if element_type.which() == "struct":
|
||||
return container.elementType
|
||||
return None
|
||||
|
||||
|
||||
def collect_schema(root: Any) -> dict[str, dict]:
|
||||
structs: dict[str, dict] = {}
|
||||
stack = [root]
|
||||
while stack:
|
||||
schema = stack.pop()
|
||||
type_id = hex_id(schema.node.id)
|
||||
if type_id in structs:
|
||||
continue
|
||||
structs[type_id] = encode_struct(schema)
|
||||
for _name, field in schema.fields.items():
|
||||
try:
|
||||
child = _child_struct_schema(field)
|
||||
except Exception:
|
||||
child = None
|
||||
if child is not None:
|
||||
stack.append(child)
|
||||
return structs
|
||||
|
||||
|
||||
def dump_schema(path: str) -> None:
|
||||
from cereal import log
|
||||
payload = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||
print(f"wrote schema dump with {len(payload['structs'])} structs to {path}")
|
||||
|
||||
|
||||
def types_equal(a: dict, b: dict) -> bool:
|
||||
if a.get("kind") != b.get("kind"):
|
||||
return False
|
||||
kind = a["kind"]
|
||||
if kind in ("struct", "enum", "interface", "group"):
|
||||
return a.get("typeId") == b.get("typeId")
|
||||
if kind == "list":
|
||||
return types_equal(a["element"], b["element"])
|
||||
return True
|
||||
|
||||
|
||||
def type_repr(t: dict) -> str:
|
||||
kind = t.get("kind", "?")
|
||||
if kind in ("struct", "enum", "interface", "group"):
|
||||
return f"{kind}({t.get('typeId')})"
|
||||
if kind == "list":
|
||||
return f"list<{type_repr(t['element'])}>"
|
||||
return kind
|
||||
|
||||
|
||||
def field_is_union_variant(field: dict) -> bool:
|
||||
return field.get("discriminant") is not None
|
||||
|
||||
|
||||
def index_fields_by_ordinal(struct: dict) -> dict[int, dict]:
|
||||
indexed: dict[int, dict] = {}
|
||||
for field in struct["fields"]:
|
||||
ordinal = field.get("ordinal")
|
||||
if ordinal is None:
|
||||
continue
|
||||
indexed[ordinal] = field
|
||||
return indexed
|
||||
|
||||
|
||||
def compare(sunnypilot_dump: dict, upstream_dump: dict) -> list[str]:
|
||||
violations: list[str] = []
|
||||
sunnypilot_structs: dict[str, dict] = sunnypilot_dump["structs"]
|
||||
upstream_structs: dict[str, dict] = upstream_dump["structs"]
|
||||
|
||||
sunnypilot_struct_referenced_from_shared: set[str] = set()
|
||||
|
||||
for type_id, sunnypilot_struct in sunnypilot_structs.items():
|
||||
upstream_struct = upstream_structs.get(type_id)
|
||||
if upstream_struct is None:
|
||||
continue
|
||||
|
||||
sunnypilot_fields = index_fields_by_ordinal(sunnypilot_struct)
|
||||
upstream_fields = index_fields_by_ordinal(upstream_struct)
|
||||
display = sunnypilot_struct["displayName"]
|
||||
|
||||
for ordinal, sunnypilot_field in sunnypilot_fields.items():
|
||||
upstream_field = upstream_fields.get(ordinal)
|
||||
if upstream_field is None:
|
||||
if field_is_union_variant(sunnypilot_field):
|
||||
violations.append(
|
||||
f"[R2] {display} @{ordinal} ('{sunnypilot_field['name']}', {type_repr(sunnypilot_field['type'])}): "
|
||||
f"union variant not present upstream. upstream cannot parse this discriminant."
|
||||
)
|
||||
continue
|
||||
|
||||
if not types_equal(sunnypilot_field["type"], upstream_field["type"]):
|
||||
violations.append(
|
||||
f"[R1] {display} @{ordinal}: type mismatch. "
|
||||
f"sunnypilot='{sunnypilot_field['name']}' {type_repr(sunnypilot_field['type'])} vs "
|
||||
f"upstream='{upstream_field['name']}' {type_repr(upstream_field['type'])}."
|
||||
)
|
||||
continue
|
||||
|
||||
cursor = sunnypilot_field["type"]
|
||||
while cursor.get("kind") == "list":
|
||||
cursor = cursor["element"]
|
||||
if cursor.get("kind") in ("struct", "group", "interface") and cursor.get("typeId"):
|
||||
sunnypilot_struct_referenced_from_shared.add(cursor["typeId"])
|
||||
|
||||
for type_id, sunnypilot_struct in sunnypilot_structs.items():
|
||||
if type_id in upstream_structs:
|
||||
continue
|
||||
if type_id in sunnypilot_struct_referenced_from_shared:
|
||||
violations.append(
|
||||
f"[R5] struct {sunnypilot_struct['displayName']} ({type_id}) exists only on sunnypilot "
|
||||
f"but is referenced from an upstream-shared field. upstream cannot resolve this type."
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def load_peer(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def run_read(peer_path: str) -> int:
|
||||
from cereal import log
|
||||
peer_dump = load_peer(peer_path)
|
||||
local_dump = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
}
|
||||
violations = compare(sunnypilot_dump=peer_dump, upstream_dump=local_dump)
|
||||
|
||||
if not violations:
|
||||
print("cereal compat OK: upstream openpilot can parse sunnypilot routes "
|
||||
"(no leaked structs, no ordinal collisions).")
|
||||
return 0
|
||||
|
||||
print(f"cereal compat FAIL: upstream openpilot would misparse sunnypilot routes "
|
||||
f"({len(violations)} violation(s)):")
|
||||
for v in violations:
|
||||
print(f" {v}")
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="sunnypilot <-> upstream cereal compatibility validator (schema-level)."
|
||||
)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
|
||||
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
|
||||
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
|
||||
args = parser.parse_args()
|
||||
Returns:
|
||||
A new initialized schema instance
|
||||
"""
|
||||
struct_instance = struct.new_message()
|
||||
field_name, field_metadata = prop
|
||||
|
||||
try:
|
||||
if args.generate:
|
||||
dump_schema(args.file)
|
||||
return 0
|
||||
return run_read(args.file)
|
||||
except ImportError as exc:
|
||||
print(f"error: cannot import cereal ({exc}). did scons build cereal?")
|
||||
return 2
|
||||
field_type = field_metadata.proto.slot.type.which()
|
||||
|
||||
# Initialize different types of fields
|
||||
if field_type in ('list', 'text', 'data'):
|
||||
struct_instance.init(field_name, 1)
|
||||
print_debug(f"Initialized list/text/data field: {field_name}")
|
||||
elif field_type in ('struct', 'object'):
|
||||
struct_instance.init(field_name)
|
||||
print_debug(f"Initialized struct/object field: {field_name}")
|
||||
|
||||
return struct_instance
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating instance for {field_name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_schema_fields(schema_struct: Any) -> List[Tuple[str, Any]]:
|
||||
"""
|
||||
Retrieve all fields from a given schema structure.
|
||||
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
Returns:
|
||||
A list of field names and their metadata
|
||||
"""
|
||||
try:
|
||||
# Get all fields from the schema
|
||||
schema_fields = list(schema_struct.schema.fields.items())
|
||||
|
||||
print_debug("Discovered schema fields:")
|
||||
for field_name, field_metadata in schema_fields:
|
||||
print_debug(f"- {field_name}")
|
||||
|
||||
return schema_fields
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving schema fields: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def generate_schema_instances(schema_struct: Any) -> List[Any]:
|
||||
"""
|
||||
Generate instances for all fields in a given schema.
|
||||
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
Returns:
|
||||
A list of schema instances
|
||||
"""
|
||||
schema_fields = get_schema_fields(schema_struct)
|
||||
instances = []
|
||||
|
||||
for field_prop in schema_fields:
|
||||
try:
|
||||
instance = create_schema_instance(schema_struct, field_prop)
|
||||
if instance is not None:
|
||||
instances.append(instance)
|
||||
except Exception as e:
|
||||
print(f"Skipping field due to error: {e}")
|
||||
|
||||
print(f"Generated {len(instances)} schema instances")
|
||||
return instances
|
||||
|
||||
|
||||
def persist_instances(instances: List[Any], filename: str) -> None:
|
||||
"""
|
||||
Write schema instances to a binary file.
|
||||
|
||||
Args:
|
||||
instances: List of schema instances
|
||||
filename: Output file path
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'wb') as f:
|
||||
for instance in instances:
|
||||
f.write(instance.to_bytes())
|
||||
|
||||
print(f"Successfully wrote {len(instances)} instances to {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error persisting instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def read_instances(filename: str, schema_type: Any) -> List[Any]:
|
||||
"""
|
||||
Read schema instances from a binary file.
|
||||
|
||||
Args:
|
||||
filename: Input file path
|
||||
schema_type: The schema type to use for reading
|
||||
|
||||
Returns:
|
||||
A list of read schema instances
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
instances = list(schema_type.read_multiple_bytes(data))
|
||||
|
||||
print(f"Read {len(instances)} instances from {filename}")
|
||||
return instances
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def compare_schemas(original_instances: List[Any], read_instances: List[Any]) -> bool:
|
||||
"""
|
||||
Compare original and read-back instances to detect potential breaking changes.
|
||||
|
||||
Args:
|
||||
original_instances: List of originally generated instances
|
||||
read_instances: List of instances read back from file
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether schemas appear compatible
|
||||
"""
|
||||
if len(original_instances) != len(read_instances):
|
||||
print("❌ Schema Compatibility Warning: Instance count mismatch")
|
||||
return False
|
||||
|
||||
compatible = True
|
||||
for struct in read_instances:
|
||||
try:
|
||||
getattr(struct, struct.which()) # Attempting to access the field to validate readability
|
||||
except Exception as e:
|
||||
print(f"❌ Structural change detected: {struct.which()} is not readable.\nFull error: {e}")
|
||||
compatible = False
|
||||
|
||||
return compatible
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
CLI entry point for schema compatibility testing.
|
||||
"""
|
||||
# Setup argument parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Cap\'n Proto Schema Compatibility Testing Tool',
|
||||
epilog='Test schema compatibility by generating and reading back instances.'
|
||||
)
|
||||
|
||||
# Add mutually exclusive group for generation or reading mode
|
||||
mode_group = parser.add_mutually_exclusive_group(required=True)
|
||||
mode_group.add_argument('-g', '--generate', action='store_true',
|
||||
help='Generate schema instances')
|
||||
mode_group.add_argument('-r', '--read', action='store_true',
|
||||
help='Read and validate schema instances')
|
||||
|
||||
# Common arguments
|
||||
parser.add_argument('-f', '--file',
|
||||
default='schema_instances.bin',
|
||||
help='Output/input binary file (default: schema_instances.bin)')
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Import the schema dynamically
|
||||
try:
|
||||
from cereal import log
|
||||
schema_type = log.Event
|
||||
except ImportError:
|
||||
print("Error: Unable to import schema. Ensure 'cereal' is installed.")
|
||||
sys.exit(1)
|
||||
|
||||
# Execute based on mode
|
||||
if args.generate:
|
||||
print("🔧 Generating Schema Instances")
|
||||
instances = generate_schema_instances(schema_type)
|
||||
persist_instances(instances, args.file)
|
||||
print("✅ Instance generation complete")
|
||||
|
||||
elif args.read:
|
||||
print("🔍 Reading and Validating Schema Instances")
|
||||
generated_instances = generate_schema_instances(schema_type)
|
||||
read_back_instances = read_instances(args.file, schema_type)
|
||||
|
||||
# Compare schemas
|
||||
if compare_schemas(generated_instances, read_back_instances):
|
||||
print("✅ Schema Compatibility: No breaking changes detected")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("❌ Potential Schema Breaking Changes Detected")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "POP model (Default)"
|
||||
#define DEFAULT_MODEL "OP Model 7 (Default)"
|
||||
|
||||
@@ -179,6 +179,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
134
docs/CARS.md
134
docs/CARS.md
@@ -22,7 +22,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi[<sup>11</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>11</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>11</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Equinox 2019-22">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Silverado 1500 2020-21">Buy Here</a></sub></details>|||
|
||||
@@ -32,34 +32,34 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica 2021-23">Buy Here</a></sub></details>|||
|
||||
|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|
||||
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|CUPRA[<sup>11</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|
||||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Focus 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus Hybrid 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2018">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2019-21">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2022-23">Buy Here</a></sub></details>|||
|
||||
@@ -74,18 +74,18 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Genesis|GV70 Electrified (Australia Only) 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis GV70 Electrified (Australia Only) 2022">Buy Here</a></sub></details>|||
|
||||
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis GV70 Electrified (with HDA II) 2023-24">Buy Here</a></sub></details>|||
|
||||
|Genesis|GV80 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis GV80 2023">Buy Here</a></sub></details>|||
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|14 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
@@ -117,9 +117,9 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Custin 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Custin 2023">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra 2017-18">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra GT 2017-20">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra Hybrid 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Elantra Hybrid 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Genesis 2015-16">Buy Here</a></sub></details>|||
|
||||
|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai i30 2017-19">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq 5 (Southeast Asia and Europe only) 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -136,17 +136,17 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai I connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Kona Hybrid 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Nexo 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Nexo 2021">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Palisade 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Palisade 2020-22">Buy Here</a></sub></details>|<a href="https://youtu.be/TAnDqjF4fDY?t=456" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Palisade 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Palisade 2020-22">Buy Here</a></sub></details>|<a href="https://youtu.be/TAnDqjF4fDY?t=456" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Cruz 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Cruz 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Santa Fe 2019-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe 2019-20">Buy Here</a></sub></details>|<a href="https://youtu.be/bjDR0YjM__s" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Santa Fe 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/VnHzSTygTS4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Santa Fe 2019-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe 2019-20">Buy Here</a></sub></details>|<a href="https://youtu.be/bjDR0YjM__s" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Fe 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/VnHzSTygTS4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Santa Fe Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Sonata 2018-19">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Sonata 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Sonata 2020-23">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=ix63r9kE3Fw" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Sonata 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Sonata 2020-23">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=ix63r9kE3Fw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Sonata Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Staria 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Staria 2023">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Tucson 2021">Buy Here</a></sub></details>|||
|
||||
@@ -156,8 +156,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Tucson Hybrid 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Tucson Hybrid 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Tucson Plug-in Hybrid 2024">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Veloster 2019-20">Buy Here</a></sub></details>|||
|
||||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival 2022-24">Buy Here</a></sub></details>|||
|
||||
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Carnival (China only) 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Ceed 2019-21">Buy Here</a></sub></details>|||
|
||||
@@ -170,10 +170,10 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K5 Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Kia|K7 2017|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K7 2017">Buy Here</a></sub></details>|||
|
||||
|Kia|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia K8 Hybrid (with HDA II) 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro EV 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2020">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2020">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV (with HDA II) 2024-25|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV (with HDA II) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro EV (without HDA II) 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro EV (without HDA II) 2023-25">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Niro Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
@@ -188,21 +188,21 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima 2019-20">Buy Here</a></sub></details>|||
|
||||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Optima Hybrid 2019">Buy Here</a></sub></details>|||
|
||||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Seltos 2021">Buy Here</a></sub></details>|||
|
||||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Sorento 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento 2021-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Sorento Hybrid 2021-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento Hybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sorento Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sportage 2023-24">Buy Here</a></sub></details>|||
|
||||
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Sportage Hybrid 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2019-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus GS F 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -223,25 +223,25 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus UX Hybrid 2019-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator 2020-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator Plug-in Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|MAN[<sup>11</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|MAN[<sup>11</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|MAN[<sup>11</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|MAN[<sup>11</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-20, 2024">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|
||||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2025">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2025">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>11</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>11</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
@@ -252,7 +252,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>12,14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
@@ -279,50 +279,50 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Toyota|C-HR 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2017-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2021-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross (Non-US only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross Hybrid (Non-US only) 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid (South America only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Mirai 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2022">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2023-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas Cross Sport 2020-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen California 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Caravelle 2020">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Golf 2014-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf Alltrack 2015-19">Buy Here</a></sub></details>|||
|
||||
@@ -331,7 +331,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTI 2015-21">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2019-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
docs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
docs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://zensical.org/docs/
|
||||
26
docs/README.md
Normal file
26
docs/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# openpilot docs
|
||||
|
||||
This is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
## Development
|
||||
NOTE: Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
mkdocs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://www.mkdocs.org/getting-started/
|
||||
* https://github.com/ntno/mkdocs-terminal
|
||||
@@ -1 +0,0 @@
|
||||
../../selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
5
docs/car-porting/brand-port.md
Normal file
5
docs/car-porting/brand-port.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Developing a car brand port
|
||||
|
||||
A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
|
||||
65
docs/car-porting/car-state-signals.md
Normal file
65
docs/car-porting/car-state-signals.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# CarState signals
|
||||
|
||||
## Required for basic lateral control
|
||||
|
||||
* `brakePressed`
|
||||
* `cruiseState`
|
||||
* `doorOpen`
|
||||
* `espDisabled`
|
||||
* `gasPressed`
|
||||
* `gearShifter`
|
||||
* `leftBlinker` / `rightBlinker`
|
||||
* `seatbeltUnlatched`
|
||||
* `standstill`
|
||||
* `steeringAngleDeg`
|
||||
* `steeringPressed`
|
||||
* `steeringTorque`
|
||||
* `steerFaultPermanent`
|
||||
* `steerFaultTemporary`
|
||||
* `vCruise`
|
||||
* `wheelSpeeds.[fl|fr|rl|rr]`: Speed of each of the car's four wheels, in m/s. The car's CAN bus often broadcasts the
|
||||
speed in kph, so the helper function `parse_wheel_speeds` performs this conversion by default.
|
||||
|
||||
## Recommended / Required for openpilot longitudinal control
|
||||
|
||||
* `accFaulted`
|
||||
* `espActive`
|
||||
* `parkingBrake`
|
||||
|
||||
## Application Dependent
|
||||
|
||||
* `blockPcmEnable`
|
||||
* `buttonEnable`
|
||||
* `brakeHoldActive`
|
||||
* `carFaultedNonCritical`
|
||||
* `invalidLkasSetting`
|
||||
* `lowSpeedAlert`
|
||||
* `regenBraking`
|
||||
* `steeringAngleOffsetDeg`
|
||||
* `steeringDisengage`
|
||||
* `steeringTorqueEps`
|
||||
* `stockLkas`
|
||||
* `vCruiseCluster`
|
||||
* `vEgoCluster`
|
||||
* `vehicleSensorsInvalid`
|
||||
|
||||
## Automatically populated
|
||||
|
||||
* `buttonEvents`
|
||||
|
||||
These values are populated automatically by `parse_wheel_speeds`:
|
||||
|
||||
* `aEgo`: Acceleration of the ego vehicle, Kalman filtered derivative of `vEgo`.
|
||||
* `vEgo`: Speed of the ego vehicle, Kalman filtered from `vEgoRaw`.
|
||||
* `vEgoRaw`: Speed of the ego vehicle, based on the average of all four wheel speeds, unfiltered.
|
||||
|
||||
## Optional
|
||||
|
||||
* `brake`
|
||||
* `charging`
|
||||
* `fuelGauge`
|
||||
* `leftBlindspot` / `rightBlindspot`
|
||||
* `steeringRateDeg`
|
||||
* `stockAeb`
|
||||
* `stockFcw`
|
||||
* `yawRate`
|
||||
5
docs/car-porting/model-port.md
Normal file
5
docs/car-porting/model-port.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Developing a car model port
|
||||
|
||||
A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
|
||||
85
docs/car-porting/reverse-engineering.md
Normal file
85
docs/car-porting/reverse-engineering.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Stimulus-Response Tests
|
||||
|
||||
These are example test drives that can help identify the CAN bus messaging necessary for ADAS control. Each scripted
|
||||
test should be done in a separate route (ignition cycle). These tests are a guide, not necessarily exhaustive.
|
||||
|
||||
While testing, constant power to the comma device is highly recommended, using [comma power](https://comma.ai/shop/comma-power) if
|
||||
necessary to make sure all test activity is fully captured and for ease of uploading. If constant power isn't
|
||||
available, keep the ignition on for at least one minute after your test to make sure power loss doesn't result
|
||||
in loss of the last minute of testing data.
|
||||
|
||||
## Stationary ignition-only tests, part 1
|
||||
|
||||
1. Ignition on, but don't start engine, remain in Park
|
||||
2. Open and close each door in a defined order: driver, passenger, rear left, rear right
|
||||
3. Re-enter the vehicle, close the driver's door, and fasten the driver's seatbelt
|
||||
4. Slowly press and release the accelerator pedal 3 times
|
||||
5. Slowly press and release the brake pedal 3 times
|
||||
6. Hold the brake and move the gearshift to reverse, then neutral, then drive, then sport/eco/etc if applicable
|
||||
7. Return to Park, ignition off
|
||||
|
||||
Brake-pressed information may show up in several messages and signals, both as on/off states and as a percentage or
|
||||
pressure. It may reflect a switch on the driver's brake pedal, or a pressure-threshold state, or signals to turn on
|
||||
the rear brake lights. Start by identifying all the potential signals, and confirm while driving with ACC later.
|
||||
|
||||
Locate signals for all four door states if possible, but some cars only expose the driver's door state on the ADAS bus.
|
||||
Driver/passenger door signals may or may not change positions for LHD vs RHD cars. For cars where only the driver's
|
||||
door signal is available, the same signal may follow the driver.
|
||||
|
||||
## Stationary ignition-only tests, part 2
|
||||
|
||||
1. Ignition on, but don't start engine, remain in Park
|
||||
2. Press each ACC button in a defined order: main switch on/off, set, resume, cancel, accel, decel, gap adjust
|
||||
3. Set the left turn signal for about five seconds
|
||||
4. Operate the left turn signal one time in its touch-to-pass mode
|
||||
5. Set the right turn signal for about five seconds
|
||||
6. Operate the right turn signal one time in its touch-to-pass mode
|
||||
7. Set the hazard / emergency indicator switch for about five seconds
|
||||
8. Ignition off
|
||||
|
||||
Your vehicle may have a momentary-press main ACC switch or a physical toggle that remains set. Actual ACC engagement
|
||||
isn't necessary for purposes of detecting the ACC button presses.
|
||||
|
||||
## Steering angle and steering torque tests
|
||||
|
||||
Power steering should be available. On ICE cars, engine RPM may be present.
|
||||
|
||||
1. Ignition on, start engine if applicable, remain in Park
|
||||
2. Rotate the steering wheel as follows, with a few seconds pause between each step
|
||||
* Start as close to exact center as possible
|
||||
* Turn to 45 degrees right and hold
|
||||
* Turn to 90 degrees right and hold
|
||||
* Turn to 180 degrees right and hold
|
||||
* Turn to full lock right and hold, with firm pressure against lock
|
||||
* Release the wheel and allow it to bounce back slightly from lock
|
||||
* Turn to 180 degrees left and hold
|
||||
* Return to center and release
|
||||
3. Ignition off
|
||||
|
||||
Performing the full test to the right, followed by an abbreviated test to the left, helps give additional confirmation
|
||||
of signal scale, and sign/direction for both the steering wheel angle and driver input torque signals.
|
||||
|
||||
## Low speed / parking lot driving tests
|
||||
|
||||
Before this test, drive to a place like an empty parking lot where you are free to drive in a series of curves.
|
||||
|
||||
1. Ignition on, start engine if applicable, prepare to drive
|
||||
2. Slowly (10-20mph at most) drive a figure-8 if possible, or at least one sharp left and one sharp right.
|
||||
3. Come to a complete stop
|
||||
4. When and where safe, drive in reverse for a short distance (10-15 feet)
|
||||
5. Park the car in a safe place, ignition off
|
||||
|
||||
## High speed / highway driving tests
|
||||
|
||||
Select a place and time where you can safely set cruise control at normal travel speeds with little interference from
|
||||
traffic ahead, and safely test the response of your factory lane guidance system.
|
||||
|
||||
1. Ignition on, start engine if applicable, prepare to drive
|
||||
2. When safely able, engage adaptive cruise control below 50 mph
|
||||
3. When safely able, use the ACC buttons to accelerate to 50mph, then 55mph, then 60mph
|
||||
4. Disengage adaptive cruise
|
||||
5. When safely able, allow your factory lane guidance to prevent lane departures, 2-3 times on both the left and right
|
||||
|
||||
The series of setpoints can be adjusted to local traffic regulations, and of course metric units. The specific cruise
|
||||
setpoints are useful for locating the ACC HUD signals later, and confirming their precise scaling. When the car reaches
|
||||
and holds the setpoint, that can also provide additional confirmation of wheel speed scaling.
|
||||
@@ -8,7 +8,7 @@ A car port enables openpilot support on a particular car. Each car model openpil
|
||||
|
||||
# Structure of a car port
|
||||
|
||||
All car-specific code is contained in the [opendbc](https://github.com/commaai/opendbc) project.
|
||||
Virtually all car-specific code is contained in two other repositories: [opendbc](https://github.com/commaai/opendbc) and [panda](https://github.com/commaai/panda).
|
||||
|
||||
## opendbc
|
||||
|
||||
@@ -23,8 +23,8 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b
|
||||
|
||||
## safety
|
||||
|
||||
* `opendbc/safety/modes/[brand].h`: Brand-specific safety logic
|
||||
* `opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
|
||||
* `opendbc_repo/opendbc/safety/modes/[brand].h`: Brand-specific safety logic
|
||||
* `opendbc_repo/opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
|
||||
|
||||
## openpilot
|
||||
|
||||
@@ -32,20 +32,8 @@ For historical reasons, openpilot still contains a small amount of car-specific
|
||||
|
||||
* `selfdrive/car/car_specific.py`: Brand-specific event logic
|
||||
|
||||
# How do I port car?
|
||||
# Overview
|
||||
|
||||
[Jason Young](https://github.com/jyoung8607) gave a talk at COMMA_CON with an overview of the car porting process. The talk is available on YouTube:
|
||||
|
||||
https://www.youtube.com/watch?v=XxPS5TpTUnI
|
||||
|
||||
## Brand Port
|
||||
|
||||
A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
|
||||
|
||||
## Model Port
|
||||
|
||||
A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
|
||||
@@ -1,3 +1,9 @@
|
||||
# openpilot glossary
|
||||
|
||||
{{GLOSSARY_DEFINITIONS}}
|
||||
* **onroad**: openpilot's system state while ignition is on
|
||||
* **offroad**: openpilot's system state while ignition is off
|
||||
* **route**: a route is a recording of an onroad session
|
||||
* **segment**: routes are split into one minute chunks called segments.
|
||||
* **comma connect**: the web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).
|
||||
* **panda**: this is the secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).
|
||||
* **comma four**: the latest hardware by comma.ai for running openpilot. more info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
|
||||
|
||||
@@ -6,9 +6,9 @@ Check out our [Python library](https://github.com/commaai/openpilot/blob/master/
|
||||
|
||||
For each segment, openpilot records the following log types:
|
||||
|
||||
## rlog.zst
|
||||
## rlog.bz2
|
||||
|
||||
rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/openpilot/blob/master/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages.
|
||||
rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for a list of all the logged services. They're a bzip2 archive of the serialized capnproto messages.
|
||||
|
||||
## {f,e,d}camera.hevc
|
||||
|
||||
@@ -18,10 +18,12 @@ Each camera stream is H.265 encoded and written to its respective file.
|
||||
* `ecamera.hevc` is the wide road camera
|
||||
* `dcamera.hevc` is the driver camera
|
||||
|
||||
## qlog.zst & qcamera.ts
|
||||
## qlog.bz2 & qcamera.ts
|
||||
|
||||
qlogs are a decimated subset of the rlogs. Check out [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for the decimation.
|
||||
|
||||
|
||||
qcameras are H.264 encoded, lower res versions of the fcamera.hevc. The video shown in [comma connect](https://connect.comma.ai/) is from the qcameras.
|
||||
|
||||
qlogs and qcameras are designed to be small enough to upload instantly on slow internet, yet useful enough for most analysis and debugging.
|
||||
|
||||
qlogs and qcameras are designed to be small enough to upload instantly on slow internet and store forever, yet useful enough for most analysis and debugging.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# How to Give Feedback
|
||||
|
||||
Feedback is one of the highest leverage ways to contribute to openpilot as a user.
|
||||
|
||||
## Driving
|
||||
|
||||
Got feedback about how your car drives?
|
||||
Join the community Discord, then use the form in `#submit-feedback`.
|
||||
|
||||
Before posting feedback, please ensure:
|
||||
|
||||
- **openpilot is up to date** you should be on the latest openpilot release or nightly
|
||||
- **both road-facing cameras have a clear view** your windshield is clean, lenses are clean, etc.
|
||||
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
|
||||
|
||||
## Driver Monitoring
|
||||
|
||||
If you find DM annoying while being perfectly attentive, these are likely false positives and we want to fix them!
|
||||
In general, driver monitoring feedback is very actionable, and we can fix your complaint within a release cycle.
|
||||
|
||||
To post your feedback:
|
||||
|
||||
1. Join the [community Discord](https://discord.comma.ai).
|
||||
2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue.
|
||||
3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera.
|
||||
4. Post the segment in the `#openpilot-experience` channel on Discord with a good description.
|
||||
|
||||
Before posting feedback, please ensure:
|
||||
|
||||
- **openpilot is up to date** you should be on the latest openpilot release or nightly
|
||||
- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
|
||||
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
|
||||
|
||||
## Other bugs
|
||||
|
||||
Got an issue with something else? Open an issue on our [GitHub issue tracker](https://github.com/commaai/openpilot/issues/new/choose).
|
||||
@@ -7,11 +7,25 @@ This is the roadmap for the next major openpilot releases. Also check out
|
||||
* [Bounties](https://comma.ai/bounties) for paid individual issues
|
||||
* [#current-projects](https://discord.com/channels/469524606043160576/1249579909739708446) in Discord for discussion on work-in-progress projects
|
||||
|
||||
## openpilot 0.10
|
||||
|
||||
openpilot 0.10 will be the first release with a driving policy trained in
|
||||
a [learned simulator](https://youtu.be/EqQNZXqzFSI).
|
||||
|
||||
* Driving model trained in a learned simulator
|
||||
* Always-on driver monitoring (behind a toggle)
|
||||
* GPS removed from the driving stack
|
||||
* 100KB qlogs
|
||||
* `nightly` pushed after 1000 hours of hardware-in-the-loop testing
|
||||
* Car interface code moved into [opendbc](https://github.com/commaai/opendbc)
|
||||
* openpilot on PC for Linux x86, Linux arm64, and Mac (Apple Silicon)
|
||||
|
||||
## openpilot 1.0
|
||||
|
||||
openpilot 1.0 will feature a fully end-to-end driving policy.
|
||||
|
||||
* End-to-end longitudinal control in Chill mode
|
||||
* Automatic Emergency Braking (AEB)
|
||||
* Driver monitoring with sleep detection
|
||||
* Rolling updates/releases pushed out by CI
|
||||
* [panda safety 1.0](https://github.com/orgs/commaai/projects/27)
|
||||
|
||||
44
docs/css/tooltip.css
Normal file
44
docs/css/tooltip.css
Normal file
@@ -0,0 +1,44 @@
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
|
||||
[data-tooltip] .tooltip-content {
|
||||
width: max-content;
|
||||
max-width: 25em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: white;
|
||||
color: #404040;
|
||||
box-shadow: 0 4px 14px 0 rgba(0,0,0,.2), 0 0 0 1px rgba(0,0,0,.05);
|
||||
padding: 10px;
|
||||
font: 14px/1.5 Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.1s, visibility 0s;
|
||||
z-index: 1000;
|
||||
pointer-events: none; /* Prevent accidental interaction */
|
||||
}
|
||||
|
||||
[data-tooltip]:hover .tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto; /* Allow interaction when visible */
|
||||
}
|
||||
|
||||
.tooltip-content .tooltip-glossary-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tooltip-content .tooltip-glossary-link:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import posixpath
|
||||
import re
|
||||
import tomllib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.preprocessors import Preprocessor
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from zensical.extensions.links import LinksProcessor
|
||||
|
||||
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
||||
|
||||
GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
|
||||
|
||||
SKIP_TAGS = {
|
||||
"a",
|
||||
"code",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"kbd",
|
||||
"pre",
|
||||
"script",
|
||||
"style",
|
||||
}
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
text = re.sub(r"[*_~]", "", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def load_glossary() -> tuple[list[GlossaryTerm], str]:
|
||||
with GLOSSARY_FILE.open("rb") as f:
|
||||
glossary_data = tomllib.load(f).get("glossary", {})
|
||||
|
||||
glossary: list[GlossaryTerm] = []
|
||||
rendered = []
|
||||
for key, value in glossary_data.items():
|
||||
label = str(key).strip().replace("_", " ")
|
||||
description = str(value).strip()
|
||||
if not description:
|
||||
continue
|
||||
|
||||
slug = label.replace(" ", "-").replace("_", "-").lower()
|
||||
glossary.append((slug, re.compile(rf"(?<!\w){re.escape(label)}(?!\w)", re.IGNORECASE), clean_tooltip(description)))
|
||||
rendered.append(f'* <span id="{slug}"></span>**{label}**: {description}')
|
||||
|
||||
return glossary, "\n".join(rendered)
|
||||
|
||||
|
||||
class GlossaryPreprocessor(Preprocessor):
|
||||
def __init__(self, md, glossary: str):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
markdown = "\n".join(lines)
|
||||
if GLOSSARY_PLACEHOLDER not in markdown:
|
||||
return lines
|
||||
return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
|
||||
|
||||
|
||||
class GlossaryTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, glossary: list[GlossaryTerm]):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
self.seen: set[str] = set()
|
||||
|
||||
def run(self, root: ET.Element) -> None:
|
||||
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
||||
processor = self.md.treeprocessors[at]
|
||||
if not isinstance(processor, LinksProcessor):
|
||||
raise TypeError("Links processor not registered")
|
||||
if processor.path == GLOSSARY_PAGE:
|
||||
return
|
||||
|
||||
self.seen.clear()
|
||||
glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
|
||||
self._walk(root, glossary_href)
|
||||
|
||||
def _walk(self, element: ET.Element, glossary_href: str) -> None:
|
||||
if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
|
||||
return
|
||||
|
||||
self._replace(element, glossary_href)
|
||||
|
||||
idx = 0
|
||||
while idx < len(element):
|
||||
child = element[idx]
|
||||
self._walk(child, glossary_href)
|
||||
idx = self._replace(element, glossary_href, idx) + 1
|
||||
|
||||
def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
|
||||
child = None if index is None else parent[index]
|
||||
text = parent.text if child is None else child.tail
|
||||
pieces = self._pieces(text or "", glossary_href)
|
||||
if not pieces:
|
||||
return -1 if index is None else index
|
||||
|
||||
if child is None:
|
||||
parent.text = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
# Insert replacements for parent.text before the first existing child.
|
||||
insert_at = -1
|
||||
else:
|
||||
assert index is not None
|
||||
child.tail = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
insert_at = index
|
||||
|
||||
start = 1 if isinstance(pieces[0], str) else 0
|
||||
previous = child
|
||||
|
||||
for piece in pieces[start:]:
|
||||
if isinstance(piece, str):
|
||||
previous.tail = (previous.tail or "") + piece
|
||||
continue
|
||||
|
||||
insert_at += 1
|
||||
parent.insert(insert_at, piece)
|
||||
previous = piece
|
||||
|
||||
return insert_at
|
||||
|
||||
def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
pieces: list[str | ET.Element] = []
|
||||
cursor = 0
|
||||
|
||||
while True:
|
||||
best = None
|
||||
for slug, pattern, tooltip in self.glossary:
|
||||
if slug in self.seen:
|
||||
continue
|
||||
|
||||
found = pattern.search(text, cursor)
|
||||
if found is None:
|
||||
continue
|
||||
|
||||
candidate = (slug, tooltip, found.start(), found.end())
|
||||
if best is None:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
_, _, best_start, best_end = best
|
||||
_, _, current_start, current_end = candidate
|
||||
if current_start < best_start:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
if current_start == best_start and current_end - current_start > best_end - best_start:
|
||||
best = candidate
|
||||
|
||||
if best is None:
|
||||
break
|
||||
|
||||
slug, tooltip, start, end = best
|
||||
if start > cursor:
|
||||
pieces.append(text[cursor:start])
|
||||
|
||||
link = ET.Element(
|
||||
"a",
|
||||
{
|
||||
"class": "glossary-term",
|
||||
"data-glossary-term": "",
|
||||
"href": f"{glossary_href}{slug}",
|
||||
},
|
||||
)
|
||||
ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
|
||||
ET.SubElement(
|
||||
link,
|
||||
"span",
|
||||
{
|
||||
"class": "glossary-term__tooltip",
|
||||
"data-search-exclude": "",
|
||||
},
|
||||
).text = tooltip
|
||||
pieces.append(link)
|
||||
self.seen.add(slug)
|
||||
cursor = end
|
||||
|
||||
if not pieces:
|
||||
return []
|
||||
if cursor < len(text):
|
||||
pieces.append(text[cursor:])
|
||||
return pieces
|
||||
|
||||
|
||||
class GlossaryExtension(Extension):
|
||||
def extendMarkdown(self, md) -> None:
|
||||
md.registerExtension(self)
|
||||
glossary, rendered = load_glossary()
|
||||
|
||||
md.preprocessors.register(
|
||||
GlossaryPreprocessor(md, rendered),
|
||||
"docs-ext-glossary-preprocessor",
|
||||
27,
|
||||
)
|
||||
md.treeprocessors.register(
|
||||
GlossaryTreeprocessor(md, glossary),
|
||||
"docs-ext-glossary-treeprocessor",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def makeExtension(**kwargs) -> GlossaryExtension:
|
||||
return GlossaryExtension(**kwargs)
|
||||
@@ -1,8 +0,0 @@
|
||||
[glossary]
|
||||
onroad = "openpilot's system state while ignition is on."
|
||||
offroad = "openpilot's system state while ignition is off."
|
||||
route = "A route is a recording of an onroad session."
|
||||
segment = "Routes are split into one minute chunks called segments."
|
||||
"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
|
||||
panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
|
||||
"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
|
||||
12
docs/getting-started/what-is-openpilot.md
Normal file
12
docs/getting-started/what-is-openpilot.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# What is openpilot?
|
||||
|
||||
[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
|
||||
|
||||
|
||||
## How do I use it?
|
||||
|
||||
openpilot is designed to be used on the comma four.
|
||||
|
||||
## How does it work?
|
||||
|
||||
In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
|
||||
68
docs/hooks/glossary.py
Normal file
68
docs/hooks/glossary.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
def load_glossary(file_path="docs/glossary.toml"):
|
||||
with open(file_path, "rb") as f:
|
||||
glossary_data = tomllib.load(f)
|
||||
return glossary_data.get("glossary", {})
|
||||
|
||||
def generate_anchor_id(name):
|
||||
return name.replace(" ", "-").replace("_", "-").lower()
|
||||
|
||||
def format_markdown_term(name, definition):
|
||||
anchor_id = generate_anchor_id(name)
|
||||
markdown = f"* [**{name.replace('_', ' ').title()}**](#{anchor_id})"
|
||||
if definition.get("abbreviation"):
|
||||
markdown += f" *({definition['abbreviation']})*"
|
||||
if definition.get("description"):
|
||||
markdown += f": {definition['description']}\n"
|
||||
return markdown
|
||||
|
||||
def glossary_markdown(vocabulary):
|
||||
markdown = ""
|
||||
for category, terms in vocabulary.items():
|
||||
markdown += f"## {category.replace('_', ' ').title()}\n\n"
|
||||
for name, definition in terms.items():
|
||||
markdown += format_markdown_term(name, definition)
|
||||
return markdown
|
||||
|
||||
def format_tooltip_html(term_key, definition, html):
|
||||
display_term = term_key.replace("_", " ").title()
|
||||
clean_description = re.sub(r"\[(.+)]\(.+\)", r"\1", definition["description"])
|
||||
glossary_link = (
|
||||
f"<a href='/concepts/glossary#{term_key}' class='tooltip-glossary-link' title='View in glossary'>Glossary🔗</a>"
|
||||
)
|
||||
return re.sub(
|
||||
re.escape(display_term),
|
||||
lambda
|
||||
match: f"<span data-tooltip>{match.group(0)}<span class='tooltip-content'>{clean_description} {glossary_link}</span></span>",
|
||||
html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
def apply_tooltip(_term_key, _definition, pattern, html):
|
||||
return re.sub(
|
||||
pattern,
|
||||
lambda match: format_tooltip_html(_term_key, _definition, match.group(0)),
|
||||
html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
def tooltip_html(vocabulary, html):
|
||||
for _category, terms in vocabulary.items():
|
||||
for term_key, definition in terms.items():
|
||||
if definition.get("description"):
|
||||
pattern = rf"(?<!\w){re.escape(term_key.replace('_', ' ').title())}(?![^<]*<\/a>)(?!\([^)]*\))"
|
||||
html = apply_tooltip(term_key, definition, pattern, html)
|
||||
return html
|
||||
|
||||
# Page Hooks
|
||||
def on_page_markdown(markdown, **kwargs):
|
||||
glossary = load_glossary()
|
||||
return markdown.replace("{{GLOSSARY_DEFINITIONS}}", glossary_markdown(glossary))
|
||||
|
||||
def on_page_content(html, **kwargs):
|
||||
if kwargs.get("page").title == "Glossary":
|
||||
return html
|
||||
glossary = load_glossary()
|
||||
return tooltip_html(glossary, html)
|
||||
@@ -1,15 +1,15 @@
|
||||
# connect to a comma 3X or comma four
|
||||
# connect to a comma four
|
||||
|
||||
A comma device is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
|
||||
A comma four is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
|
||||
|
||||
## Serial Console
|
||||
|
||||
On the comma 3X, the serial console is accessible from the main OBD-C port, forwarded through the panda.
|
||||
Access it using `panda/scripts/som_debug.sh`.
|
||||
On both the comma three and comma four, the serial console is accessible from the main OBD-C port.
|
||||
Connect the comma four to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
|
||||
|
||||
comma four also exposes a serial console, albeit through an internal debug connector. Dedicated debug hardware coming soon to the comma shop.
|
||||
On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
|
||||
|
||||
Login to the default user with:
|
||||
On the comma four, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
|
||||
|
||||
* Username: `comma`
|
||||
* Password: `comma`
|
||||
@@ -25,7 +25,7 @@ In order to SSH into your device, you'll need a GitHub account with SSH keys. Se
|
||||
* Port: `22`
|
||||
|
||||
Here's an example command for connecting to your device using its tethered connection:<br />
|
||||
`ssh comma@192.168.43.1 -i ~/.ssh/my_github_key`
|
||||
`ssh comma@192.168.43.1`
|
||||
|
||||
For doing development work on device, it's recommended to use [SSH agent forwarding](https://docs.github.com/en/developers/overview/using-ssh-agent-forwarding).
|
||||
|
||||
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
|
||||
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
|
||||
|
||||
> [!NOTE]
|
||||
> The default port for ADB is 5555.
|
||||
> The default port for ADB is 5555 on the comma four.
|
||||
|
||||
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).
|
||||
|
||||
@@ -55,7 +55,7 @@ The public keys are only fetched from your GitHub account once. In order to upda
|
||||
|
||||
The `id_rsa` key in this directory only works while your device is in the setup state with no software installed. After installation, that default key will be removed.
|
||||
|
||||
## ssh.comma.ai proxy
|
||||
#### ssh.comma.ai proxy
|
||||
|
||||
With a [comma prime subscription](https://comma.ai/connect), you can SSH into your comma device from anywhere.
|
||||
|
||||
@@ -79,7 +79,6 @@ Host ssh.comma.ai
|
||||
```
|
||||
ssh -i ~/.ssh/my_github_key -o ProxyCommand="ssh -i ~/.ssh/my_github_key -W %h:%p -p %p %h@ssh.comma.ai" comma@ffffffffffffffff
|
||||
```
|
||||
|
||||
(Replace `ffffffffffffffff` with your dongle_id)
|
||||
|
||||
### ssh.comma.ai host key fingerprint
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# What is openpilot?
|
||||
|
||||
[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
|
||||
|
||||
|
||||
## How do I use it?
|
||||
|
||||
openpilot is designed to be used on the comma four.
|
||||
|
||||
## How does it work?
|
||||
|
||||
In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
|
||||
1
docs/index.md
Symbolic link
1
docs/index.md
Symbolic link
@@ -0,0 +1 @@
|
||||
getting-started/what-is-openpilot.md
|
||||
@@ -1,42 +0,0 @@
|
||||
.md-logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
44
mkdocs.yml
Normal file
44
mkdocs.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
site_name: openpilot docs
|
||||
repo_url: https://github.com/commaai/openpilot/
|
||||
site_url: https://docs.comma.ai
|
||||
|
||||
exclude_docs: README.md
|
||||
|
||||
strict: true
|
||||
docs_dir: docs
|
||||
site_dir: docs_site/
|
||||
|
||||
hooks:
|
||||
- docs/hooks/glossary.py
|
||||
extra_css:
|
||||
- css/tooltip.css
|
||||
theme:
|
||||
name: readthedocs
|
||||
navigation_depth: 3
|
||||
|
||||
nav:
|
||||
- Getting Started:
|
||||
- What is openpilot?: getting-started/what-is-openpilot.md
|
||||
- How-to:
|
||||
- Turn the speed blue: how-to/turn-the-speed-blue.md
|
||||
- Connect to a comma 3X: how-to/connect-to-comma.md
|
||||
# - Make your first pull request: how-to/make-first-pr.md
|
||||
#- Replay a drive: how-to/replay-a-drive.md
|
||||
- Concepts:
|
||||
- Logs: concepts/logs.md
|
||||
- Safety: concepts/safety.md
|
||||
- Glossary: concepts/glossary.md
|
||||
- Car Porting:
|
||||
- What is a car port?: car-porting/what-is-a-car-port.md
|
||||
- Porting a car brand: car-porting/brand-port.md
|
||||
- Porting a car model: car-porting/model-port.md
|
||||
- Contributing:
|
||||
- Roadmap: contributing/roadmap.md
|
||||
#- Architecture: contributing/architecture.md
|
||||
- Contributing Guide →: https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md
|
||||
- Links:
|
||||
- Blog →: https://blog.comma.ai
|
||||
- Bounties →: https://comma.ai/bounties
|
||||
- GitHub →: https://github.com/commaai
|
||||
- Discord →: https://discord.comma.ai
|
||||
- X →: https://x.com/comma_ai
|
||||
Submodule opendbc_repo updated: df807f8be3...427032a89a
2
panda
2
panda
Submodule panda updated: 5a90799dac...6cd1972ecf
@@ -82,7 +82,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"Jinja2",
|
||||
"zensical",
|
||||
"mkdocs",
|
||||
]
|
||||
|
||||
testing = [
|
||||
@@ -150,7 +150,7 @@ quiet-level = 3
|
||||
# if you've got a short variable name that's getting flagged, add it here
|
||||
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite,ser"
|
||||
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, *.pem, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
||||
|
||||
# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml
|
||||
[tool.ruff]
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
wrapper that materializes symlinks in docs/ before build
|
||||
|
||||
we can delete this once zensical supports symlinks:
|
||||
https://github.com/zensical/backlog/issues/55
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
SITE_DIR = REPO_ROOT / "docs_site"
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
# Local docs build helpers live under docs/ so they stay near the content
|
||||
# source. The wrapper prunes them from docs_site/ after build.
|
||||
sys.path.insert(0, str(DOCS_DIR))
|
||||
|
||||
|
||||
def _materialize(docs: Path) -> dict[Path, str]:
|
||||
originals: dict[Path, str] = {}
|
||||
for link in docs.rglob("*"):
|
||||
if not link.is_symlink():
|
||||
continue
|
||||
target = link.resolve()
|
||||
if not target.is_file():
|
||||
continue
|
||||
originals[link] = os.readlink(link)
|
||||
link.unlink()
|
||||
shutil.copy2(target, link)
|
||||
return originals
|
||||
|
||||
|
||||
def _restore(originals: dict[Path, str]) -> None:
|
||||
for link, target in originals.items():
|
||||
link.unlink(missing_ok=True)
|
||||
os.symlink(target, link)
|
||||
|
||||
|
||||
def _raise_interrupt(*_):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def _prune_site_output() -> None:
|
||||
shutil.rmtree(SITE_DIR / "ext", ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
signal.signal(signal.SIGTERM, _raise_interrupt)
|
||||
originals = _materialize(DOCS_DIR)
|
||||
try:
|
||||
from zensical.main import cli
|
||||
cli(standalone_mode=False)
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||||
_prune_site_output()
|
||||
finally:
|
||||
_restore(originals)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -33,7 +33,16 @@ if __name__ == "__main__":
|
||||
print("|-| ----- | --------- |")
|
||||
|
||||
for f in glob.glob(BASEDIR + MODEL_PATH + "/*.onnx"):
|
||||
# TODO: add checkpoint to DM
|
||||
if "dmonitoring" in f:
|
||||
continue
|
||||
|
||||
fn = os.path.basename(f)
|
||||
master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn)
|
||||
master_path = MASTER_PATH + MODEL_PATH + fn
|
||||
if os.path.exists(master_path):
|
||||
master = get_checkpoint(master_path)
|
||||
master_col = f"[{master}](https://reporter.comma.life/experiment/{master})"
|
||||
else:
|
||||
master_col = "N/A (new model)"
|
||||
pr = get_checkpoint(BASEDIR + MODEL_PATH + fn)
|
||||
print("|", fn, "|", f"[{master}](https://reporterv2.comma.life/{master})", "|", f"[{pr}](https://reporterv2.comma.life/{pr})", "|")
|
||||
print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_center.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_center.png
LFS
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{% set footnote_tag = '[<sup>{}</sup>](#footnotes)' %}
|
||||
{% set star_icon = '[](##)' %}
|
||||
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>' %}
|
||||
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>' %}
|
||||
{# Force hardware column wider by using a blank image with max width. #}
|
||||
{% set width_tag = '<a href="##"><img width=2000></a>%s<br> ' %}
|
||||
{% set hardware_col_name = 'Hardware Needed' %}
|
||||
|
||||
@@ -11,7 +11,7 @@ FOOTNOTE_TAG = "<sup>{}</sup>"
|
||||
STAR_ICON = '<a href="##"><img valign="top" ' + \
|
||||
'src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-star-{}.svg" width="22" /></a>'
|
||||
VIDEO_ICON = '<a href="{}" target="_blank">' + \
|
||||
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg" /></a>'
|
||||
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg"></img></a>'
|
||||
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
|
||||
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
|
||||
ARROW_SYMBOL = "➡️"
|
||||
|
||||
@@ -21,7 +21,7 @@ tg_flags = {
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
|
||||
|
||||
# Get model metadata
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)]
|
||||
cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
@@ -59,6 +59,5 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -40,8 +40,10 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
MODELS_DIR = Path(__file__).parent / 'models'
|
||||
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
|
||||
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
||||
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
|
||||
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
||||
ON_POLICY_PKL_PATH = MODELS_DIR / 'driving_on_policy_tinygrad.pkl'
|
||||
ON_POLICY_METADATA_PATH = MODELS_DIR / 'driving_on_policy_metadata.pkl'
|
||||
OFF_POLICY_PKL_PATH = MODELS_DIR / 'driving_off_policy_tinygrad.pkl'
|
||||
OFF_POLICY_METADATA_PATH = MODELS_DIR / 'driving_off_policy_metadata.pkl'
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
LONG_SMOOTH_SECONDS = 0.3
|
||||
@@ -156,7 +158,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
with open(OFF_POLICY_METADATA_PATH, 'rb') as f:
|
||||
off_policy_metadata = pickle.load(f)
|
||||
self.off_policy_input_shapes = off_policy_metadata['input_shapes']
|
||||
self.off_policy_output_slices = off_policy_metadata['output_slices']
|
||||
off_policy_output_size = off_policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(ON_POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
@@ -180,11 +188,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
|
||||
self.off_policy_output = np.zeros(off_policy_output_size, dtype=np.float32)
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
|
||||
self.update_imgs = None
|
||||
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH)))
|
||||
self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH)))
|
||||
|
||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
@@ -233,9 +243,17 @@ class ModelState(ModelStateBase):
|
||||
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
|
||||
self.off_policy_output = self.off_policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
off_policy_outputs_dict = self.parser.parse_off_policy_outputs(self.slice_outputs(self.off_policy_output, self.off_policy_output_slices))
|
||||
off_policy_outputs_dict.pop('plan')
|
||||
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **off_policy_outputs_dict, **policy_outputs_dict}
|
||||
if 'planplus' in combined_outputs_dict and 'plan' in combined_outputs_dict:
|
||||
combined_outputs_dict['plan'] = combined_outputs_dict['plan'] + combined_outputs_dict['planplus']
|
||||
if SEND_RAW_PRED:
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy(), self.off_policy_output.copy()])
|
||||
|
||||
return combined_outputs_dict
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
driving_policy.onnx
|
||||
@@ -1 +0,0 @@
|
||||
driving_vision.onnx
|
||||
BIN
selfdrive/modeld/models/driving_off_policy.onnx
LFS
Normal file
BIN
selfdrive/modeld/models/driving_off_policy.onnx
LFS
Normal file
Binary file not shown.
BIN
selfdrive/modeld/models/driving_on_policy.onnx
LFS
Normal file
BIN
selfdrive/modeld/models/driving_on_policy.onnx
LFS
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -96,11 +96,17 @@ class Parser:
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
return outs
|
||||
|
||||
def parse_off_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_binary_crossentropy('lane_lines_prob', outs)
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
self.parse_binary_crossentropy('lead_prob', outs)
|
||||
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
|
||||
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
|
||||
@@ -110,7 +116,7 @@ class Parser:
|
||||
return outs
|
||||
|
||||
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
if 'planplus' in outs:
|
||||
@@ -120,5 +126,6 @@ class Parser:
|
||||
|
||||
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
outs = self.parse_vision_outputs(outs)
|
||||
outs = self.parse_off_policy_outputs(outs)
|
||||
outs = self.parse_policy_outputs(outs)
|
||||
return outs
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#define CUTOFF_IL 400
|
||||
#define SATURATE_IL 1000
|
||||
|
||||
#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048
|
||||
|
||||
ExitHandler do_exit;
|
||||
|
||||
bool check_connected(Panda *panda) {
|
||||
@@ -32,8 +34,15 @@ bool check_connected(Panda *panda) {
|
||||
}
|
||||
|
||||
bool process_mads_heartbeat(SubMaster *sm) {
|
||||
const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience();
|
||||
const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0;
|
||||
|
||||
const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads();
|
||||
return sm->allAliveAndValid({"selfdriveStateSP"}) && mads.getEnabled();
|
||||
const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled();
|
||||
|
||||
const bool engaged = sm->allAliveAndValid({"selfdriveStateSP"}) && heartbeat_type;
|
||||
|
||||
return engaged;
|
||||
}
|
||||
|
||||
Panda *connect(std::string serial) {
|
||||
@@ -143,8 +152,6 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||
ps.setSoundOutputLevel(health.sound_output_level_pkt);
|
||||
ps.setControlsAllowedLateral(health.controls_allowed_lateral_pkt);
|
||||
ps.setControlsAllowedLongitudinal(health.controls_allowed_longitudinal_pkt);
|
||||
}
|
||||
|
||||
void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const can_health_t &can_health) {
|
||||
@@ -299,7 +306,7 @@ void process_panda_state(Panda *panda, PubMaster *pm, bool engaged, bool engaged
|
||||
panda->send_heartbeat(engaged, engaged_mads);
|
||||
}
|
||||
|
||||
void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, bool is_onroad) {
|
||||
void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) {
|
||||
static Params params;
|
||||
static SubMaster sm({"deviceState", "driverCameraState"});
|
||||
|
||||
@@ -309,8 +316,6 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control,
|
||||
static int prev_ir_pwr = 999;
|
||||
static uint32_t prev_frame_id = UINT32_MAX;
|
||||
static bool driver_view = false;
|
||||
static bool not_car = false;
|
||||
static bool not_car_checked = false;
|
||||
|
||||
// TODO: can we merge these?
|
||||
static FirstOrderFilter integ_lines_filter(0, 30.0, 0.05);
|
||||
@@ -356,21 +361,6 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control,
|
||||
ir_pwr = 0;
|
||||
}
|
||||
|
||||
// turn off IR leds if body
|
||||
if (!not_car_checked && is_onroad) {
|
||||
std::string cp_bytes = params.get("CarParams");
|
||||
if (cp_bytes.size() > 0) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
not_car = CP.getNotCar();
|
||||
not_car_checked = true;
|
||||
}
|
||||
}
|
||||
if (not_car) {
|
||||
ir_pwr = 0;
|
||||
}
|
||||
|
||||
if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0) {
|
||||
int16_t ir_panda = util::map_val(ir_pwr, 0, 100, 0, MAX_IR_PANDA_VAL);
|
||||
panda->set_ir_pwr(ir_panda);
|
||||
@@ -390,7 +380,7 @@ void pandad_run(Panda *panda) {
|
||||
|
||||
Params params;
|
||||
RateKeeper rk("pandad", 100);
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP"});
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"});
|
||||
PubMaster pm({"can", "pandaStates", "peripheralState"});
|
||||
PandaSafety panda_safety(panda);
|
||||
bool engaged = false;
|
||||
@@ -404,7 +394,7 @@ void pandad_run(Panda *panda) {
|
||||
|
||||
// Process peripheral state at 20 Hz
|
||||
if (rk.frame() % 5 == 0) {
|
||||
process_peripheral_state(panda, &pm, no_fan_control, is_onroad);
|
||||
process_peripheral_state(panda, &pm, no_fan_control);
|
||||
}
|
||||
|
||||
// Process panda state at 10 Hz
|
||||
|
||||
@@ -230,7 +230,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
if self.CP.notCar:
|
||||
# wait for everything to init first
|
||||
if self.sm.frame > int(2. / DT_CTRL) and self.initialized:
|
||||
if self.sm.frame > int(5. / DT_CTRL) and self.initialized:
|
||||
# body always wants to enable
|
||||
self.events.add(EventName.pcmEnable)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def _diff_capnp_values(v1, v2, path, tolerance):
|
||||
for i in range(n):
|
||||
yield from _diff_capnp_values(v1[i], v2[i], path + (str(i),), tolerance)
|
||||
if n2 > n:
|
||||
yield 'add', dot, [(i, v2[i]) for i in range(n, n2)]
|
||||
yield 'add', dot, list(enumerate(v2[n:], n))
|
||||
if n1 > n:
|
||||
yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)]))
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@ def diff_format(diffs, ref, new, field) -> list[str]:
|
||||
msg_type = field.split(".")[0]
|
||||
ref_ts = [(m.logMonoTime, MsgWrap(m)) for m in ref.get(msg_type, [])]
|
||||
new_wrapped = [MsgWrap(m) for m in new.get(msg_type, [])]
|
||||
if not ref_ts or not new_wrapped:
|
||||
return format_numeric_diffs(diffs)
|
||||
return format_diff(diffs, ref_ts, new_wrapped, field)
|
||||
|
||||
|
||||
|
||||
@@ -7,15 +7,13 @@ from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.version import RELEASE_BRANCHES
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
SETTINGS_ZONE_WIDTH = 280
|
||||
ALERTS_ZONE_WIDTH = 180
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
@@ -30,37 +28,6 @@ NETWORK_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class AlertsPill(Widget):
|
||||
ICON_OFFSET = 12
|
||||
COUNT_OFFSET = 40
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 104, 52))
|
||||
|
||||
self._pill_bg_txt = gui_app.texture("icons_mici/alerts_pill.png", 104, 52)
|
||||
self._warning_txt = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", 36, 36)
|
||||
self._alert_count_callback: Callable[[], int] | None = None
|
||||
|
||||
def set_alert_count_callback(self, callback: Callable[[], int] | None):
|
||||
self._alert_count_callback = callback
|
||||
|
||||
def _render(self, _):
|
||||
alert_count = self._alert_count_callback() if self._alert_count_callback else 0
|
||||
if alert_count > 0:
|
||||
pill_w, pill_h = self._pill_bg_txt.width, self._pill_bg_txt.height
|
||||
rl.draw_texture_ex(self._pill_bg_txt, rl.Vector2(self.rect.x, self.rect.y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
warn_x = self.rect.x + self.ICON_OFFSET
|
||||
warn_y = self.rect.y + (pill_h - self._warning_txt.height) / 2
|
||||
rl.draw_texture_ex(self._warning_txt, rl.Vector2(warn_x, warn_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||
gui_label(count_rect, str(alert_count), font_size=36,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
|
||||
class NetworkIcon(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -117,8 +84,6 @@ class MiciHomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._on_settings_click: Callable | None = None
|
||||
self._on_alerts_click: Callable | None = None
|
||||
self._alert_count_callback: Callable[[], int] | None = None
|
||||
|
||||
self._last_refresh = 0
|
||||
self._mouse_down_t: None | float = None
|
||||
@@ -131,8 +96,6 @@ class MiciHomeLayout(Widget):
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
|
||||
self._alerts_pill = AlertsPill()
|
||||
|
||||
self._status_bar_layout = HBoxLayout([
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
@@ -178,23 +141,13 @@ class MiciHomeLayout(Widget):
|
||||
self._last_refresh = rl.get_time()
|
||||
self._update_params()
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
|
||||
alert_count_callback: Callable[[], int] | None = None):
|
||||
def set_callbacks(self, on_settings: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_alerts_click = on_alerts
|
||||
self._alert_count_callback = alert_count_callback
|
||||
self._alerts_pill.set_alert_count_callback(alert_count_callback)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if not self._did_long_press:
|
||||
relative_x = mouse_pos.x - self.rect.x
|
||||
has_alerts = self._alert_count_callback and self._alert_count_callback() > 0
|
||||
if relative_x < SETTINGS_ZONE_WIDTH:
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
elif has_alerts and relative_x > self.rect.width - ALERTS_ZONE_WIDTH:
|
||||
if self._on_alerts_click:
|
||||
self._on_alerts_click()
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
self._did_long_press = False
|
||||
|
||||
def _get_version_text(self) -> tuple[str, str, str, str] | None:
|
||||
@@ -250,8 +203,3 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||
self._status_bar_layout.render(footer_rect)
|
||||
|
||||
# TODO: add alignment to hboxlayout and add to there
|
||||
self._alerts_pill.set_position(self.rect.x + self.rect.width - self._alerts_pill.rect.width - HOME_PADDING,
|
||||
self.rect.y + self.rect.height - self._alerts_pill.rect.height)
|
||||
self._alerts_pill.render()
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
@@ -60,11 +61,7 @@ class MiciMainLayout(Scroller):
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
on_alerts=lambda: self._scroll_to(self._alerts_layout),
|
||||
alert_count_callback=self._alerts_layout.active_alerts,
|
||||
)
|
||||
self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
|
||||
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
||||
|
||||
@@ -72,11 +69,6 @@ class MiciMainLayout(Scroller):
|
||||
layout_x = int(layout.rect.x)
|
||||
self._scroller.scroll_to(layout_x, smooth=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
# TODO: Hack to run alert updates while not in view. Add a nav stack tick?
|
||||
self._alerts_layout._update_state()
|
||||
|
||||
def _render(self, _):
|
||||
if not self._setup:
|
||||
if self._alerts_layout.active_alerts() > 0:
|
||||
|
||||
@@ -178,8 +178,6 @@ class AugmentedRoadView(CameraView):
|
||||
# update offroad label
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._offroad_label.set_text("system booting")
|
||||
elif ui_state.ignition and not ui_state.started:
|
||||
self._offroad_label.set_text("openpilot can't start\ncheck alerts")
|
||||
else:
|
||||
self._offroad_label.set_text("start the car to\nuse sunnypilot")
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# TODO: Only left for DM preview, remove
|
||||
LOOKING_CENTER_THRESHOLD_UPPER = math.radians(6)
|
||||
LOOKING_CENTER_THRESHOLD_LOWER = math.radians(3)
|
||||
|
||||
@@ -61,6 +59,8 @@ class DriverStateRenderer(Widget):
|
||||
|
||||
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size)
|
||||
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
|
||||
center_size = round(36 / self.BASE_SIZE * self._rect.width)
|
||||
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height))
|
||||
|
||||
def set_should_draw(self, should_draw: bool):
|
||||
@@ -113,7 +113,16 @@ class DriverStateRenderer(Widget):
|
||||
dest_rect,
|
||||
rl.Vector2(dest_rect.width / 2, dest_rect.height / 2),
|
||||
self._rotation_filter.x - 90,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)),
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x * (1 - self._looking_center_filter.x))),
|
||||
)
|
||||
|
||||
rl.draw_texture_ex(
|
||||
self._dm_center,
|
||||
(int(self._rect.x + (self._rect.width - self._dm_center.width) / 2),
|
||||
int(self._rect.y + (self._rect.height - self._dm_center.height) / 2)),
|
||||
0,
|
||||
1.0,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x * self._looking_center_filter.x)),
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -165,22 +174,11 @@ class DriverStateRenderer(Widget):
|
||||
# Get monitoring state
|
||||
driver_data = self.get_driver_data()
|
||||
driver_orient = driver_data.faceOrientation
|
||||
driver_position = driver_data.facePosition
|
||||
|
||||
if len(driver_orient) != 3:
|
||||
return
|
||||
|
||||
# Calibrate orientation so looking straight ahead at road (instead of at device) is (0, 0, 0)
|
||||
sm = ui_state.sm
|
||||
if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
|
||||
cal_rpy = sm['liveCalibration'].rpyCalib
|
||||
else:
|
||||
cal_rpy = [0.0, 0.0, 0.0]
|
||||
|
||||
_, pitch, yaw = face_orientation_from_net(driver_orient, driver_position, cal_rpy)
|
||||
pitch += math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
|
||||
yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
|
||||
|
||||
pitch, yaw, roll = driver_orient
|
||||
pitch = self._pitch_filter.update(pitch)
|
||||
yaw = self._yaw_filter.update(yaw)
|
||||
|
||||
@@ -194,6 +192,7 @@ class DriverStateRenderer(Widget):
|
||||
if DEBUG:
|
||||
pitchd = math.degrees(pitch)
|
||||
yawd = math.degrees(yaw)
|
||||
rolld = math.degrees(roll)
|
||||
|
||||
rl.draw_line_ex((0, 100), (200, 100), 3, rl.RED)
|
||||
rl.draw_line_ex((0, 120), (200, 120), 3, rl.RED)
|
||||
@@ -201,11 +200,13 @@ class DriverStateRenderer(Widget):
|
||||
|
||||
pitch_x = 100 + pitchd
|
||||
yaw_x = 100 + yawd
|
||||
roll_x = 100 + rolld
|
||||
rl.draw_circle(int(pitch_x), 100, 5, rl.GREEN)
|
||||
rl.draw_circle(int(yaw_x), 120, 5, rl.GREEN)
|
||||
rl.draw_circle(int(roll_x), 140, 5, rl.GREEN)
|
||||
|
||||
# filter head rotation, handling wrap-around
|
||||
rotation = math.degrees(math.atan2(pitch * 2, yaw)) # reduce yaw sensitivity
|
||||
rotation = math.degrees(math.atan2(pitch, yaw))
|
||||
angle_diff = rotation - self._rotation_filter.x
|
||||
angle_diff = ((angle_diff + 180) % 360) - 180
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
|
||||
@@ -6,10 +6,12 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, ToggleActionSP
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
|
||||
@@ -23,6 +25,7 @@ class DisplayLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
@@ -61,10 +64,27 @@ class DisplayLayout(Widget):
|
||||
f"{value} s" if value < 60 else f"{int(value/60)} m"),
|
||||
inline=True
|
||||
)
|
||||
self._screensaver_toggle = toggle_item_sp(
|
||||
param="ScreenSaverEnabled",
|
||||
title=lambda: tr("Enable sunnypilot Screen Saver"),
|
||||
description=lambda: tr("Enable screen saver when the device is offroad & idle. " +
|
||||
"The screen saver will kick off after the interactivity timeout expires, and will stay on for the duration configured below."),
|
||||
)
|
||||
self._screensaver_timeout = option_item_sp(
|
||||
param="ScreenSaverTimeout",
|
||||
title=lambda: tr("Screen Saver Timeout"),
|
||||
description=lambda: tr("Configure how long the screen saver should stay on after the interactivity timeout expires."),
|
||||
min_value=60,
|
||||
max_value=600,
|
||||
value_change_step=60,
|
||||
label_callback=lambda value: f"{int(value/60)} m"
|
||||
)
|
||||
items = [
|
||||
self._onroad_brightness,
|
||||
self._onroad_brightness_timer,
|
||||
self._interactivity_timeout,
|
||||
self._screensaver_toggle,
|
||||
self._screensaver_timeout,
|
||||
]
|
||||
return items
|
||||
|
||||
@@ -84,9 +104,21 @@ class DisplayLayout(Widget):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
brightness_val = self._onroad_brightness.action_item.current_value
|
||||
for _item in self._scroller._items:
|
||||
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
|
||||
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
|
||||
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
|
||||
raw_value = self._params.get(_item.action_item.param_key, return_default=True)
|
||||
if _item.action_item.value_map:
|
||||
reverse_map = {v: k for k, v in _item.action_item.value_map.items()}
|
||||
raw_value = reverse_map.get(raw_value, _item.action_item.current_value)
|
||||
_item.action_item.set_value(raw_value)
|
||||
|
||||
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
|
||||
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
|
||||
|
||||
self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state())
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
self._initialize_items()
|
||||
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||
|
||||
@@ -112,7 +112,7 @@ class ModelsLayout(Widget):
|
||||
self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
|
||||
@staticmethod
|
||||
def calculate_cache_size():
|
||||
def _calculate_cache_size():
|
||||
cache_size = 0.0
|
||||
if os.path.exists(CUSTOM_MODEL_PATH):
|
||||
cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2)
|
||||
@@ -122,7 +122,7 @@ class ModelsLayout(Widget):
|
||||
def _callback(response):
|
||||
if response == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
dialog = ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"),
|
||||
tr("Clear Cache"), callback=_callback)
|
||||
@@ -155,7 +155,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
|
||||
self.last_cache_calc_time = current_time
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
device._reset_interactive_timeout()
|
||||
|
||||
@@ -5,45 +5,13 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
class CurrentModelInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.current_model_text = UnifiedLabel(tr("default model"), 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
|
||||
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def _render(self, _):
|
||||
self.current_model_header.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self.current_model_header.render()
|
||||
|
||||
self.current_model_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self.current_model_text.render()
|
||||
|
||||
self.info_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self.info_header.render()
|
||||
|
||||
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.info_text.render()
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
@@ -52,35 +20,25 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_info = CurrentModelInfo()
|
||||
self._download_progress = "."
|
||||
self._download_frame = 0
|
||||
self._was_downloading = False
|
||||
|
||||
self.select_model_btn = BigButton(tr("select model"))
|
||||
self.select_model_btn.set_click_callback(self._show_folders)
|
||||
self.current_model_btn = BigButton(tr("current model"))
|
||||
self.current_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
|
||||
self.main_items = [self.current_model_btn, self.cancel_download_btn]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
|
||||
def _get_grouped_bundles(self, favorites = None):
|
||||
def _get_grouped_bundles(self):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
|
||||
if favorites:
|
||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||
folders.setdefault("favorites", []).append(fav_bundle)
|
||||
|
||||
return folders
|
||||
|
||||
def _show_selection_view(self, items, back_callback: Callable):
|
||||
@@ -91,25 +49,18 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
self.focused_widget = self.current_model_btn
|
||||
folders = self._get_grouped_bundles()
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(tr("default model"))
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder.lower() in ["release models", "master models", "favorites"]:
|
||||
if folder.lower() in ["release models", "master models"]:
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
if folder.lower() == "favorites":
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
|
||||
def _select_model(self, bundle):
|
||||
@@ -121,10 +72,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
folders = self._get_grouped_bundles()
|
||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
||||
|
||||
btns = []
|
||||
@@ -138,62 +86,29 @@ class ModelsLayoutMici(NavScroller):
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._was_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = False
|
||||
if self.focused_widget and self.focused_widget in self.main_items:
|
||||
x = self._scroller._pad
|
||||
for item in self.main_items:
|
||||
if not item.is_visible:
|
||||
continue
|
||||
if item == self.focused_widget:
|
||||
break
|
||||
x += item.rect.width + self._scroller._spacing
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(x)
|
||||
self.focused_widget = None
|
||||
else:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_info.current_model_header._shimmer = False
|
||||
self.current_model_info.info_header._shimmer = False
|
||||
|
||||
manager = self.model_manager
|
||||
self._download_frame += 1
|
||||
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
||||
if should_update:
|
||||
self._download_progress = self._download_progress + "." if len(self._download_progress) < 3 else ""
|
||||
|
||||
is_downloading = (manager.selectedBundle
|
||||
and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
if self._was_downloading and not is_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = is_downloading
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("active model"))
|
||||
self.current_model_info.current_model_text.set_text(manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else tr("default model"))
|
||||
self.current_model_info.info_header.set_text(tr("cache size"))
|
||||
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
||||
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
self.current_model_info.info_header.set_text(tr("error") + self._download_progress)
|
||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.current_model_btn.set_value("downloading...")
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
device.set_override_interactive_timeout(5)
|
||||
progress = 0.0
|
||||
count = 0
|
||||
for model in manager.selectedBundle.models:
|
||||
count += 1
|
||||
p = model.artifact.downloadProgress
|
||||
if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
progress += p.progress
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||
custom.ModelManagerSP.DownloadStatus.cached):
|
||||
progress += 100.0
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("downloading"))
|
||||
self.current_model_info.current_model_header._shimmer = True
|
||||
self.current_model_info.current_model_text.set_text(f"{manager.selectedBundle.internalName.lower()}")
|
||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||
self.current_model_info.info_header._shimmer = True
|
||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
||||
|
||||
else:
|
||||
self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model"))
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.current_model_btn.set_text(tr("current model"))
|
||||
|
||||
@@ -5,32 +5,18 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
ICON_SIZE = 70
|
||||
BIG_ICON_SIZE = 110
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
self._scroller._items[2].set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
self.icon_offroad_enable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/always_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_disable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
@@ -39,53 +25,10 @@ class SettingsLayoutSP(OP.SettingsLayout):
|
||||
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
# onroad: enable button sits at the front (left of toggles)
|
||||
self._enable_offroad_btn_onroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_onroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_onroad.set_visible(lambda: ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
# offroad: enable button sits at the end (right of developer)
|
||||
self._enable_offroad_btn_offroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_offroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_offroad.set_visible(lambda: not ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
self._disable_offroad_btn = BigCircleButton(self.icon_offroad_disable, red=False)
|
||||
self._disable_offroad_btn.set_click_callback(lambda: self._handle_always_offroad(False))
|
||||
self._disable_offroad_btn.set_visible(lambda: ui_state.always_offroad)
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, sunnylink_btn)
|
||||
items.insert(2, models_btn)
|
||||
|
||||
# front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad
|
||||
items.insert(0, self._enable_offroad_btn_onroad)
|
||||
items.insert(0, self._disable_offroad_btn)
|
||||
# end slot: enable-offroad (right of developer)
|
||||
items.append(self._enable_offroad_btn_offroad)
|
||||
|
||||
self._scroller._items.clear()
|
||||
for item in items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
def _handle_always_offroad(self, enable: bool):
|
||||
|
||||
def _set_offroad_status(status: bool):
|
||||
if not ui_state.engaged:
|
||||
ui_state.params.put_bool("OffroadMode", status)
|
||||
ui_state.always_offroad = status
|
||||
|
||||
if not enable:
|
||||
dlg = BigConfirmationDialog(tr("slide to exit always offroad"), self.icon_offroad_slider, red=False,
|
||||
confirm_callback=lambda: _set_offroad_status(False))
|
||||
else:
|
||||
if ui_state.engaged:
|
||||
gui_app.push_widget(BigDialog(tr("disengage to enable always offroad"), "", ))
|
||||
return
|
||||
|
||||
dlg = BigConfirmationDialog(tr("slide to force offroad"), self.icon_offroad_slider, red=True,
|
||||
confirm_callback=lambda: _set_offroad_status(True))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
@@ -4,8 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from cereal import custom
|
||||
@@ -15,43 +13,11 @@ from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkC
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
class SunnylinkInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self.device_id_header = UnifiedLabel(tr("device id"), 48, max_width=max_width, text_color=header_color,
|
||||
font_weight=FontWeight.DISPLAY, shimmer=True)
|
||||
self.device_id_text = UnifiedLabel(UNREGISTERED_SUNNYLINK_DONGLE_ID, 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, scroll=True)
|
||||
|
||||
self.sponsor_header = UnifiedLabel(tr("sponsor tier"), 48, max_width=max_width, text_color=header_color,
|
||||
font_weight=FontWeight.DISPLAY, shimmer=True)
|
||||
self.sponsor_text = UnifiedLabel("N/A", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def _render(self, _):
|
||||
self.device_id_header.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self.device_id_header.render()
|
||||
|
||||
self.device_id_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self.device_id_text.render()
|
||||
|
||||
self.sponsor_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self.sponsor_header.render()
|
||||
|
||||
self.sponsor_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.sponsor_text.render()
|
||||
|
||||
class SunnylinkLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
@@ -61,8 +27,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
self._sunnylink_info = SunnylinkInfo()
|
||||
|
||||
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
|
||||
initial_state=self._sunnylink_enabled,
|
||||
toggle_callback=self._sunnylink_toggle_callback)
|
||||
@@ -76,7 +40,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
toggle_callback=self._sunnylink_uploader_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._sunnylink_info,
|
||||
self._sunnylink_toggle,
|
||||
self._sunnylink_sponsor_button,
|
||||
self._sunnylink_pair_button,
|
||||
@@ -96,10 +59,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
self._sunnylink_uploader_toggle.set_visible(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
self._sunnylink_info.device_id_text.set_text(ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID)
|
||||
self._sunnylink_info.sponsor_text.set_text(ui_state.sunnylink_state.get_sponsor_tier().name.lower() or "N/A")
|
||||
self._sunnylink_info.set_visible(self._sunnylink_enabled)
|
||||
|
||||
if ui_state.sunnylink_state.is_sponsor():
|
||||
self._sunnylink_sponsor_button.set_text(tr("thanks"))
|
||||
self._sunnylink_sponsor_button.set_value(ui_state.sunnylink_state.get_sponsor_tier().name.lower())
|
||||
@@ -116,11 +75,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.update_params()
|
||||
ui_state.sunnylink_state.set_settings_open(True)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.sunnylink_state.set_settings_open(False)
|
||||
|
||||
@staticmethod
|
||||
def _sunnylink_toggle_callback(state: bool):
|
||||
@@ -240,14 +194,9 @@ class SunnylinkPairBigButton(BigButton):
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
|
||||
dlg: BigDialog | SunnylinkPairingDialog | None = None
|
||||
|
||||
if network_type == 0:
|
||||
dlg = BigDialog(tr("no internet"), tr("please connect to WiFi & try again"))
|
||||
elif UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
dlg = BigDialog(tr("sunnylink dongle id not found"), tr("please reboot & try again"))
|
||||
if UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
dlg = BigDialog(tr("sunnylink Dongle ID not found. Please reboot & try again."), "")
|
||||
elif self.sponsor_pairing:
|
||||
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
|
||||
elif not self.sponsor_pairing:
|
||||
|
||||
@@ -11,6 +11,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP
|
||||
|
||||
OpenpilotState = log.SelfdriveState.OpenpilotState
|
||||
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
@@ -34,6 +35,8 @@ class UIStateSP:
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
self.screensaver = ScreenSaverSP()
|
||||
|
||||
self.update_params()
|
||||
|
||||
self.onroad_brightness_timer: int = 0
|
||||
@@ -146,15 +149,27 @@ class UIStateSP:
|
||||
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
||||
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
||||
self.always_offroad = self.params.get_bool("OffroadMode")
|
||||
self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled")
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
@staticmethod
|
||||
def _set_awake(on: bool, _ui_state):
|
||||
def _set_awake(_device, on: bool, _ui_state):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
|
||||
if not on and _ui_state.screensaver_enabled:
|
||||
if _ui_state.screensaver.was_dismissed:
|
||||
_ui_state.screensaver.deinit()
|
||||
gui_app.pop_widget()
|
||||
return True
|
||||
else:
|
||||
_ui_state.screensaver.initialize(dismiss_callback=lambda: _device._set_awake(False))
|
||||
gui_app.push_widget(_ui_state.screensaver)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
|
||||
if not awake or not _ui_state.started:
|
||||
|
||||
@@ -299,11 +299,11 @@ class Device(DeviceSP):
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(on, ui_state)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
if DeviceSP._set_awake(self, on, ui_state):
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
|
||||
|
||||
# Global instance
|
||||
|
||||
@@ -33,7 +33,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.enabled = False
|
||||
self.active = False
|
||||
self.available = False
|
||||
self.lateral_mismatch_counter = 0
|
||||
self.allow_always = False
|
||||
self.no_main_cruise = False
|
||||
self.selfdrive = selfdrive
|
||||
@@ -105,17 +104,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.events.remove(old_event)
|
||||
self.events_sp.add(new_event)
|
||||
|
||||
def data_sample(self):
|
||||
# When the safety and selfdrived do not agree on controls_allowed_lateral
|
||||
# we want to disengage sunnypilot. However the status from the panda goes through
|
||||
# another socket other than the CAN messages and one can arrive earlier than the other.
|
||||
# Therefore we allow a mismatch for two samples, then we trigger the disengagement.
|
||||
if not self.active or self.selfdrive.enabled:
|
||||
self.lateral_mismatch_counter = 0
|
||||
elif any(not ps.controlsAllowedLateral for ps in self.selfdrive.sm['pandaStates']
|
||||
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||
self.lateral_mismatch_counter += 1
|
||||
|
||||
def update_events(self, CS: structs.CarState):
|
||||
if not self.selfdrive.enabled and self.enabled:
|
||||
if CS.standstill:
|
||||
@@ -198,9 +186,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if self.state_machine.state == State.paused:
|
||||
self.events_sp.add(EventNameSP.silentLkasEnable)
|
||||
|
||||
if self.lateral_mismatch_counter >= 200:
|
||||
self.events_sp.add(EventNameSP.controlsMismatchLateral)
|
||||
|
||||
self.events.remove(EventName.pcmDisable)
|
||||
self.events.remove(EventName.buttonCancel)
|
||||
self.events.remove(EventName.pedalPressed)
|
||||
@@ -210,8 +195,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if not self.enabled_toggle:
|
||||
return
|
||||
|
||||
self.data_sample()
|
||||
|
||||
self.update_events(CS)
|
||||
|
||||
if not self.CP.passive and self.selfdrive.initialized:
|
||||
|
||||
@@ -13,7 +13,7 @@ if PC:
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
@@ -42,7 +42,7 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ from openpilot.sunnypilot import get_file_hash
|
||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
|
||||
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
|
||||
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
|
||||
OFF_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_off_policy.onnx")
|
||||
ON_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx")
|
||||
|
||||
|
||||
def update_model_hash():
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH, "w") as f:
|
||||
f.write(combined_hash)
|
||||
|
||||
@@ -1 +1 @@
|
||||
5d4d21f1899de21137f69d74a4602c44cc5a6b04cf4e4aa9d0ec9206f8c30350
|
||||
793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48
|
||||
|
||||
@@ -6,16 +6,17 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, OFF_POLICY_ONNX_PATH, ON_POLICY_ONNX_PATH
|
||||
import hashlib
|
||||
|
||||
|
||||
class TestDefaultModel:
|
||||
def test_compare_onnx_hashes(self):
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -46,8 +46,5 @@ class CruiseHelper:
|
||||
if self.button_frame_counts[ButtonType.gapAdjustCruise] >= DISTANCE_LONG_PRESS and not self.experimental_mode_switched:
|
||||
self._experimental_mode = not experimental_mode
|
||||
self.params.put_bool_nonblocking("ExperimentalMode", self._experimental_mode)
|
||||
if self._experimental_mode:
|
||||
events.add(EventNameSP.experimentalModeSwitched)
|
||||
else:
|
||||
events.add(EventNameSP.chillModeSwitched)
|
||||
events.add(EventNameSP.experimentalModeSwitched)
|
||||
self.experimental_mode_switched = True
|
||||
|
||||
@@ -38,8 +38,6 @@ class ControlsExt(ModelStateBase):
|
||||
enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||
torque_versions = self.params.get("TorqueControlTune")
|
||||
if not enforce_torque_control:
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
return LatControlTorqueV0(self.CP, self.CP_SP, CI, dt) # FIXME-SP: revert when upstream fixes tuning issues with v1
|
||||
return lac
|
||||
|
||||
if torque_versions == 0.0: # v0
|
||||
|
||||
@@ -14,8 +14,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
LAT_PLAN_MIN_IDX = 5
|
||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
# from selfdrive/controls/lib/latcontrol_torque.py
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
|
||||
def get_predicted_lateral_jerk(lat_accels, t_diffs):
|
||||
@@ -58,10 +61,9 @@ class LatControlTorqueExtBase:
|
||||
self.lookahead_lateral_jerk: float = 0.0
|
||||
|
||||
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
|
||||
self.torque_params = lac_torque.torque_params
|
||||
|
||||
self._ff = 0.0
|
||||
self._pid = PIDController(KP, KI)
|
||||
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI)
|
||||
self._pid_log = None
|
||||
self._setpoint = 0.0
|
||||
self._measurement = 0.0
|
||||
|
||||
@@ -75,14 +75,14 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
def update_feedforward_torque_space(self, CS):
|
||||
torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
self._pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
|
||||
CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True)
|
||||
CS.vEgo, CS.aEgo), self.lac_torque.torque_params, gravity_adjusted=True)
|
||||
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
|
||||
FRICTION_THRESHOLD, self.torque_params)
|
||||
FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
def update_output_torque(self, CS):
|
||||
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
@@ -159,6 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.model.friction_override:
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
self.update_output_torque(CS)
|
||||
|
||||
@@ -181,11 +181,7 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
},
|
||||
|
||||
EventNameSP.experimentalModeSwitched: {
|
||||
ET.WARNING: NormalPermanentAlert("Experimental Mode", duration=1.5)
|
||||
},
|
||||
|
||||
EventNameSP.chillModeSwitched: {
|
||||
ET.WARNING: NormalPermanentAlert("Chill Mode", duration=1.5)
|
||||
ET.WARNING: NormalPermanentAlert("Experimental Mode Switched", duration=1.5)
|
||||
},
|
||||
|
||||
EventNameSP.wrongCarModeAlertOnly: {
|
||||
|
||||
@@ -1126,6 +1126,14 @@
|
||||
"title": "Route Count",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverEnabled": {
|
||||
"title": "Enable Screen Saver",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverTimeout": {
|
||||
"title": "Screen Saver Timeout",
|
||||
"description": ""
|
||||
},
|
||||
"SecOCKey": {
|
||||
"title": "Sec Oc Key",
|
||||
"description": ""
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.updated.updated import Updater
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tici", "staging-c3-new", "staging-tici"),
|
||||
("tici", "dev-c3-new", "staging-tici"),
|
||||
("tici", "master", "master-tici"),
|
||||
("tici", "master-dev-c3-new", "master-tici"),
|
||||
("tizi", "staging-c3-new", "staging"),
|
||||
("tizi", "dev-c3-new", "dev"),
|
||||
("tizi", "master-dev-c3-new", "master-dev"),
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_sp_branch_migrations_from_current_branch(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
mocker.patch.object(Updater, "get_branch", return_value=branch)
|
||||
|
||||
assert Updater().target_branch == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tici", "staging-c3-new", "staging-tici"),
|
||||
("tici", "dev-c3-new", "staging-tici"),
|
||||
("tici", "master", "master-tici"),
|
||||
("tici", "master-dev-c3-new", "master-tici"),
|
||||
("tizi", "staging-c3-new", "staging"),
|
||||
("tizi", "dev-c3-new", "dev"),
|
||||
("tizi", "master-dev-c3-new", "master-dev"),
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_sp_branch_migrations_from_param(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.put("UpdaterTargetBranch", branch)
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
|
||||
try:
|
||||
assert Updater().target_branch == expected
|
||||
finally:
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch"), [
|
||||
("tici", "unknown"),
|
||||
("tizi", "unknown"),
|
||||
("mici", "unknown"),
|
||||
])
|
||||
def test_sp_branch_migrations_passthrough(mocker, device_type, branch):
|
||||
params = Params()
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
mocker.patch.object(Updater, "get_branch", return_value=branch)
|
||||
|
||||
assert Updater().target_branch == branch
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import numpy as np
|
||||
import base64
|
||||
import io
|
||||
|
||||
from openpilot.tools.lib.logreader import LogReader, ReadMode
|
||||
|
||||
|
||||
def extract_mem_cpu_data(lr):
|
||||
times, mems, cpus = [], [], []
|
||||
start_time = None
|
||||
|
||||
for msg in lr:
|
||||
if msg.which() == 'procLog':
|
||||
if start_time is None:
|
||||
start_time = msg.logMonoTime
|
||||
mem = msg.procLog.mem
|
||||
mem_usage = (mem.total - mem.available) / mem.total * 100
|
||||
cpu_usages = [(total - cpu.idle) / total * 100 for cpu in msg.procLog.cpuTimes
|
||||
if (total := cpu.idle + cpu.user + cpu.system + cpu.nice + cpu.iowait + cpu.irq + cpu.softirq) > 0]
|
||||
avg_cpu = sum(cpu_usages) / len(cpu_usages) if cpu_usages else 0
|
||||
times.append((msg.logMonoTime - start_time) / 1e9)
|
||||
mems.append(mem_usage)
|
||||
cpus.append(avg_cpu)
|
||||
return times, mems, cpus
|
||||
|
||||
|
||||
def process_segment(lr):
|
||||
return [extract_mem_cpu_data(lr)]
|
||||
|
||||
|
||||
def calculate_r_squared(y_true, y_pred):
|
||||
ss_res = np.sum((y_true - y_pred) ** 2)
|
||||
ss_tot = np.sum((y_true - np.mean(y_true)) ** 2)
|
||||
return 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
|
||||
|
||||
|
||||
def plot_results(segments, segment_data, route_name):
|
||||
valid_data = [d for d in segment_data if d and d[0]]
|
||||
if not valid_data:
|
||||
print("No valid data to plot")
|
||||
return
|
||||
|
||||
avg_mems = [np.mean(d[1]) for d in valid_data]
|
||||
avg_cpus = [np.mean(d[2]) for d in valid_data]
|
||||
valid_segments = [segments[i] for i, d in enumerate(segment_data) if d and d[0]]
|
||||
|
||||
height = max(10, 5 + len(valid_segments) * 0.4)
|
||||
fig1, ax1 = plt.subplots(1, 1, figsize=(12, height), dpi=150)
|
||||
|
||||
y_pos = range(len(valid_segments))
|
||||
ax1.barh([y - 0.2 for y in y_pos], avg_mems, height=0.4, color="dodgerblue", alpha=0.8, label="Avg Mem %")
|
||||
ax1.barh([y + 0.2 for y in y_pos], avg_cpus, height=0.4, color="green", alpha=0.8, label="Avg CPU %")
|
||||
|
||||
for i, (mem, cpu) in enumerate(zip(avg_mems, avg_cpus, strict=True)):
|
||||
ax1.text(mem, i - 0.2, f"{mem:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
ax1.text(cpu, i + 0.2, f"{cpu:.1f}%", va="center", fontsize=8, color="#005a9e", fontweight="bold")
|
||||
|
||||
ax1.set_yticks(y_pos)
|
||||
ax1.set_yticklabels([f"Seg {s}" for s in valid_segments])
|
||||
ax1.set_xlabel("Usage (%)")
|
||||
ax1.set_title("Average Memory and CPU Usage by Segment")
|
||||
ax1.legend()
|
||||
ax1.grid(axis="x", linestyle="--", alpha=0.5)
|
||||
ax1.invert_yaxis()
|
||||
|
||||
fig2, ax2 = plt.subplots(1, 1, figsize=(12, 8), dpi=150)
|
||||
combined_times, combined_mems, combined_cpus = [], [], []
|
||||
time_offset = 0.0
|
||||
for times, mems, cpus in valid_data:
|
||||
if times:
|
||||
combined_times.extend([t + time_offset for t in times])
|
||||
combined_mems.extend(mems)
|
||||
combined_cpus.extend(cpus)
|
||||
time_offset += max(times)
|
||||
|
||||
ax2.plot(combined_times, combined_mems, color="red", label="Memory Usage", alpha=0.6)
|
||||
ax2.plot(combined_times, combined_cpus, color="blue", label="CPU Usage", alpha=0.6)
|
||||
|
||||
warmup_sec = 60
|
||||
if len(combined_times) > 1 and combined_times[-1] > warmup_sec:
|
||||
mask = np.array(combined_times) > warmup_sec
|
||||
x_reg = np.array(combined_times)[mask]
|
||||
|
||||
y_mem_reg = np.array(combined_mems)[mask]
|
||||
slope_mem, intercept_mem = np.polyfit(x_reg, y_mem_reg, 1)
|
||||
trend_mem = slope_mem * x_reg + intercept_mem
|
||||
r2_mem = calculate_r_squared(y_mem_reg, trend_mem)
|
||||
ax2.plot(x_reg, trend_mem, color="darkred", linestyle="--", linewidth=2.5,
|
||||
label=f"Mem Trend (Slope: {slope_mem:.4f} %/s, R²: {r2_mem:.2f})")
|
||||
|
||||
y_cpu_reg = np.array(combined_cpus)[mask]
|
||||
slope_cpu, intercept_cpu = np.polyfit(x_reg, y_cpu_reg, 1)
|
||||
trend_cpu = slope_cpu * x_reg + intercept_cpu
|
||||
r2_cpu = calculate_r_squared(y_cpu_reg, trend_cpu)
|
||||
ax2.plot(x_reg, trend_cpu, color="navy", linestyle="--", linewidth=2.5,
|
||||
label=f"CPU Trend (Slope: {slope_cpu:.4f} %/s, R²: {r2_cpu:.2f})")
|
||||
|
||||
ax2.set_xlabel("Time (s)")
|
||||
ax2.set_ylabel("Usage (%)")
|
||||
ax2.set_title("Memory and CPU Usage Over Time")
|
||||
ax2.legend(loc='lower left', fontsize='small', framealpha=0.9)
|
||||
ax2.grid(True, linestyle="--", alpha=0.5)
|
||||
|
||||
buffer1 = io.BytesIO()
|
||||
fig1.savefig(buffer1, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer1.seek(0)
|
||||
img1 = base64.b64encode(buffer1.getvalue()).decode()
|
||||
|
||||
buffer2 = io.BytesIO()
|
||||
fig2.savefig(buffer2, format='webp', bbox_inches='tight', pad_inches=1.0)
|
||||
buffer2.seek(0)
|
||||
img2 = base64.b64encode(buffer2.getvalue()).decode()
|
||||
|
||||
filename = f"memory_usage_{route_name}.html"
|
||||
save_path = os.path.join(os.path.dirname(__file__), "plots", filename)
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
|
||||
html_template = (
|
||||
"<style>body{font-family:Arial,sans-serif;margin:20px}" +
|
||||
"h1,h2,h3{text-align:center;margin:5px 0}h2{margin-bottom:10px}" +
|
||||
"img{width:100%;max-width:800px;height:auto;display:block;margin:0 auto}</style>" +
|
||||
f"<h1>Memory Profile Report</h1><h3>Route: {route_name.replace('_', '/')}</h3>" +
|
||||
f"<img src='data:image/webp;base64,{img1}'>" +
|
||||
f"<img src='data:image/webp;base64,{img2}'>"
|
||||
)
|
||||
|
||||
plt.close(fig1)
|
||||
plt.close(fig2)
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
f.write(html_template)
|
||||
|
||||
print(f"Report saved to {save_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Extract memory usage from route logs.')
|
||||
parser.add_argument('route_or_segment_name', help='Route or segment name from comma connect')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
print(f"Fetching logs for {args.route_or_segment_name}")
|
||||
lr = LogReader(args.route_or_segment_name, default_mode=ReadMode.QLOG)
|
||||
segment_data = lr.run_across_segments(24, process_segment)
|
||||
segments = list(range(len(segment_data)))
|
||||
route_name = args.route_or_segment_name.replace('/', '_')
|
||||
plot_results(segments, segment_data, route_name)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import requests
|
||||
from openpilot.tools.lib.route import Route
|
||||
|
||||
|
||||
def get_segments(source, route_id, camera, seg_range):
|
||||
if "@" in source or "comma-" in source or "sunny-" in source: # SSH
|
||||
if not route_id:
|
||||
raise ValueError("route_id required for SSH")
|
||||
cmd = ["ssh", source, f"ls -d /data/media/0/realdata/{route_id.split('--')[0]}--*"]
|
||||
output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode("utf-8").strip()
|
||||
return [{
|
||||
"type": "ssh",
|
||||
"host": source,
|
||||
"src": os.path.join(path, camera),
|
||||
"num": int(path.split("--")[-1])
|
||||
} for path in sorted(output.split("\n"), key=lambda x: int(x.split("--")[-1])) if path]
|
||||
else: # URL
|
||||
route = Route(route_id)
|
||||
cameras = [camera]
|
||||
if camera == "fcamera.hevc":
|
||||
cameras.extend([c for c in ["ecamera.hevc", "qcamera.ts"] if c != camera])
|
||||
|
||||
for cam in cameras:
|
||||
attr_name = "camera_paths" if cam == "fcamera.hevc" else f"{cam.split('.')[0]}_paths"
|
||||
paths = getattr(route, attr_name)()
|
||||
if any(paths):
|
||||
return [{"type": "url", "src": url, "num": idx, "cam": cam} for idx, url in enumerate(paths) if url]
|
||||
|
||||
raise ValueError(f"No footage found for {route_id}")
|
||||
|
||||
|
||||
def download(job, out_dir):
|
||||
destination = os.path.join(out_dir, f"{job['num']}_{os.path.basename(job.get('cam', job.get('src')))}")
|
||||
if os.path.exists(destination) and os.path.getsize(destination) > 0:
|
||||
return destination
|
||||
|
||||
print(f"Downloading segment {job['num']}")
|
||||
if job["type"] == "ssh":
|
||||
subprocess.check_call(["scp", f"{job['host']}:{job['src']}", destination])
|
||||
else:
|
||||
with requests.get(job["src"], stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(destination, 'wb') as f:
|
||||
shutil.copyfileobj(r.raw, f)
|
||||
return destination
|
||||
|
||||
|
||||
def mux(files, output_file, codec):
|
||||
list_filename = f"{output_file}.list.txt"
|
||||
with open(list_filename, 'w') as f:
|
||||
f.write('\n'.join([f"file '{os.path.abspath(name)}'" for name in files]))
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"ffmpeg", "-y", "-probesize", "100M", "-analyzeduration", "100M", "-f", "concat",
|
||||
"-safe", "0", "-r", "20", "-i", list_filename, "-c", "copy", "-tag:v", codec, output_file
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
print(f"Saved: {output_file} ({os.path.getsize(output_file) / 1048576:.2f} MB)")
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["open", "-R", output_file])
|
||||
finally:
|
||||
if os.path.exists(list_filename):
|
||||
os.remove(list_filename)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("source")
|
||||
parser.add_argument("route_id", nargs='?')
|
||||
parser.add_argument("--output", "-o", default="output.mp4")
|
||||
parser.add_argument("--camera", "-c", default="fcamera.hevc")
|
||||
parser.add_argument("--keep-segments", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
route_id_str = args.route_id or args.source
|
||||
segment_range = None
|
||||
if "/" in route_id_str:
|
||||
route_id_str, range_str = route_id_str.rsplit("/", 1)
|
||||
if ":" in range_str or range_str.isdigit():
|
||||
segment_range = range_str
|
||||
|
||||
is_ssh = "@" in args.source or "comma-" in args.source or "sunny-" in args.source
|
||||
if not is_ssh and len(route_id_str.split("--")) > 2:
|
||||
route_id_str = "--".join(route_id_str.split("--")[:2])
|
||||
|
||||
segments = get_segments(args.source, route_id_str, args.camera, segment_range)
|
||||
if segment_range:
|
||||
if ":" in segment_range:
|
||||
parts = segment_range.split(":")
|
||||
start_idx = int(parts[0]) if parts[0] else None
|
||||
end_idx = int(parts[1]) if parts[1] else None
|
||||
else:
|
||||
start_idx = int(segment_range)
|
||||
end_idx = start_idx + 1
|
||||
|
||||
segments = [
|
||||
segment for segment in segments
|
||||
if (start_idx is None or segment['num'] >= start_idx) and (end_idx is None or segment['num'] < end_idx)
|
||||
]
|
||||
|
||||
download_dir = f"{route_id_str}_segments"
|
||||
os.makedirs(download_dir, exist_ok=True)
|
||||
|
||||
downloaded_files = sorted(
|
||||
[download(segment, download_dir) for segment in segments],
|
||||
key=lambda x: int(os.path.basename(x).split("_")[0])
|
||||
)
|
||||
|
||||
camera_name = segments[0].get('cam', args.camera)
|
||||
codec = "hvc1" if camera_name.endswith("hevc") else "avc1"
|
||||
mux(downloaded_files, f"{route_id_str}--{args.output}", codec)
|
||||
|
||||
if not args.keep_segments:
|
||||
shutil.rmtree(download_dir)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -91,9 +91,6 @@ class LPABase(ABC):
|
||||
def switch_profile(self, iccid: str) -> None:
|
||||
pass
|
||||
|
||||
def process_notifications(self) -> None:
|
||||
pass
|
||||
|
||||
def is_comma_profile(self, iccid: str) -> bool:
|
||||
return any(iccid.startswith(prefix) for prefix in ('8985235',))
|
||||
|
||||
|
||||
@@ -338,9 +338,6 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
show_alert = (not onroad_conditions["device_temp_good"] or not startup_conditions["device_temp_engageable"]) and onroad_conditions["ignition"]
|
||||
set_offroad_alert_if_changed("Offroad_TemperatureTooHigh", show_alert, extra_text=extra_text)
|
||||
|
||||
if show_alert:
|
||||
msg.deviceState.fanSpeedPercentDesired = 100
|
||||
|
||||
# Handle offroad/onroad transition
|
||||
should_start = all(onroad_conditions.values())
|
||||
if started_ts is None:
|
||||
@@ -438,10 +435,9 @@ def hardware_thread(end_event, hw_queue) -> None:
|
||||
statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired)
|
||||
statlog.gauge("screen_brightness_percent", msg.deviceState.screenBrightnessPercent)
|
||||
|
||||
# report to server once every 10 minutes, or every 1s when thermally blocked
|
||||
# report to server once every 10 minutes
|
||||
rising_edge_started = should_start and not should_start_prev
|
||||
status_packet_interval = 1. if show_alert else 600.
|
||||
if rising_edge_started or (count % int(status_packet_interval / DT_HW)) == 0:
|
||||
if rising_edge_started or (count % int(600. / DT_HW)) == 0:
|
||||
dat = {
|
||||
'count': count,
|
||||
'pandaStates': [strip_deprecated_keys(p.to_dict()) for p in pandaStates],
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# GSMA Certificate Issuer (CI) bundle for eSIM RSP
|
||||
# Source: https://euicc-manual.osmocom.org/docs/pki/ci/bundle.pem
|
||||
|
||||
issuer=
|
||||
countryName = CH
|
||||
organizationName = OISTE Foundation
|
||||
commonName = OISTE GSMA CI G1
|
||||
notBefore=2024-01-16 23:17:39Z
|
||||
notAfter=2059-01-07 23:17:38Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB9zCCAZ2gAwIBAgIUSpBSCCDYPOEG/IFHUCKpZ2pIAQMwCgYIKoZIzj0EAwIw
|
||||
QzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xGTAXBgNV
|
||||
BAMMEE9JU1RFIEdTTUEgQ0kgRzEwIBcNMjQwMTE2MjMxNzM5WhgPMjA1OTAxMDcy
|
||||
MzE3MzhaMEMxCzAJBgNVBAYTAkNIMRkwFwYDVQQKDBBPSVNURSBGb3VuZGF0aW9u
|
||||
MRkwFwYDVQQDDBBPSVNURSBHU01BIENJIEcxMFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||
AQcDQgAEvZ3s3PFC4NgrCcCMmHJ6DJ66uzAHuLcvjJnOn+TtBNThS7YHLDyHCa2v
|
||||
7D+zTP+XTtgqgcLoB56Gha9EQQQ4xKNtMGswDwYDVR0TAQH/BAUwAwEB/zAQBgNV
|
||||
HREECTAHiAVghXQFDjAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwHQYDVR0OBBYE
|
||||
FEwnlnrSDBSzkelgHkHmBK1XwCIvMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
|
||||
AgNIADBFAiBVcywTj017jKpAQ+gwy4MqK2hQvzve6lkvQkgSP6ykHwIhAI0KFwCD
|
||||
jnPbmcJsG41hUrWNlf+IcrMvFuYii0DasBNi
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
organizationName = GSM Association
|
||||
commonName = GSM Association - RSP2 Root CI1
|
||||
notBefore=2017-02-22 00:00:00Z
|
||||
notAfter=2052-02-21 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICSTCCAe+gAwIBAgIQbmhWeneg7nyF7hg5Y9+qejAKBggqhkjOPQQDAjBEMRgw
|
||||
FgYDVQQKEw9HU00gQXNzb2NpYXRpb24xKDAmBgNVBAMTH0dTTSBBc3NvY2lhdGlv
|
||||
biAtIFJTUDIgUm9vdCBDSTEwIBcNMTcwMjIyMDAwMDAwWhgPMjA1MjAyMjEyMzU5
|
||||
NTlaMEQxGDAWBgNVBAoTD0dTTSBBc3NvY2lhdGlvbjEoMCYGA1UEAxMfR1NNIEFz
|
||||
c29jaWF0aW9uIC0gUlNQMiBSb290IENJMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABJ1qutL0HCMX52GJ6/jeibsAqZfULWj/X10p/Min6seZN+hf5llovbCNuB2n
|
||||
unLz+O8UD0SUCBUVo8e6n9X1TuajgcAwgb0wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wEwYDVR0RBAwwCogIKwYBBAGC6WAwFwYDVR0gAQH/BA0wCzAJ
|
||||
BgdngRIBAgEAME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9nc21hLWNybC5zeW1h
|
||||
dXRoLmNvbS9vZmZsaW5lY2EvZ3NtYS1yc3AyLXJvb3QtY2kxLmNybDAdBgNVHQ4E
|
||||
FgQUgTcPUSXQsdQI1MOyMubSXnlb6/swCgYIKoZIzj0EAwIDSAAwRQIgIJdYsOMF
|
||||
WziPK7l8nh5mu0qiRiVf25oa9ullG/OIASwCIQDqCmDrYf+GziHXBOiwJwnBaeBO
|
||||
aFsiLzIEOaUuZwdNUw==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = US
|
||||
organizationName = Entrust, Inc.
|
||||
organizationalUnitName = See www.entrust.net/legal-terms
|
||||
organizationalUnitName = (c) 2016 Entrust, Inc. - for authorized use only
|
||||
commonName = Entrust eSIM Certification Authority
|
||||
notBefore=2016-11-16 16:04:02Z
|
||||
notAfter=2051-10-16 16:34:02Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC6DCCAo2gAwIBAgIRAIy4GT7M5nHsAAAAAFgsinowCgYIKoZIzj0EAwIwgbkx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9T
|
||||
ZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAx
|
||||
NiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNV
|
||||
BAMTJEVudHJ1c3QgZVNJTSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0xNjEx
|
||||
MTYxNjA0MDJaGA8yMDUxMTAxNjE2MzQwMlowgbkxCzAJBgNVBAYTAlVTMRYwFAYD
|
||||
VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
|
||||
L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNiBFbnRydXN0LCBJbmMuIC0g
|
||||
Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNVBAMTJEVudHJ1c3QgZVNJTSBD
|
||||
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
||||
BAdzwGHeQ1Wb2f4DmHTByR5/IWL3JugQ1U3908a++bHdlt+TTA7K4c5cYZ+51Yz/
|
||||
hg/bacxguPDh9uQUK6Wg3a6jcjBwMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
|
||||
BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAVBgNVHREEDjAMiApghkgB
|
||||
hvpsFAoAMB0GA1UdDgQWBBQWcEt/NR42B/GMS3AAXDoAPf1BSjAKBggqhkjOPQQD
|
||||
AgNJADBGAiEAspjXMvaBZyAg86Z0AAtT0yBRAi1EyaAfNz9kDJeAE04CIQC3efj8
|
||||
ATL7/tDBOhANy3cK8PS/1NIlu9vqMLCZsZvJ0Q==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = FR
|
||||
organizationName = OBERTHUR TECHNOLOGIES
|
||||
organizationalUnitName = TELECOM
|
||||
commonName = MC4 OT ROOT CI v1
|
||||
notBefore=2016-11-15 00:00:01Z
|
||||
notAfter=2046-11-08 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICOjCCAeGgAwIBAgIBATAKBggqhkjOPQQDAjBbMQswCQYDVQQGEwJGUjEeMBwG
|
||||
A1UEChMVT0JFUlRIVVIgVEVDSE5PTE9HSUVTMRAwDgYDVQQLEwdURUxFQ09NMRow
|
||||
GAYDVQQDExFNQzQgT1QgUk9PVCBDSSB2MTAeFw0xNjExMTUwMDAwMDFaFw00NjEx
|
||||
MDgyMzU5NTlaMFsxCzAJBgNVBAYTAkZSMR4wHAYDVQQKExVPQkVSVEhVUiBURUNI
|
||||
Tk9MT0dJRVMxEDAOBgNVBAsTB1RFTEVDT00xGjAYBgNVBAMTEU1DNCBPVCBST09U
|
||||
IENJIHYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHb/Gajt3OZxuaDSklBQE
|
||||
D4lOd6PGPLSvtfkM952ubdyy45tJwAeA0eEii0CLrFT6tcfXkW+H/5mQyMRXaAUk
|
||||
T6OBlTCBkjAfBgNVHSMEGDAWgBTNbmC3LXoGPLyEYluR6A/jBAbhPjAdBgNVHQ4E
|
||||
FgQUzW5gty16Bjy8hGJbkegP4wQG4T4wDgYDVR0PAQH/BAQDAgAGMBcGA1UdIAEB
|
||||
/wQNMAswCQYHZ4ESAQIBADAWBgNVHREEDzANiAsrBgEEAYHvb7OITTAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIEw4Nc7f2fDtoH+6ON/bknfDQxmT
|
||||
ikThXjhpLtSrSKN2AiAxHxgC87L0FDnH8dJNlkdGX9c0JIx6oLheIplfS6k+jg==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
commonName = SubMan V4.2 CI Google Pixel
|
||||
organizationName = Giesecke and Devrient GmbH
|
||||
organizationalUnitName = Mobile Security
|
||||
countryName = DE
|
||||
notBefore=2017-05-10 00:00:00Z
|
||||
notAfter=2027-05-10 00:00:00Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICaTCCAg6gAwIBAgICASwwCgYIKoZIzj0EAwIwczElMCMGA1UEAxMcIFN1Yk1h
|
||||
biBWNC4yIENJIEdvb2dsZSBQaXhlbDEjMCEGA1UEChMaR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50IEdtYkgxGDAWBgNVBAsTD01vYmlsZSBTZWN1cml0eTELMAkGA1UEBhMC
|
||||
REUwHhcNMTcwNTEwMDAwMDAwWhcNMjcwNTEwMDAwMDAwWjBzMSUwIwYDVQQDExwg
|
||||
U3ViTWFuIFY0LjIgQ0kgR29vZ2xlIFBpeGVsMSMwIQYDVQQKExpHaWVzZWNrZSBh
|
||||
bmQgRGV2cmllbnQgR21iSDEYMBYGA1UECxMPTW9iaWxlIFNlY3VyaXR5MQswCQYD
|
||||
VQQGEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHNorfaJsGzqWNawyAhl
|
||||
IAv9QL2/+b9RsUoso06t/dKX1MRr5CUJ51acvv5TAFhQKIml+dwLbFnV5aO+8W6Z
|
||||
wxajgZEwgY4wHwYDVR0jBBgwFoAUtg8LiX/WMLiM/tYWH46oCMU4KsMwHQYDVR0O
|
||||
BBYEFLYPC4l/1jC4jP7WFh+OqAjFOCrDMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAB
|
||||
Af8EDTALMAkGB2eBEgECAQAwDwYDVR0TAQH/BAUwAwEB/zASBgNVHREECzAJiAcr
|
||||
BgEEAdwPMAoGCCqGSM49BAMCA0kAMEYCIQDpoZcuAQrjATW8U+AWqMUJ0dY6nWW1
|
||||
R1QmFzVZ1yMXSwIhALCvRqkCtgiavdeFeSgsSNbY5Fhd+QoCltuSh1U4TE7A
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = DE
|
||||
commonName = SubMan V4.2 CI
|
||||
organizationName = Giesecke and Devrient
|
||||
organizationalUnitName = Mobile Security
|
||||
notBefore=2016-08-12 13:51:48Z
|
||||
notAfter=2026-08-12 13:51:48Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICUjCCAfigAwIBAgIDQgAAMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAkRFMRcw
|
||||
FQYDVQQDEw5TdWJNYW4gVjQuMiBDSTEeMBwGA1UEChMVR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50MRgwFgYDVQQLEw9Nb2JpbGUgU2VjdXJpdHkwHhcNMTYwODEyMTM1MTQ4
|
||||
WhcNMjYwODEyMTM1MTQ4WjBgMQswCQYDVQQGEwJERTEXMBUGA1UEAxMOU3ViTWFu
|
||||
IFY0LjIgQ0kxHjAcBgNVBAoTFUdpZXNlY2tlIGFuZCBEZXZyaWVudDEYMBYGA1UE
|
||||
CxMPTW9iaWxlIFNlY3VyaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYIgl
|
||||
VQr9wbXOlwPp8qMg5Df08Cli9Mc+lpr3Lwa9PlVA3QWlLeX4GfD4H3phLBqVIa17
|
||||
yHttmtheTxi0KoEqhKOBoDCBnTAdBgNVHQ4EFgQU6lOt7zMpuVCa/XVf1Ei4LcG8
|
||||
7P8wDgYDVR0PAQH/BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAPBgNV
|
||||
HRMBAf8EBTADAQH/MBIGA1UdEQQLMAmIBysGAQQB3A8wLgYDVR0fBCcwJTAjoCGg
|
||||
H4YdaHR0cDovL2dpLWRlLmNvbS90ZXN0LmNybC5wZW0wCgYIKoZIzj0EAwIDSAAw
|
||||
RQIhAMMx2L/VHDiOW+Fl/OuFmhCdizYM17Yn9zAVieKO2T0iAiANWtCMmY+DzkqK
|
||||
yHxBFX0U2tBd682zP4DpgRt8j3Ylew==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -3,10 +3,8 @@
|
||||
import atexit
|
||||
import base64
|
||||
import fcntl
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import requests
|
||||
import serial
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -17,12 +15,8 @@ from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.hardware.base import LPABase, LPAError, Profile
|
||||
|
||||
GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem")
|
||||
|
||||
DEFAULT_DEVICE = "/dev/modem_at0"
|
||||
DEFAULT_BAUD = 9600
|
||||
@@ -32,7 +26,6 @@ ISDR_AID = "A0000005591010FFFFFFFF8900000100"
|
||||
MM = "org.freedesktop.ModemManager1"
|
||||
MM_MODEM = MM + ".Modem"
|
||||
ES10X_MSS = 120
|
||||
HTTP_TIMEOUT = 30
|
||||
OPEN_ISDR_RETRIES = 10
|
||||
OPEN_ISDR_RETRY_DELAY_S = 0.25
|
||||
OPEN_ISDR_RESET_ATTEMPT = 5
|
||||
@@ -44,24 +37,10 @@ DEBUG = os.environ.get("DEBUG") == "1"
|
||||
# TLV Tags
|
||||
TAG_ICCID = 0x5A
|
||||
TAG_STATUS = 0x80
|
||||
TAG_EUICC_INFO = 0xBF20
|
||||
TAG_PREPARE_DOWNLOAD = 0xBF21
|
||||
TAG_BPP_COMMAND = 0xBF23
|
||||
TAG_PROFILE_METADATA = 0xBF25
|
||||
TAG_INSTALL_RESULT_DATA = 0xBF27
|
||||
TAG_LIST_NOTIFICATION = 0xBF28
|
||||
TAG_SET_NICKNAME = 0xBF29
|
||||
TAG_RETRIEVE_NOTIFICATION = 0xBF2B
|
||||
TAG_PROFILE_INFO_LIST = 0xBF2D
|
||||
TAG_EUICC_CHALLENGE = 0xBF2E
|
||||
TAG_NOTIFICATION_METADATA = 0xBF2F
|
||||
TAG_NOTIFICATION_SENT = 0xBF30
|
||||
TAG_SET_NICKNAME = 0xBF29
|
||||
TAG_ENABLE_PROFILE = 0xBF31
|
||||
TAG_DELETE_PROFILE = 0xBF33
|
||||
TAG_BPP = 0xBF36
|
||||
TAG_PROFILE_INSTALL_RESULT = 0xBF37
|
||||
TAG_AUTH_SERVER = 0xBF38
|
||||
TAG_CANCEL_SESSION = 0xBF41
|
||||
TAG_OK = 0xA0
|
||||
|
||||
PROFILE_OK = 0x00
|
||||
@@ -73,42 +52,6 @@ PROFILE_ERROR_CODES = {
|
||||
0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling",
|
||||
PROFILE_CAT_BUSY: "catBusy", 0x06: "undefinedError",
|
||||
}
|
||||
AUTH_SERVER_ERROR_CODES = {
|
||||
0x01: "eUICCVerificationFailed", 0x02: "eUICCCertificateExpired",
|
||||
0x03: "eUICCCertificateRevoked", 0x05: "invalidServerSignature",
|
||||
0x06: "euiccCiPKUnknown", 0x0A: "matchingIdRefused",
|
||||
0x10: "insufficientMemory",
|
||||
}
|
||||
BPP_COMMAND_NAMES = {
|
||||
0: "initialiseSecureChannel", 1: "configureISDP", 2: "storeMetadata",
|
||||
3: "storeMetadata2", 4: "replaceSessionKeys", 5: "loadProfileElements",
|
||||
}
|
||||
BPP_ERROR_REASONS = {
|
||||
1: "incorrectInputValues", 2: "invalidSignature", 3: "invalidTransactionId",
|
||||
4: "unsupportedCrtValues", 5: "unsupportedRemoteOperationType",
|
||||
6: "unsupportedProfileClass", 7: "scp03tStructureError", 8: "scp03tSecurityError",
|
||||
9: "iccidAlreadyExistsOnEuicc", 10: "insufficientMemoryForProfile",
|
||||
11: "installInterrupted", 12: "peProcessingError", 13: "dataMismatch",
|
||||
14: "invalidNAA",
|
||||
}
|
||||
BPP_ERROR_MESSAGES = {
|
||||
9: "This eSIM profile is already installed on this device.",
|
||||
10: "Not enough memory on the eUICC to install this profile.",
|
||||
12: "Profile installation failed. The QR code may have already been used.",
|
||||
}
|
||||
|
||||
# SGP.22 §5.2.6 — SM-DP+ reason/subject codes mapped to user-friendly messages
|
||||
ES9P_ERROR_MESSAGES: dict[tuple[str, str], str] = {
|
||||
('3.8', '8.2.6'): "This eSIM profile is already installed on another device. Please use a new QR code.",
|
||||
('3.8', '8.2.1'): "This eSIM profile has expired. Please request a new QR code.",
|
||||
('3.8', '8.1'): "The SM-DP+ server refused this request.",
|
||||
('3.1', '8.2.6'): "This eSIM profile has been revoked by the carrier.",
|
||||
('3.9', '8.2.6'): "This eSIM profile download has already been completed.",
|
||||
('2.1', '8.8'): "The device is not compatible with this eSIM profile.",
|
||||
('1.2', '8.1'): "The SM-DP+ server is temporarily unavailable. Try again later.",
|
||||
}
|
||||
|
||||
NOTIFICATION_OPERATIONS = {0x80: "install", 0x40: "enable", 0x20: "disable", 0x10: "delete"}
|
||||
|
||||
STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"}
|
||||
ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"}
|
||||
@@ -402,15 +345,6 @@ def es10x_command(client: AtClient, data: bytes) -> bytes:
|
||||
|
||||
# --- Profile operations ---
|
||||
|
||||
NOTIFICATION: FieldMap = {
|
||||
TAG_STATUS: ("seqNumber", lambda v: int.from_bytes(v, "big")),
|
||||
0x81: ("profileManagementOperation",
|
||||
lambda v: NOTIFICATION_OPERATIONS.get(next((m for m in NOTIFICATION_OPERATIONS if len(v) >= 2 and v[1] & m), 0), "unknown")),
|
||||
0x0C: ("notificationAddress", lambda v: v.decode("utf-8", errors="ignore")),
|
||||
TAG_ICCID: ("iccid", tbcd_to_string),
|
||||
}
|
||||
|
||||
|
||||
def decode_profiles(blob: bytes) -> list[dict]:
|
||||
root = require_tag(blob, TAG_PROFILE_INFO_LIST, "ProfileInfoList")
|
||||
list_ok = find_tag(root, TAG_OK)
|
||||
@@ -436,278 +370,6 @@ def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None:
|
||||
raise RuntimeError(f"SetNickname failed with status 0x{code:02X}")
|
||||
|
||||
|
||||
# --- ES9P HTTP ---
|
||||
|
||||
def es9p_request(smdp_address: str, endpoint: str, payload: dict, error_prefix: str = "Request", session: requests.Session | None = None) -> dict:
|
||||
url = f"https://{smdp_address}/gsma/rsp2/es9plus/{endpoint}"
|
||||
headers = {"User-Agent": "gsma-rsp-lpad", "X-Admin-Protocol": "gsma/rsp/v2.3.0", "Content-Type": "application/json"}
|
||||
http = session or requests
|
||||
resp = http.post(url, json=payload, headers=headers, timeout=HTTP_TIMEOUT, verify=GSMA_CI_BUNDLE)
|
||||
resp.raise_for_status()
|
||||
if not resp.content:
|
||||
return {}
|
||||
data = resp.json()
|
||||
if "header" in data and "functionExecutionStatus" in data["header"]:
|
||||
status = data["header"]["functionExecutionStatus"]
|
||||
if status.get("status") == "Failed":
|
||||
sd = status.get("statusCodeData", {})
|
||||
reason = sd.get("reasonCode", "unknown")
|
||||
subject = sd.get("subjectCode", "unknown")
|
||||
msg = ES9P_ERROR_MESSAGES.get((reason, subject),
|
||||
f"{error_prefix} failed: {reason}/{subject} - {sd.get('message', 'unknown')}")
|
||||
raise RuntimeError(msg)
|
||||
return data
|
||||
|
||||
|
||||
# --- Notifications ---
|
||||
|
||||
def list_notifications(client: AtClient) -> list[dict]:
|
||||
response = es10x_command(client, encode_tlv(TAG_LIST_NOTIFICATION, b""))
|
||||
root = require_tag(response, TAG_LIST_NOTIFICATION, "ListNotificationResponse")
|
||||
metadata_list = find_tag(root, TAG_OK)
|
||||
if metadata_list is None:
|
||||
return []
|
||||
return [decode_struct(value, NOTIFICATION) for tag, value in iter_tlv(metadata_list) if tag == TAG_NOTIFICATION_METADATA]
|
||||
|
||||
|
||||
def process_notifications(client: AtClient) -> None:
|
||||
for notification in list_notifications(client):
|
||||
seq_number, smdp_address = notification["seqNumber"], notification["notificationAddress"]
|
||||
try:
|
||||
request = encode_tlv(TAG_RETRIEVE_NOTIFICATION, encode_tlv(TAG_OK, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
|
||||
response = es10x_command(client, request)
|
||||
content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"),
|
||||
TAG_OK, "RetrieveNotificationsListResponse")
|
||||
pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
|
||||
if pending_notif is None:
|
||||
raise RuntimeError("Missing PendingNotification")
|
||||
|
||||
es9p_request(smdp_address, "handleNotification", {"pendingNotification": b64e(pending_notif)}, "HandleNotification")
|
||||
|
||||
response = es10x_command(client, encode_tlv(TAG_NOTIFICATION_SENT, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
|
||||
root = require_tag(response, TAG_NOTIFICATION_SENT, "NotificationSentResponse")
|
||||
if int.from_bytes(require_tag(root, TAG_STATUS, "RemoveNotificationFromList status"), "big") != 0:
|
||||
raise RuntimeError("RemoveNotificationFromList failed")
|
||||
except Exception as e:
|
||||
print(f"notification {seq_number} failed: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
# --- Authentication & Download ---
|
||||
|
||||
def get_challenge_and_info(client: AtClient) -> tuple[bytes, bytes]:
|
||||
challenge_resp = es10x_command(client, encode_tlv(TAG_EUICC_CHALLENGE, b""))
|
||||
challenge = require_tag(require_tag(challenge_resp, TAG_EUICC_CHALLENGE, "GetEuiccDataResponse"),
|
||||
TAG_STATUS, "challenge in response")
|
||||
info_resp = es10x_command(client, encode_tlv(TAG_EUICC_INFO, b""))
|
||||
require_tag(info_resp, TAG_EUICC_INFO, "GetEuiccInfo1Response")
|
||||
return challenge, info_resp
|
||||
|
||||
|
||||
def authenticate_server(client: AtClient, b64_signed1: str, b64_sig1: str, b64_pk_id: str, b64_cert: str, matching_id: str) -> str:
|
||||
tac = bytes([0x35, 0x29, 0x06, 0x11])
|
||||
device_info = encode_tlv(TAG_STATUS, tac) + encode_tlv(0xA1, b"")
|
||||
ctx_inner = encode_tlv(TAG_STATUS, matching_id.encode("utf-8")) + encode_tlv(0xA1, device_info)
|
||||
content = b64d(b64_signed1) + b64d(b64_sig1) + b64d(b64_pk_id) + b64d(b64_cert) + encode_tlv(0xA0, ctx_inner)
|
||||
response = es10x_command(client, encode_tlv(TAG_AUTH_SERVER, content))
|
||||
root = require_tag(response, TAG_AUTH_SERVER, "AuthenticateServerResponse")
|
||||
error_tag = find_tag(root, 0xA1)
|
||||
if error_tag is not None:
|
||||
code = int.from_bytes(error_tag, "big") if error_tag else 0
|
||||
raise RuntimeError(f"AuthenticateServer rejected by eUICC: {AUTH_SERVER_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def prepare_download(client: AtClient, b64_signed2: str, b64_sig2: str, b64_cert: str, cc: str | None = None) -> str:
|
||||
smdp_signed2 = b64d(b64_signed2)
|
||||
smdp_signature2 = b64d(b64_sig2)
|
||||
smdp_certificate = b64d(b64_cert)
|
||||
smdp_signed2_root = find_tag(smdp_signed2, 0x30)
|
||||
if smdp_signed2_root is None:
|
||||
raise RuntimeError("Invalid smdpSigned2")
|
||||
transaction_id = find_tag(smdp_signed2_root, TAG_STATUS)
|
||||
cc_required_flag = find_tag(smdp_signed2_root, 0x01)
|
||||
if transaction_id is None or cc_required_flag is None:
|
||||
raise RuntimeError("Invalid smdpSigned2")
|
||||
content = smdp_signed2 + smdp_signature2
|
||||
if int.from_bytes(cc_required_flag, "big") != 0:
|
||||
if not cc:
|
||||
raise RuntimeError("Confirmation code required but not provided")
|
||||
content += encode_tlv(0x04, hashlib.sha256(hashlib.sha256(cc.encode("utf-8")).digest() + transaction_id).digest())
|
||||
content += smdp_certificate
|
||||
response = es10x_command(client, encode_tlv(TAG_PREPARE_DOWNLOAD, content))
|
||||
require_tag(response, TAG_PREPARE_DOWNLOAD, "PrepareDownloadResponse")
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def _parse_tlv_header_len(data: bytes) -> int:
|
||||
tag_len = 2 if data[0] & 0x1F == 0x1F else 1
|
||||
length_byte = data[tag_len]
|
||||
return tag_len + (1 + (length_byte & 0x7F) if length_byte & 0x80 else 1)
|
||||
|
||||
|
||||
def _split_bpp(bpp: bytes) -> list[bytes]:
|
||||
"""Split a BoundProfilePackage into APDU chunks per SGP.22 §5.7.6."""
|
||||
root_value = None
|
||||
for tag, value, start, end in iter_tlv(bpp, with_positions=True):
|
||||
if tag == TAG_BPP:
|
||||
root_value = value
|
||||
val_start = start + _parse_tlv_header_len(bpp[start:end])
|
||||
break
|
||||
if root_value is None:
|
||||
raise RuntimeError("Invalid BoundProfilePackage")
|
||||
|
||||
chunks: list[bytes] = []
|
||||
for tag, value, start, end in iter_tlv(root_value, with_positions=True):
|
||||
if tag == TAG_BPP_COMMAND:
|
||||
chunks.append(bpp[0 : val_start + end])
|
||||
elif tag in (0xA0, 0xA2):
|
||||
chunks.append(bpp[val_start + start : val_start + end])
|
||||
elif tag in (0xA1, 0xA3):
|
||||
hdr_len = _parse_tlv_header_len(root_value[start:end])
|
||||
chunks.append(bpp[val_start + start : val_start + start + hdr_len])
|
||||
for _, _, cs, ce in iter_tlv(value, with_positions=True):
|
||||
chunks.append(value[cs:ce])
|
||||
return chunks
|
||||
|
||||
|
||||
def _parse_install_result(response: bytes) -> dict[str, Any] | None:
|
||||
"""Parse a ProfileInstallResult from an APDU response, or None if not present."""
|
||||
root = find_tag(response, TAG_PROFILE_INSTALL_RESULT)
|
||||
if not root:
|
||||
return None
|
||||
result_data = find_tag(root, TAG_INSTALL_RESULT_DATA)
|
||||
if not result_data:
|
||||
return None
|
||||
result: dict[str, Any] = {"seqNumber": 0, "success": False, "bppCommandId": None, "errorReason": None}
|
||||
notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA)
|
||||
if notif_meta:
|
||||
seq_num = find_tag(notif_meta, TAG_STATUS)
|
||||
if seq_num:
|
||||
result["seqNumber"] = int.from_bytes(seq_num, "big")
|
||||
final_result = find_tag(result_data, 0xA2)
|
||||
if final_result:
|
||||
for tag, value in iter_tlv(final_result):
|
||||
if tag == 0xA0:
|
||||
result["success"] = True
|
||||
elif tag == 0xA1:
|
||||
bpp_cmd = find_tag(value, TAG_STATUS)
|
||||
if bpp_cmd:
|
||||
result["bppCommandId"] = int.from_bytes(bpp_cmd, "big")
|
||||
err = find_tag(value, 0x81)
|
||||
if err:
|
||||
result["errorReason"] = int.from_bytes(err, "big")
|
||||
return result
|
||||
|
||||
|
||||
def load_bpp(client: AtClient, b64_bpp: str) -> dict:
|
||||
bpp = b64d(b64_bpp)
|
||||
result = None
|
||||
for chunk in _split_bpp(bpp):
|
||||
response = es10x_command(client, chunk)
|
||||
if response:
|
||||
result = _parse_install_result(response) or result
|
||||
|
||||
if result is None:
|
||||
raise RuntimeError("Profile installation failed: no result from eUICC")
|
||||
if not result["success"] and result["errorReason"] is not None:
|
||||
msg = BPP_ERROR_MESSAGES.get(result["errorReason"])
|
||||
if not msg:
|
||||
cmd_name = BPP_COMMAND_NAMES.get(result["bppCommandId"], f"unknown({result['bppCommandId']})")
|
||||
err_name = BPP_ERROR_REASONS.get(result["errorReason"], f"unknown({result['errorReason']})")
|
||||
msg = f"Profile installation failed at {cmd_name}: {err_name}"
|
||||
raise RuntimeError(msg)
|
||||
if not result["success"]:
|
||||
raise RuntimeError("Profile installation failed: no result from eUICC")
|
||||
return result
|
||||
|
||||
|
||||
def parse_metadata(b64_metadata: str) -> dict:
|
||||
root = find_tag(b64d(b64_metadata), TAG_PROFILE_METADATA)
|
||||
if root is None:
|
||||
raise RuntimeError("Invalid profileMetadata")
|
||||
return decode_struct(root, PROFILE)
|
||||
|
||||
|
||||
def cancel_session(client: AtClient, transaction_id: bytes, reason: int = 127) -> str:
|
||||
content = encode_tlv(0x80, transaction_id) + encode_tlv(0x81, bytes([reason]))
|
||||
response = es10x_command(client, encode_tlv(TAG_CANCEL_SESSION, content))
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
|
||||
"""Parse 'LPA:1$smdp.example.com$MATCHING-ID' into (smdp_address, matching_id)."""
|
||||
if not activation_code.startswith("LPA:"):
|
||||
raise ValueError("Invalid activation code format")
|
||||
parts = activation_code[4:].split("$")
|
||||
if len(parts) != 3:
|
||||
raise ValueError("Invalid activation code format")
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
def _b64_field(data: dict, key: str) -> str:
|
||||
return base64_trim(data[key])
|
||||
|
||||
|
||||
def _cancel_session_safe(client: AtClient, smdp: str, tx_id: str, session: requests.Session) -> None:
|
||||
b64_cancel = ""
|
||||
try:
|
||||
b64_cancel = cancel_session(client, b64d(tx_id))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
es9p_request(smdp, "cancelSession", {"transactionId": tx_id, "cancelSessionResponse": b64_cancel}, "CancelSession", session=session)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def download_profile(client: AtClient, activation_code: str) -> str:
|
||||
"""Download and install an eSIM profile. Returns the ICCID of the installed profile."""
|
||||
if not system_time_valid():
|
||||
raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
|
||||
smdp, matching_id = parse_lpa_activation_code(activation_code)
|
||||
challenge, euicc_info = get_challenge_and_info(client)
|
||||
session = requests.Session()
|
||||
tx_id = None
|
||||
|
||||
try:
|
||||
# step 1: initiate authentication
|
||||
auth = es9p_request(smdp, "initiateAuthentication", {
|
||||
"smdpAddress": smdp, "euiccChallenge": b64e(challenge),
|
||||
"euiccInfo1": b64e(euicc_info), "matchingId": matching_id,
|
||||
}, "Authentication", session=session)
|
||||
tx_id = _b64_field(auth, "transactionId")
|
||||
|
||||
# step 2: authenticate server
|
||||
b64_auth = authenticate_server(client,
|
||||
_b64_field(auth, "serverSigned1"), _b64_field(auth, "serverSignature1"),
|
||||
_b64_field(auth, "euiccCiPKIdToBeUsed"), _b64_field(auth, "serverCertificate"),
|
||||
matching_id)
|
||||
|
||||
# step 3: authenticate client + get metadata
|
||||
cli = es9p_request(smdp, "authenticateClient", {
|
||||
"transactionId": tx_id, "authenticateServerResponse": b64_auth,
|
||||
}, "Authentication", session=session)
|
||||
iccid = parse_metadata(_b64_field(cli, "profileMetadata"))["iccid"]
|
||||
|
||||
# step 4: prepare download
|
||||
b64_prep = prepare_download(client,
|
||||
_b64_field(cli, "smdpSigned2"), _b64_field(cli, "smdpSignature2"),
|
||||
_b64_field(cli, "smdpCertificate"))
|
||||
|
||||
# step 5: get and install bound profile package
|
||||
bpp = es9p_request(smdp, "getBoundProfilePackage", {
|
||||
"transactionId": tx_id, "prepareDownloadResponse": b64_prep,
|
||||
}, "GetBoundProfilePackage", session=session)
|
||||
load_bpp(client, _b64_field(bpp, "boundProfilePackage"))
|
||||
return iccid
|
||||
except Exception:
|
||||
if tx_id:
|
||||
_cancel_session_safe(client, smdp, tx_id, session)
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class TiciLPA(LPABase):
|
||||
def __init__(self):
|
||||
if hasattr(self, '_client'):
|
||||
@@ -747,12 +409,6 @@ class TiciLPA(LPABase):
|
||||
def get_active_profile(self) -> Profile | None:
|
||||
return None
|
||||
|
||||
def process_notifications(self) -> None:
|
||||
if not system_time_valid():
|
||||
raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
|
||||
with self._acquire_channel():
|
||||
process_notifications(self._client)
|
||||
|
||||
def delete_profile(self, iccid: str) -> None:
|
||||
if self.is_comma_profile(iccid):
|
||||
raise LPAError("refusing to delete a comma profile")
|
||||
@@ -764,10 +420,7 @@ class TiciLPA(LPABase):
|
||||
raise LPAError(f"DeleteProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
|
||||
|
||||
def download_profile(self, qr: str, nickname: str | None = None) -> None:
|
||||
with self._acquire_channel():
|
||||
iccid = download_profile(self._client, qr)
|
||||
if nickname and iccid:
|
||||
set_profile_nickname(self._client, iccid, nickname)
|
||||
return None
|
||||
|
||||
def nickname_profile(self, iccid: str, nickname: str) -> None:
|
||||
with self._acquire_channel():
|
||||
|
||||
@@ -60,19 +60,17 @@ class OptionControlSP(ItemAction):
|
||||
|
||||
def set_value(self, value: int):
|
||||
"""Set the control to a specific value"""
|
||||
if not (self.min_value <= value <= self.max_value):
|
||||
return
|
||||
if value == self.current_value:
|
||||
return
|
||||
self.current_value = value
|
||||
if self.value_map:
|
||||
self.params.put(self.param_key, self.value_map[value])
|
||||
elif self.use_float_scaling:
|
||||
self.params.put(self.param_key, value / 100.0)
|
||||
else:
|
||||
self.params.put(self.param_key, value)
|
||||
if self.on_value_changed:
|
||||
self.on_value_changed(value)
|
||||
if self.min_value <= value <= self.max_value:
|
||||
self.current_value = value
|
||||
if self.value_map:
|
||||
self.params.put(self.param_key, self.value_map[value])
|
||||
else:
|
||||
if self.use_float_scaling:
|
||||
self.params.put(self.param_key, value / 100.0)
|
||||
else:
|
||||
self.params.put(self.param_key, value)
|
||||
if self.on_value_changed:
|
||||
self.on_value_changed(value)
|
||||
|
||||
def get_displayed_value(self) -> str:
|
||||
"""Get the displayed value, handling value mapping if present"""
|
||||
@@ -159,10 +157,10 @@ class OptionControlSP(ItemAction):
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect):
|
||||
new_value = self.current_value - self.value_change_step
|
||||
new_value = max(self.min_value, new_value)
|
||||
self.set_value(new_value)
|
||||
self.current_value -= self.value_change_step
|
||||
self.current_value = max(self.min_value, self.current_value)
|
||||
elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect):
|
||||
new_value = self.current_value + self.value_change_step
|
||||
new_value = min(self.max_value, new_value)
|
||||
self.set_value(new_value)
|
||||
self.current_value += self.value_change_step
|
||||
self.current_value = min(self.max_value, self.current_value)
|
||||
|
||||
self.set_value(self.current_value)
|
||||
|
||||
108
system/ui/sunnypilot/widgets/screen_saver.py
Normal file
108
system/ui/sunnypilot/widgets/screen_saver.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
class ScreenSaverSP(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
self.screensaver_timeout = Params().get("ScreenSaverTimeout", return_default=True)
|
||||
self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1")
|
||||
|
||||
self.x = 0.0
|
||||
self.y = 100.0
|
||||
self.vx = 120.0 if self._is_mici else 300.0
|
||||
self.vy = 70.0 if self._is_mici else 200.0
|
||||
self._hue = 150
|
||||
self.color = rl.color_from_hsv(self._hue, 1, 1)
|
||||
|
||||
self.text = "sunnypilot"
|
||||
self.font_size = 50 if self._is_mici else 200
|
||||
self._start_time = None
|
||||
self._dismiss = False
|
||||
self.dismiss_callback = None
|
||||
|
||||
@property
|
||||
def was_dismissed(self) -> bool:
|
||||
return self._dismiss
|
||||
|
||||
def initialize(self, dismiss_callback: Callable):
|
||||
if self._start_time is None:
|
||||
self._start_time = time.monotonic()
|
||||
self.dismiss_callback = dismiss_callback
|
||||
self._dismiss = False
|
||||
|
||||
def deinit(self):
|
||||
self._dismiss = False
|
||||
self._start_time = None
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos):
|
||||
self._reset()
|
||||
return super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _reset(self):
|
||||
self._dismiss = True
|
||||
self._start_time = None
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.font = gui_app.font(FontWeight.AUDIOWIDE)
|
||||
text_size = measure_text_cached(self.font, self.text, self.font_size, 0)
|
||||
self.logo_width = text_size.x
|
||||
self.logo_height = text_size.y
|
||||
|
||||
if self._start_time and time.monotonic() - self._start_time > self.screensaver_timeout:
|
||||
self._reset()
|
||||
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
self.x += self.vx * dt
|
||||
self.y += self.vy * dt
|
||||
|
||||
hit_x = hit_y = False
|
||||
if self.x + self.logo_width > self.rect.width:
|
||||
self.vx *= -1
|
||||
self.x = self.rect.width - self.logo_width
|
||||
hit_x = True
|
||||
elif self.x < 0:
|
||||
self.vx *= -1
|
||||
self.x = 0
|
||||
hit_x = True
|
||||
|
||||
if self.y + self.logo_height > self.rect.height:
|
||||
self.vy *= -1
|
||||
self.y = self.rect.height - self.logo_height
|
||||
hit_y = True
|
||||
elif self.y < 0:
|
||||
self.vy *= -1
|
||||
self.y = 0
|
||||
hit_y = True
|
||||
|
||||
def hue_dist(a, b):
|
||||
d = abs(a - b)
|
||||
return min(d, 360 - d)
|
||||
|
||||
if hit_x or hit_y:
|
||||
while hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 90:
|
||||
pass
|
||||
self._hue = new_hue
|
||||
self.color = rl.color_from_hsv(self._hue, 1, 1)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if self._dismiss:
|
||||
self.dismiss_callback()
|
||||
return 0
|
||||
|
||||
self.set_rect(rect)
|
||||
rl.clear_background(rl.BLACK)
|
||||
rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color)
|
||||
return -1
|
||||
@@ -198,10 +198,7 @@ class TreeOptionDialog(MultiOptionDialog):
|
||||
|
||||
self.option_buttons = self.visible_items
|
||||
self.options = [item.text for item in self.visible_items]
|
||||
# Rebuild scroller items to ensure proper setup of touch callbacks
|
||||
self.scroller._items.clear()
|
||||
for item in self.option_buttons:
|
||||
self.scroller.add_widget(item)
|
||||
self.scroller._items = self.visible_items
|
||||
if reset_scroll:
|
||||
self.scroller.scroll_panel.set_offset(0)
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.updated.updated import Updater
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_target_branch_migration_from_current_branch(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
mocker.patch.object(Updater, "get_branch", return_value=branch)
|
||||
|
||||
assert Updater().target_branch == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_target_branch_migration_from_param(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.put("UpdaterTargetBranch", branch)
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
|
||||
try:
|
||||
assert Updater().target_branch == expected
|
||||
finally:
|
||||
params.remove("UpdaterTargetBranch")
|
||||
@@ -24,10 +24,6 @@ SP_BRANCH_MIGRATIONS = {
|
||||
("tizi", "staging-c3-new"): "staging",
|
||||
("tizi", "dev-c3-new"): "dev",
|
||||
("tizi", "master-dev-c3-new"): "master-dev",
|
||||
("tizi", "release3"): "release-tizi",
|
||||
("tizi", "release3-staging"): "release-tizi-staging",
|
||||
("mici", "release3"): "release-mici",
|
||||
("mici", "release3-staging"): "release-mici-staging",
|
||||
}
|
||||
|
||||
BUILD_METADATA_FILENAME = "build.json"
|
||||
|
||||
@@ -33,6 +33,6 @@ fi
|
||||
|
||||
# Build _cabana
|
||||
cd "$ROOT"
|
||||
scons -j4 tools/cabana/_cabana cereal/messaging/bridge
|
||||
scons -j4 tools/cabana/_cabana
|
||||
|
||||
exec "$DIR/_cabana" "$@"
|
||||
|
||||
@@ -106,8 +106,8 @@ cereal::PandaState::PandaType Panda::get_hw_type() {
|
||||
|
||||
|
||||
|
||||
void Panda::send_heartbeat(bool engaged, bool engaged_mads) {
|
||||
control_write(0xf3, engaged, engaged_mads);
|
||||
void Panda::send_heartbeat(bool engaged) {
|
||||
control_write(0xf3, engaged, 0);
|
||||
}
|
||||
|
||||
void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) {
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
// Panda functionality
|
||||
cereal::PandaState::PandaType get_hw_type();
|
||||
void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U);
|
||||
void send_heartbeat(bool engaged, bool engaged_mads = false);
|
||||
void send_heartbeat(bool engaged);
|
||||
void set_can_speed_kbps(uint16_t bus, uint16_t speed);
|
||||
void set_data_speed_kbps(uint16_t bus, uint16_t speed);
|
||||
bool can_receive(std::vector<can_frame>& out_vec);
|
||||
|
||||
@@ -276,10 +276,7 @@ struct RouteIdentifier {
|
||||
std::string display_slice() const {
|
||||
const int begin = slice_explicit ? slice_begin : available_begin;
|
||||
const int end = slice_explicit ? slice_end : available_end;
|
||||
if (end < 0) {
|
||||
return std::to_string(begin) + ":";
|
||||
}
|
||||
if (end == begin) {
|
||||
if (end < 0 || end == begin) {
|
||||
return std::to_string(begin);
|
||||
}
|
||||
return std::to_string(begin) + ":" + std::to_string(end);
|
||||
@@ -381,7 +378,7 @@ public:
|
||||
StreamAccumulator &operator=(const StreamAccumulator &) = delete;
|
||||
|
||||
void setDbcName(const std::string &dbc_name);
|
||||
void appendEvent(kj::ArrayPtr<const capnp::word> data);
|
||||
void appendEvent(cereal::Event::Which which, kj::ArrayPtr<const capnp::word> data);
|
||||
void appendCanFrames(CanServiceKind service, const std::vector<LiveCanFrame> &frames);
|
||||
StreamExtractBatch takeBatch();
|
||||
const std::string &carFingerprint() const;
|
||||
|
||||
@@ -598,7 +598,9 @@ struct StreamPoller::Impl {
|
||||
}
|
||||
kj::ArrayPtr<const capnp::word> data(reinterpret_cast<const capnp::word *>(msg->getData()),
|
||||
size / sizeof(capnp::word));
|
||||
accumulator->appendEvent(data);
|
||||
capnp::FlatArrayMessageReader event_reader(data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
accumulator->appendEvent(event.which(), data);
|
||||
received_messages.fetch_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ bool apply_route_slice_change(AppSession *session, UiState *state, std::string_v
|
||||
int begin = 0;
|
||||
int end = 0;
|
||||
if (!parse_slice_spec(slice_text, &begin, &end)) {
|
||||
state->error_text = "Slice must be N, N:, or N:M.";
|
||||
state->error_text = "Slice must be N or N:M.";
|
||||
state->open_error_popup = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "tools/jotpluggler/common.h"
|
||||
|
||||
#include <capnp/dynamic.h>
|
||||
#include <kj/exception.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
@@ -206,18 +205,6 @@ struct LoadStats {
|
||||
mutable std::mutex progress_mutex;
|
||||
};
|
||||
|
||||
// Skip individual messages that our local Cap'n Proto schema can't project,
|
||||
// such as logs recorded by a newer build.
|
||||
template <typename Fn>
|
||||
void with_parseable_event(kj::ArrayPtr<const capnp::word> data, Fn &&fn) {
|
||||
try {
|
||||
capnp::FlatArrayMessageReader event_reader(data);
|
||||
fn(event_reader.getRoot<cereal::Event>());
|
||||
} catch (const kj::Exception &) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string curve_label(std::string_view series_name) {
|
||||
return std::string(series_name.empty() ? std::string_view{"plot"} : series_name);
|
||||
}
|
||||
@@ -679,11 +666,11 @@ std::vector<TimelineEntry> extract_segment_timeline(const std::vector<Event> &ev
|
||||
if (event_record.which != cereal::Event::Which::SELFDRIVE_STATE) {
|
||||
continue;
|
||||
}
|
||||
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
|
||||
const auto sd = event.getSelfdriveState();
|
||||
const double mono_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
|
||||
append_timeline_entry(&timeline, mono_time, alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
|
||||
});
|
||||
capnp::FlatArrayMessageReader event_reader(event_record.data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
const auto sd = event.getSelfdriveState();
|
||||
const double mono_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
|
||||
append_timeline_entry(&timeline, mono_time, alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
|
||||
}
|
||||
|
||||
return timeline;
|
||||
@@ -695,9 +682,9 @@ std::vector<LogEntry> extract_segment_logs(const std::vector<Event> &events) {
|
||||
std::string last_alert_key;
|
||||
|
||||
for (const Event &event_record : events) {
|
||||
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
|
||||
append_log_event(event_record.which, event, 0.0, &logs, &last_alert_key);
|
||||
});
|
||||
capnp::FlatArrayMessageReader event_reader(event_record.data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
append_log_event(event_record.which, event, 0.0, &logs, &last_alert_key);
|
||||
}
|
||||
|
||||
return logs;
|
||||
@@ -707,9 +694,9 @@ RouteMetadata extract_segment_metadata(const std::vector<Event> &events) {
|
||||
RouteMetadata metadata;
|
||||
for (const Event &event_record : events) {
|
||||
if (event_record.which != cereal::Event::Which::CAR_PARAMS) continue;
|
||||
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
|
||||
metadata.car_fingerprint = event.getCarParams().getCarFingerprint().cStr();
|
||||
});
|
||||
capnp::FlatArrayMessageReader event_reader(event_record.data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
metadata.car_fingerprint = event.getCarParams().getCarFingerprint().cStr();
|
||||
if (!metadata.car_fingerprint.empty()) break;
|
||||
}
|
||||
return metadata;
|
||||
@@ -1276,20 +1263,24 @@ void append_fast_node(const ResolvedNode &node,
|
||||
}
|
||||
}
|
||||
|
||||
void append_event_fast_reader(cereal::Event::Which which,
|
||||
const cereal::Event::Reader &event,
|
||||
const SchemaIndex &schema,
|
||||
const dbc::Database *can_dbc,
|
||||
bool skip_raw_can,
|
||||
double time_offset,
|
||||
SeriesAccumulator *series) {
|
||||
void append_event_fast(cereal::Event::Which which,
|
||||
int32_t eidx_segnum,
|
||||
kj::ArrayPtr<const capnp::word> data,
|
||||
const SchemaIndex &schema,
|
||||
const dbc::Database *can_dbc,
|
||||
bool skip_raw_can,
|
||||
double time_offset,
|
||||
SeriesAccumulator *series) {
|
||||
if (eidx_segnum != -1) {
|
||||
return;
|
||||
}
|
||||
const uint16_t which_index = static_cast<uint16_t>(which);
|
||||
if (which_index >= schema.by_which.size() || !schema.by_which[which_index].has_value()) {
|
||||
return;
|
||||
}
|
||||
const ResolvedService &service = *schema.by_which[which_index];
|
||||
const capnp::DynamicStruct::Reader dynamic_event(event);
|
||||
const capnp::DynamicValue::Reader payload = dynamic_event.get(service.union_field);
|
||||
capnp::FlatArrayMessageReader event_reader(data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
const double tm = static_cast<double>(event.getLogMonoTime()) / 1.0e9 - time_offset;
|
||||
append_fixed_scalar_point(&series->fixed_series[static_cast<size_t>(service.valid_slot)],
|
||||
tm,
|
||||
@@ -1338,23 +1329,8 @@ void append_event_fast_reader(cereal::Event::Which which,
|
||||
}
|
||||
}
|
||||
|
||||
append_fast_node(service.payload, payload, tm, series);
|
||||
}
|
||||
|
||||
void append_event_fast(cereal::Event::Which which,
|
||||
int32_t eidx_segnum,
|
||||
kj::ArrayPtr<const capnp::word> data,
|
||||
const SchemaIndex &schema,
|
||||
const dbc::Database *can_dbc,
|
||||
bool skip_raw_can,
|
||||
double time_offset,
|
||||
SeriesAccumulator *series) {
|
||||
if (eidx_segnum != -1) {
|
||||
return;
|
||||
}
|
||||
with_parseable_event(data, [&](const cereal::Event::Reader &event) {
|
||||
append_event_fast_reader(which, event, schema, can_dbc, skip_raw_can, time_offset, series);
|
||||
});
|
||||
const capnp::DynamicStruct::Reader dynamic_event(event);
|
||||
append_fast_node(service.payload, dynamic_event.get(service.union_field), tm, series);
|
||||
}
|
||||
|
||||
void append_events_fast_range(const std::vector<Event> &events,
|
||||
@@ -2011,7 +1987,10 @@ struct StreamAccumulator::Impl {
|
||||
return;
|
||||
}
|
||||
detected_dbc_name = next_dbc;
|
||||
can_dbc = load_dbc_by_name(detected_dbc_name);
|
||||
can_dbc.reset();
|
||||
if (!detected_dbc_name.empty()) {
|
||||
can_dbc.emplace(resolve_dbc_path(detected_dbc_name));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2029,35 +2008,35 @@ void StreamAccumulator::setDbcName(const std::string &dbc_name) {
|
||||
impl_->refresh_dbc();
|
||||
}
|
||||
|
||||
void StreamAccumulator::appendEvent(kj::ArrayPtr<const capnp::word> data) {
|
||||
with_parseable_event(data, [&](const cereal::Event::Reader &event) {
|
||||
const cereal::Event::Which which = event.which();
|
||||
const double boot_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
|
||||
if (!impl_->time_offset.has_value()) {
|
||||
impl_->time_offset = boot_time;
|
||||
}
|
||||
if (which == cereal::Event::Which::CAR_PARAMS) {
|
||||
const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr();
|
||||
if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) {
|
||||
impl_->car_fingerprint = fingerprint;
|
||||
impl_->refresh_dbc();
|
||||
}
|
||||
void StreamAccumulator::appendEvent(cereal::Event::Which which, kj::ArrayPtr<const capnp::word> data) {
|
||||
capnp::FlatArrayMessageReader event_reader(data);
|
||||
const cereal::Event::Reader event = event_reader.getRoot<cereal::Event>();
|
||||
const double boot_time = static_cast<double>(event.getLogMonoTime()) / 1.0e9;
|
||||
if (!impl_->time_offset.has_value()) {
|
||||
impl_->time_offset = boot_time;
|
||||
}
|
||||
if (which == cereal::Event::Which::CAR_PARAMS) {
|
||||
const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr();
|
||||
if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) {
|
||||
impl_->car_fingerprint = fingerprint;
|
||||
impl_->refresh_dbc();
|
||||
}
|
||||
}
|
||||
|
||||
append_event_fast_reader(which,
|
||||
event,
|
||||
impl_->schema,
|
||||
impl_->can_dbc ? &*impl_->can_dbc : nullptr,
|
||||
impl_->can_dbc.has_value(),
|
||||
*impl_->time_offset,
|
||||
&impl_->series);
|
||||
append_log_event(which, event, *impl_->time_offset, &impl_->logs, &impl_->last_alert_key);
|
||||
if (which == cereal::Event::Which::SELFDRIVE_STATE) {
|
||||
const auto sd = event.getSelfdriveState();
|
||||
append_timeline_entry(&impl_->timeline, boot_time - *impl_->time_offset,
|
||||
alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
|
||||
}
|
||||
});
|
||||
append_event_fast(which,
|
||||
-1,
|
||||
data,
|
||||
impl_->schema,
|
||||
impl_->can_dbc ? &*impl_->can_dbc : nullptr,
|
||||
true,
|
||||
*impl_->time_offset,
|
||||
&impl_->series);
|
||||
append_log_event(which, event, *impl_->time_offset, &impl_->logs, &impl_->last_alert_key);
|
||||
if (which == cereal::Event::Which::SELFDRIVE_STATE) {
|
||||
const auto sd = event.getSelfdriveState();
|
||||
append_timeline_entry(&impl_->timeline, boot_time - *impl_->time_offset,
|
||||
alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
|
||||
}
|
||||
}
|
||||
|
||||
void StreamAccumulator::appendCanFrames(CanServiceKind service, const std::vector<LiveCanFrame> &frames) {
|
||||
@@ -2147,11 +2126,13 @@ RouteData load_route_data(const std::string &route_name,
|
||||
|
||||
const RouteMetadata metadata = detect_route_metadata(segments, route.selector);
|
||||
const std::string resolved_dbc = !dbc_name.empty() ? dbc_name : detect_dbc_for_fingerprint(metadata.car_fingerprint);
|
||||
const std::optional<dbc::Database> can_dbc = load_dbc_by_name(resolved_dbc);
|
||||
const std::optional<dbc::Database> can_dbc = resolved_dbc.empty()
|
||||
? std::nullopt
|
||||
: std::optional<dbc::Database>(std::in_place, resolve_dbc_path(resolved_dbc));
|
||||
|
||||
const SchemaIndex &schema = SchemaIndex::instance();
|
||||
LoadedRouteArtifacts artifacts = load_route_series_parallel(segments, schema, can_dbc ? &*can_dbc : nullptr,
|
||||
route.selector, can_dbc.has_value(), &stats);
|
||||
route.selector, !resolved_dbc.empty(), &stats);
|
||||
RouteData route_data = build_route_data(std::move(artifacts.series),
|
||||
std::move(artifacts.can_messages),
|
||||
std::move(artifacts.logs),
|
||||
|
||||
@@ -62,7 +62,7 @@ class GithubUtils:
|
||||
self.api_call(github_path, data=data, method=HTTPMethod.POST, data_call=True)
|
||||
|
||||
def get_bucket_sha(self, bucket):
|
||||
github_path = f"git/ref/heads/{bucket}"
|
||||
github_path = f"git/refs/heads/{bucket}"
|
||||
r = self.api_call(github_path, data_call=True, raise_on_failure=False)
|
||||
return r.json()['object']['sha'] if r.ok else None
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user