mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-17 02:02:05 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db300721c1 | |||
| 31dc4d8e52 | |||
| da6313dbe9 | |||
| 01a843e0ac | |||
| 097dd9b5f2 | |||
| 122ac986de | |||
| bc27262a92 | |||
| 066ba92e77 | |||
| dfb21bd53e |
@@ -35,18 +35,36 @@ jobs:
|
||||
- name: Init sunnypilot opendbc submodule
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Checkout upstream openpilot cereal
|
||||
- name: Checkout upstream openpilot
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: 'commaai/openpilot'
|
||||
path: upstream_openpilot
|
||||
sparse-checkout: cereal
|
||||
ref: "refs/heads/master"
|
||||
|
||||
- name: Init upstream opendbc submodule
|
||||
working-directory: upstream_openpilot
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Locate upstream capnp paths
|
||||
id: locate-capnp
|
||||
run: |
|
||||
CEREAL_DIR=$(find upstream_openpilot -maxdepth 4 -name log.capnp -path '*/cereal/log.capnp' -printf '%h\n' -quit)
|
||||
if [ -z "$CEREAL_DIR" ]; then
|
||||
echo "::error::Could not locate cereal/log.capnp in upstream openpilot"
|
||||
exit 1
|
||||
fi
|
||||
echo "cereal_dir=$CEREAL_DIR" >> "$GITHUB_OUTPUT"
|
||||
echo "Found upstream cereal at: $CEREAL_DIR"
|
||||
|
||||
IMPORT_ARGS=""
|
||||
CAR_CAPNP=$(find upstream_openpilot -maxdepth 5 -name car.capnp -path '*/opendbc/car/car.capnp' -printf '%h\n' -quit)
|
||||
if [ -n "$CAR_CAPNP" ]; then
|
||||
IMPORT_ARGS="-I $CAR_CAPNP"
|
||||
echo "Found car.capnp at: $CAR_CAPNP"
|
||||
fi
|
||||
echo "import_args=$IMPORT_ARGS" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
@@ -62,4 +80,5 @@ jobs:
|
||||
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||
-r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal
|
||||
-r -f /tmp/sp_schema.json --cereal-dir ${{ steps.locate-capnp.outputs.cereal_dir }} \
|
||||
${{ steps.locate-capnp.outputs.import_args }}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[submodule "opendbc"]
|
||||
path = opendbc_repo
|
||||
url = https://github.com/sunnypilot/opendbc.git
|
||||
branch = tn
|
||||
[submodule "msgq"]
|
||||
path = msgq_repo
|
||||
url = https://github.com/commaai/msgq.git
|
||||
|
||||
+30
-1
@@ -1,5 +1,34 @@
|
||||
sunnypilot Version 2026.002.000 (2026-xx-xx)
|
||||
sunnypilot Version 2026.002.000 (2026-06-28)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* ui: update gates for certain toggles by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1830
|
||||
* release: ignore upstream IsReleaseBranch by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1831
|
||||
* manager: disable DEVELOPMENT_ONLY reset by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1833
|
||||
* sunnylink: fix max time offroad values by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1835
|
||||
* ui: show default model name by @nayan8teen in https://github.com/sunnypilot/sunnypilot/pull/1837
|
||||
* sunnylink: add CarParams fallback for brand-specific capabilities by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1839
|
||||
* sunnylink SDUI: tweak DisableUpdate param for clarity by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1842
|
||||
* Revert "DM: Lancia Delta HF Integrale model" by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1849
|
||||
* modeld_v2: safe model validation by @Discountchubbs in https://github.com/sunnypilot/sunnypilot/pull/1855
|
||||
* Revert "deprecate `carState.brake`" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/sunnypilot/pull/1860
|
||||
* sunnylink: deprecate legacy params metadata by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1862
|
||||
* ui: reset Enforce Torque Control and NNLC if both are enabled by @sunnyhaibin in https://github.com/sunnypilot/sunnypilot/pull/1863
|
||||
* What's Changed (sunnypilot/opendbc)
|
||||
* Rivian: suppress ACM hold-the-wheel warning during MADS-only lateral by @lukasloetkolben in https://github.com/sunnypilot/opendbc/pull/465
|
||||
* Sync: `commaai/opendbc:master` → `sunnypilot/opendbc:master` by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/479
|
||||
* safety: add option to ignore frequency check for RX checks by @sunnyhaibin in https://github.com/sunnypilot/opendbc/pull/480
|
||||
* Revert "deprecate carState.brake" for Honda Gas Interceptor by @mvl-boston in https://github.com/sunnypilot/opendbc/pull/481
|
||||
* New Contributors (sunnypilot/sunnypilot)
|
||||
* @mvl-boston made their first contribution in https://github.com/sunnypilot/sunnypilot/pull/1860
|
||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2026.001.007...v2026.002.000
|
||||
************************
|
||||
* Synced with commaai's openpilot (v0.11.1)
|
||||
* master commit 69e2c321e49760e52f7983eaa0a5f77cb95de637 (June 02, 2026)
|
||||
* New driver monitoring model
|
||||
* Improved image processing pipeline for driver camera
|
||||
* Improved thermal policy for comma four
|
||||
* Acura MDX 2022-24 support thanks to mvl-boston!
|
||||
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
|
||||
|
||||
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
========================
|
||||
|
||||
@@ -194,7 +194,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
acceleration @8 :Acceleration;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
@@ -297,26 +296,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
greenLightAlert @0 :Bool;
|
||||
leadDepartAlert @1 :Bool;
|
||||
}
|
||||
|
||||
# Acceleration Personality (Eco / Normal / Sport)
|
||||
struct Acceleration {
|
||||
personality @0 :AccelerationPersonality;
|
||||
enabled @1 :Bool;
|
||||
maxAccel @2 :Float32; # current speed-indexed accel ceiling
|
||||
brakeNeed @3 :Float32; # repurposed: follow-gap widen added on top of the stock t_follow (s)
|
||||
decelTarget @4 :Float32; # repurposed: t_follow handed to the MPC (s)
|
||||
smoothActive @5 :Bool; # repurposed: follow-gap widen currently active
|
||||
bypassed @6 :Bool; # unused (input-shaping design has no output post-shaping / bypass)
|
||||
comfortStopActive @7 :Bool; # low-speed comfort decel-to-stop floor currently governing (behind a near-stopped lead)
|
||||
comfortStopFloor @8 :Float32; # comfort-stop floor commanded (m/s^2, negative; 0 when not engaged)
|
||||
leadUnstable @9 :Bool; # RadarDistance lead-instability telemetry (bimodal/bouncing radar lead; informational, no control effect yet)
|
||||
}
|
||||
|
||||
enum AccelerationPersonality {
|
||||
eco @0;
|
||||
normal @1;
|
||||
sport @2;
|
||||
}
|
||||
}
|
||||
|
||||
struct OnroadEventSP @0xda96579883444c35 {
|
||||
@@ -363,7 +342,6 @@ struct OnroadEventSP @0xda96579883444c35 {
|
||||
speedLimitChanged @21;
|
||||
speedLimitPending @22;
|
||||
e2eChime @23;
|
||||
laneChangeRoadEdge @24;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,8 +448,6 @@ struct LiveMapDataSP @0xf416ec09499d9d19 {
|
||||
|
||||
struct ModelDataV2SP @0xa1680744031fdb2d {
|
||||
laneTurnDirection @0 :TurnDirection;
|
||||
leftLaneChangeEdgeBlock @1 :Bool;
|
||||
rightLaneChangeEdgeBlock @2 :Bool;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Schema-level cereal compat check between sunnypilot and upstream openpilot.
|
||||
"""Validate sunnypilot routes are parseable by stock commaai/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.
|
||||
Cap'n Proto is wire-compatible across renames, type relocations, and
|
||||
additive fields. The only breaking change is a union variant that
|
||||
upstream doesn't recognize — an unknown discriminant makes the entire
|
||||
union unreadable.
|
||||
|
||||
This script checks: for every struct with a union that exists in both
|
||||
schemas, does sunnypilot introduce union variants upstream doesn't have?
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,46 +25,19 @@ def hex_id(value: int) -> str:
|
||||
return f"0x{value:016x}"
|
||||
|
||||
|
||||
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 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
|
||||
fields = []
|
||||
for name, field in schema.fields.items():
|
||||
proto = field.proto
|
||||
ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None
|
||||
discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None
|
||||
fields.append({"name": name, "ordinal": ordinal, "discriminant": discriminant})
|
||||
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()],
|
||||
"fields": fields,
|
||||
}
|
||||
|
||||
|
||||
@@ -105,15 +79,16 @@ def collect_schema(root: Any) -> dict[str, dict]:
|
||||
return structs
|
||||
|
||||
|
||||
def load_log(cereal_dir: str) -> Any:
|
||||
def load_log(cereal_dir: str, extra_imports: list[str] | None = None) -> Any:
|
||||
import capnp
|
||||
cereal_dir = os.path.abspath(cereal_dir)
|
||||
capnp.remove_import_hook()
|
||||
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
|
||||
imports = [cereal_dir] + [os.path.abspath(p) for p in (extra_imports or [])]
|
||||
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=imports)
|
||||
|
||||
|
||||
def dump_schema(cereal_dir: str, path: str) -> None:
|
||||
log = load_log(cereal_dir)
|
||||
def dump_schema(cereal_dir: str, path: str, extra_imports: list[str] | None = None) -> None:
|
||||
log = load_log(cereal_dir, extra_imports)
|
||||
payload = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
@@ -123,100 +98,37 @@ def dump_schema(cereal_dir: str, path: str) -> None:
|
||||
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_structs = sunnypilot_dump["structs"]
|
||||
upstream_structs = 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:
|
||||
for type_id, sp_struct in sunnypilot_structs.items():
|
||||
if not sp_struct["hasUnion"]:
|
||||
continue
|
||||
up_struct = upstream_structs.get(type_id)
|
||||
if up_struct is None:
|
||||
continue
|
||||
|
||||
sunnypilot_fields = index_fields_by_ordinal(sunnypilot_struct)
|
||||
upstream_fields = index_fields_by_ordinal(upstream_struct)
|
||||
display = sunnypilot_struct["displayName"]
|
||||
up_ordinals = {f["ordinal"] for f in up_struct["fields"] if f.get("discriminant") is not None}
|
||||
display = sp_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."
|
||||
)
|
||||
for field in sp_struct["fields"]:
|
||||
if field.get("discriminant") is None:
|
||||
continue
|
||||
|
||||
if not types_equal(sunnypilot_field["type"], upstream_field["type"]):
|
||||
if field["ordinal"] not in up_ordinals:
|
||||
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'])}."
|
||||
f"{display} @{field['ordinal']} '{field['name']}': "
|
||||
f"union variant not present upstream (discriminant={field['discriminant']})"
|
||||
)
|
||||
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(cereal_dir: str, peer_path: str) -> int:
|
||||
log = load_log(cereal_dir)
|
||||
peer_dump = load_peer(peer_path)
|
||||
def run_read(cereal_dir: str, peer_path: str, extra_imports: list[str] | None = None) -> int:
|
||||
log = load_log(cereal_dir, extra_imports)
|
||||
with open(peer_path, "r", encoding="utf-8") as f:
|
||||
peer_dump = json.load(f)
|
||||
local_dump = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
@@ -224,32 +136,29 @@ def run_read(cereal_dir: str, peer_path: str) -> int:
|
||||
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).")
|
||||
print("cereal compat OK: upstream can parse sunnypilot routes.")
|
||||
return 0
|
||||
|
||||
print(f"cereal compat FAIL: upstream openpilot would misparse sunnypilot routes "
|
||||
f"({len(violations)} violation(s)):")
|
||||
print(f"cereal compat FAIL ({len(violations)} leaked union variant(s)):")
|
||||
for v in violations:
|
||||
print(f" {v}")
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="sunnypilot <-> upstream cereal compatibility validator (schema-level)."
|
||||
)
|
||||
parser = argparse.ArgumentParser(description="sunnypilot cereal upstream compat check")
|
||||
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)")
|
||||
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
|
||||
mode.add_argument("-r", "--read", action="store_true", help="validate against peer schema")
|
||||
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path")
|
||||
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory")
|
||||
parser.add_argument("-I", "--import-path", action="append", default=[], help="extra capnp import paths")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.generate:
|
||||
dump_schema(args.cereal_dir, args.file)
|
||||
dump_schema(args.cereal_dir, args.file, args.import_path)
|
||||
return 0
|
||||
return run_read(args.cereal_dir, args.file)
|
||||
return run_read(args.cereal_dir, args.file, args.import_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "cereal/gen/cpp/log.capnp.h"
|
||||
#include "cereal/gen/cpp/custom.capnp.h"
|
||||
|
||||
inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}},
|
||||
@@ -180,19 +179,12 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RoadEdgeLaneChangeEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// toyota specific params
|
||||
{"ToyotaAutoHold", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaTSS2Long", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaDriveMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// MADS params
|
||||
{"Mads", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
@@ -236,14 +228,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// Acceleration Personality (Eco / Normal / Sport)
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||
|
||||
// Radar Distance: de-noise the lead the MPC follows (flicker-hold + churn smoother); never reports a
|
||||
// farther/faster lead than reality, so braking stays >= stock
|
||||
{"RadarDistance", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// sunnypilot model params
|
||||
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
|
||||
+120
-117
@@ -4,24 +4,24 @@
|
||||
|
||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||
|
||||
# 340 Supported Cars
|
||||
# 342 Supported Cars
|
||||
|
||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 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=Acura ILX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|ILX 2019|All|openpilot|26 mph|25 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=Acura ILX 2019">Buy Here</a></sub></details>|||
|
||||
|Acura|MDX 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|43 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=Acura MDX 2022-24">Buy Here</a></sub></details>|||
|
||||
|Acura|MDX 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 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=Acura MDX 2022-24">Buy Here</a></sub></details>|||
|
||||
|Acura|MDX 2025-26|All except Type S|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=Acura MDX 2025-26">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 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=Acura RDX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2019-21|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=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 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=Acura TLX 2021-22">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1,5</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=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2021-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 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=Acura TLX 2021-22">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2025|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=Acura TLX 2025">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>11</sup>](#footnotes)|A3 2014-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=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>11</sup>](#footnotes)|A3 Sportback e-tron 2017-18|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 A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>11</sup>](#footnotes)|Q2 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 Q2 2018">Buy Here</a></sub></details>|||
|
||||
|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>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi[<sup>12</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 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>|||
|
||||
@@ -33,7 +33,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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>||
|
||||
|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>|||
|
||||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>|||
|
||||
@@ -47,8 +47,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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|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|Focus 2018-22[<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-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus Hybrid 2018-22[<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-22">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>|
|
||||
@@ -75,35 +75,35 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1,5</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>||
|
||||
|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 2018-22|All|openpilot available[<sup>1,5</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|City (Brazil only) 2023|All|openpilot available[<sup>1,5</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 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 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>|||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1,5</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,5</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 Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1,5</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,5</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,5</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 Hybrid 2025-26|All|openpilot available[<sup>1,5</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,5</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,5</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>|||
|
||||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 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 CR-V 2015-16">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|15 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 CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|15 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 CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V 2023-26|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 CR-V 2023-26">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1,5</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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||
|Honda|CR-V Hybrid 2023-26|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 CR-V Hybrid 2023-26">Buy Here</a></sub></details>|||
|
||||
|Honda|e 2020|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 e 2020">Buy Here</a></sub></details>|||
|
||||
|Honda|e 2020|All|openpilot available[<sup>1,5</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 e 2020">Buy Here</a></sub></details>|||
|
||||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 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 Fit 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Freed 2020|Honda Sensing|openpilot|26 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 Freed 2020">Buy Here</a></sub></details>|||
|
||||
|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 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 HR-V 2019-22">Buy Here</a></sub></details>|||
|
||||
|Honda|HR-V 2023-25|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 HR-V 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Insight 2019-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 Insight 2019-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Inspire 2018|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 Inspire 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|N-Box 2018|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|11 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 N-Box 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|HR-V 2023-25|All|openpilot available[<sup>1,5</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 HR-V 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Insight 2019-22|All|openpilot available[<sup>1,5</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 Insight 2019-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Inspire 2018|All|openpilot available[<sup>1,5</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 Inspire 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|N-Box 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|11 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 N-Box 2018">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 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 Odyssey 2018-20">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey 2021-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|43 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 Odyssey 2021-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey 2021-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 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 Odyssey 2021-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey (Singapore) 2021|Honda Sensing|openpilot|19 mph|0 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 Odyssey (Singapore) 2021">Buy Here</a></sub></details>|||
|
||||
|Honda|Odyssey (Taiwan) 2018-19|Honda Sensing|openpilot|19 mph|0 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 Odyssey (Taiwan) 2018-19">Buy Here</a></sub></details>|||
|
||||
|Honda|Passport 2019-25|All|openpilot|26 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 Passport 2019-25">Buy Here</a></sub></details>|||
|
||||
@@ -126,6 +126,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Ioniq 5 (with HDA II) 2022-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=Hyundai Ioniq 5 (with HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq 5 (without HDA II) 2022-24|Highway Driving Assist|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 Ioniq 5 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq 6 (with HDA II) 2023-24|Highway Driving Assist II|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P 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 6 (with HDA II) 2023-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq 6 (without HDA II) 2023-24|Highway Driving Assist|Stock|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 Ioniq 6 (without HDA II) 2023-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Electric 2019|Smart Cruise Control (SCC)|Stock|0 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=Hyundai Ioniq Electric 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Electric 2020|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 Ioniq Electric 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 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=Hyundai Ioniq Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
@@ -223,14 +224,15 @@ 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>12</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>||
|
||||
|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>||
|
||||
|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)|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>|||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|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-24">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>6</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>6</sup>](#footnotes)|Leaf IC 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 IC 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>6</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>6</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>|||
|
||||
@@ -238,35 +240,35 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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 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 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>|||
|
||||
|Subaru|Impreza 2017-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 Impreza 2017-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|Impreza 2020-22|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 Impreza 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|Legacy 2015-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 Legacy 2015-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|Legacy 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 Legacy 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|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 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)|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Karoq 2019-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 Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Kodiaq 2017-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 Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Octavia 2015-19[<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 Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Octavia RS 2016[<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 Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Octavia Scout 2017-19[<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 Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-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 Scala 2020-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda[<sup>11</sup>](#footnotes)|Superb 2015-22[<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 Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A 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=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B 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=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A 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=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B 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=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</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 Impreza 2017-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|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</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 Impreza 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|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</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 Legacy 2015-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|Legacy 2020-22|All[<sup>7</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 Legacy 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|Outback 2015-17|EyeSight Driver Assistance[<sup>7</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>7</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>7</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>17</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>17</sup>](#footnotes)|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Kodiaq 2017-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia 2015-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia RS 2016[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Octavia Scout 2017-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Scala 2020-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A 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=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B 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=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A 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=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B 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=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard 2019-20|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 Alphard 2019-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard Hybrid 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 Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 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 Avalon 2016">Buy Here</a></sub></details>|||
|
||||
@@ -279,8 +281,8 @@ 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 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 2018-20|All|Stock|0 mph[<sup>11</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 2021-24|All|openpilot|0 mph[<sup>11</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 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>|||
|
||||
@@ -312,60 +314,61 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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>||
|
||||
|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)|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>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTD 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 GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTE 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 GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|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)|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>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Passat Alltrack 2015-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 Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Passat GTE 2015-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 Passat GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 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 Polo 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 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 Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|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 T-Cross 2021">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|T-Roc 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 T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Taos 2022-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=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont 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 Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont Cross Sport 2021-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 Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont X 2021-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 Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Tiguan 2018-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=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Tiguan eHybrid 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 Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Touran 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=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Polo 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen[<sup>12</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|
||||
### Footnotes
|
||||
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`. <br />
|
||||
<sup>2</sup>Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia. <br />
|
||||
<sup>3</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <br />
|
||||
<sup>4</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
|
||||
<sup>5</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>6</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>7</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>8</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>9</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>10</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>11</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
||||
<sup>12</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>13</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>14</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>15</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>16</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
|
||||
<sup>5</sup>Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW. <br />
|
||||
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
||||
<sup>13</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>14</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>15</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>16</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>17</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
|
||||
|
||||
## Community Maintained Cars
|
||||
Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/).
|
||||
|
||||
+1
-1
Submodule opendbc_repo updated: 7752485b92...ffa13083ff
@@ -10,7 +10,7 @@ from cereal import car, log, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper
|
||||
from openpilot.common.swaglog import cloudlog, ForwardingHandler
|
||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||
|
||||
from opendbc.car import DT_CTRL, structs
|
||||
from opendbc.car.can_definitions import CanData, CanRecvCallable, CanSendCallable
|
||||
from opendbc.car.carlog import carlog
|
||||
@@ -121,13 +121,7 @@ class Car:
|
||||
self.CI, self.CP, self.CP_SP = CI, CI.CP, CI.CP_SP
|
||||
self.RI = RI
|
||||
|
||||
# set alternative experiences from parameters
|
||||
sp_toyota_auto_brake_hold = self.params.get_bool("ToyotaAutoHold")
|
||||
self.CP.alternativeExperience = 0
|
||||
if sp_toyota_auto_brake_hold:
|
||||
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALLOW_AEB
|
||||
|
||||
|
||||
# mads
|
||||
set_alternative_experience(self.CP, self.CP_SP, self.params)
|
||||
set_car_specific_params(self.CP, self.CP_SP, self.params)
|
||||
|
||||
@@ -13,7 +13,7 @@ from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
|
||||
from opendbc.car.can_definitions import CanData
|
||||
from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces
|
||||
from opendbc.car.fingerprints import MIGRATION
|
||||
from opendbc.car.honda.values import HondaFlags
|
||||
from opendbc.car.honda.values import CAR as HONDA, HondaFlags
|
||||
from opendbc.car.structs import car
|
||||
from opendbc.car.tests.routes import non_tested_cars, routes, CarTestRoute
|
||||
from opendbc.car.values import Platform, PLATFORMS
|
||||
@@ -358,7 +358,13 @@ class TestCarModelBase(unittest.TestCase):
|
||||
self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev())
|
||||
|
||||
if self.safety.get_brake_pressed_prev() != prev_panda_brake:
|
||||
self.assertEqual(CS.brakePressed, self.safety.get_brake_pressed_prev())
|
||||
# TODO: remove this exception once this mismatch is resolved
|
||||
brake_pressed = CS.brakePressed
|
||||
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05:
|
||||
brake_pressed = False
|
||||
|
||||
self.assertEqual(brake_pressed, self.safety.get_brake_pressed_prev())
|
||||
|
||||
if self.safety.get_regen_braking_prev() != prev_panda_regen_braking:
|
||||
self.assertEqual(CS.regenBraking, self.safety.get_regen_braking_prev())
|
||||
@@ -442,7 +448,12 @@ class TestCarModelBase(unittest.TestCase):
|
||||
checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or
|
||||
angle_can < (self.safety.get_angle_meas_min() - 1))
|
||||
|
||||
checks['brakePressed'] += CS.brakePressed != self.safety.get_brake_pressed_prev()
|
||||
# TODO: remove this exception once this mismatch is resolved
|
||||
brake_pressed = CS.brakePressed
|
||||
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
|
||||
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brakeDEPRECATED > 0.05:
|
||||
brake_pressed = False
|
||||
checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev()
|
||||
checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev()
|
||||
checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev()
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class DesireHelper:
|
||||
def get_lane_change_direction(CS):
|
||||
return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right
|
||||
|
||||
def update(self, carstate, lateral_active, lane_change_prob, left_edge_detected=False, right_edge_detected=False):
|
||||
def update(self, carstate, lateral_active, lane_change_prob):
|
||||
self.alc.update_params()
|
||||
self.lane_turn_controller.update_params()
|
||||
v_ego = carstate.vEgo
|
||||
@@ -88,8 +88,8 @@ class DesireHelper:
|
||||
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
|
||||
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
||||
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
||||
|
||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
||||
|
||||
|
||||
@@ -219,7 +219,6 @@ class LongitudinalMpc:
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = LongitudinalPlanSource.cruise
|
||||
self.t_follow_fn = None # sunnypilot: optional (t_follow, v_ego)->t_follow override; None == byte-stock
|
||||
|
||||
def reset(self):
|
||||
self.solver.reset()
|
||||
@@ -268,8 +267,8 @@ class LongitudinalMpc:
|
||||
for i in range(N):
|
||||
self.solver.cost_set(i, 'Zl', Zl)
|
||||
|
||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard, jerk_scale=1.0):
|
||||
jerk_factor = get_jerk_factor(personality) * jerk_scale
|
||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
||||
jerk_factor = get_jerk_factor(personality)
|
||||
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||
cost_weights = [X_EGO_OBSTACLE_COST, X_EGO_COST, V_EGO_COST, A_EGO_COST, jerk_factor * a_change_cost, jerk_factor * J_EGO_COST]
|
||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
||||
@@ -317,8 +316,6 @@ class LongitudinalMpc:
|
||||
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
|
||||
t_follow = get_T_FOLLOW(personality)
|
||||
v_ego = self.x0[1]
|
||||
if self.t_follow_fn is not None:
|
||||
t_follow = self.t_follow_fn(t_follow, v_ego)
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||
|
||||
@@ -52,7 +52,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.CP = CP
|
||||
self.mpc = LongitudinalMpc(dt=dt)
|
||||
LongitudinalPlannerSP.__init__(self, self.CP, CP_SP, self.mpc)
|
||||
self.mpc.t_follow_fn = self.accel.get_t_follow # Acceleration Personality: add-only follow-gap widen
|
||||
self.fcw = False
|
||||
self.dt = dt
|
||||
self.allow_throttle = True
|
||||
@@ -111,7 +110,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
# No change cost when user is controlling the speed, or when standstill
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
accel_clip = [ACCEL_MIN, self.accel.get_max_accel(v_ego)]
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
|
||||
@@ -119,8 +118,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
self.v_desired_filter.x = v_ego
|
||||
# Clip aEgo to cruise limits to prevent large accelerations when becoming active
|
||||
self.a_desired = np.clip(sm['carState'].aEgo, accel_clip[0], accel_clip[1])
|
||||
self.accel.reset() # drop any accumulated follow-gap widen so it re-ramps cleanly on re-engage
|
||||
self._e2e_transition_guard.reset() # drop the tracked baseline so it doesn't apply a stale limit on re-engage
|
||||
|
||||
# Prevent divergence, smooth in current v_ego
|
||||
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
|
||||
@@ -139,9 +136,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
if force_slow_decel:
|
||||
v_cruise = 0.0
|
||||
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality, jerk_scale=self.accel.get_jerk_scale(v_ego))
|
||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||
self.mpc.update(self.smooth_radarstate(sm['radarState']), v_cruise, personality=sm['selfdriveState'].personality)
|
||||
self.mpc.update(sm['radarState'], v_cruise, personality=sm['selfdriveState'].personality)
|
||||
|
||||
self.v_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.v_solution)
|
||||
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||
@@ -163,8 +160,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||
|
||||
is_e2e = self.is_e2e(sm)
|
||||
if is_e2e:
|
||||
if self.is_e2e(sm):
|
||||
output_a_target = min(output_a_target_e2e, output_a_target_mpc)
|
||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
||||
if output_a_target < output_a_target_mpc:
|
||||
@@ -173,15 +169,8 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
output_a_target = output_a_target_mpc
|
||||
self.output_should_stop = output_should_stop_mpc
|
||||
|
||||
output_a_target = self.smooth_e2e_transition(output_a_target)
|
||||
|
||||
# Acceleration Personality shapes only MPC INPUTS (accel ceiling above + t_follow via mpc.t_follow_fn),
|
||||
# never the output accel -- output_a_target passes through byte-stock so the MPC owns the trajectory.
|
||||
|
||||
# Lower (braking) bound and the ceiling's downward slew stay at the stock rate; only the ceiling's
|
||||
# upward slew is tier-dependent (Acceleration Personality).
|
||||
accel_clip[0] = np.clip(accel_clip[0], self.prev_accel_clip[0] - 0.05, self.prev_accel_clip[0] + 0.05)
|
||||
accel_clip[1] = np.clip(accel_clip[1], self.prev_accel_clip[1] - 0.05, self.prev_accel_clip[1] + self.accel.get_rise_rate(v_ego))
|
||||
for idx in range(2):
|
||||
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
|
||||
self.output_a_target = np.clip(output_a_target, accel_clip[0], accel_clip[1])
|
||||
self.prev_accel_clip = accel_clip
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import inspect
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def test_smoothing_params_default_off():
|
||||
params_keys = (REPO_ROOT / "common/params_keys.h").read_text()
|
||||
|
||||
assert re.search(r'"AccelPersonalityEnabled", \{PERSISTENT \| BACKUP, BOOL, "0"\}', params_keys)
|
||||
assert re.search(r'"RadarDistance", \{PERSISTENT \| BACKUP, BOOL, "0"\}', params_keys)
|
||||
# the retired dRel-bias sub-toggles must not return (their features were deleted in the input-shaping rewrite)
|
||||
assert '"StopGapBias"' not in params_keys
|
||||
assert '"LeadDecelAnticipate"' not in params_keys
|
||||
|
||||
|
||||
def test_output_is_byte_stock_and_inputs_are_shaped():
|
||||
update_src = inspect.getsource(LongitudinalPlanner.update)
|
||||
|
||||
# INPUT shaping only: the accel ceiling and the radar-conditioning seam are present...
|
||||
assert "self.accel.get_max_accel(v_ego)" in update_src
|
||||
assert "self.mpc.update(self.smooth_radarstate(sm['radarState'])" in update_src
|
||||
# ...and the OUTPUT is never post-shaped (byte-stock output; no accel shaping, no should_stop override).
|
||||
assert "smooth_target_accel" not in update_src
|
||||
assert "sng_should_stop" not in update_src # reverted: the should_stop hysteresis caused a high-speed under-brake
|
||||
|
||||
|
||||
def test_t_follow_hook_wired_and_identity_default():
|
||||
init_src = inspect.getsource(LongitudinalPlanner.__init__)
|
||||
assert "self.mpc.t_follow_fn = self.accel.get_t_follow" in init_src # planner wires the add-only widen
|
||||
|
||||
mpc_init = inspect.getsource(LongitudinalMpc.__init__)
|
||||
assert "self.t_follow_fn = None" in mpc_init # default None == byte-stock identity
|
||||
|
||||
mpc_update = inspect.getsource(LongitudinalMpc.update)
|
||||
assert "if self.t_follow_fn is not None:" in mpc_update # guarded hook, only fires when set
|
||||
|
||||
|
||||
# Tokens for the reverted input-side DEC model-stop-target (capped v_target into the MPC pre-solve). It was
|
||||
# superseded by DEC blended-mode and chased a source-fixed radar gate; it must not silently return.
|
||||
_DEC_MODEL_STOP_TOKENS = ("apply_model_stop_target", "force_stop_requested", "_update_model_stop", "MODEL_STOP_TARGET_TIME")
|
||||
|
||||
|
||||
def test_dec_model_stop_target_not_reintroduced():
|
||||
this_file = Path(__file__).resolve()
|
||||
for sub in ("selfdrive/controls", "sunnypilot/selfdrive/controls"):
|
||||
for path in (REPO_ROOT / sub).rglob("*.py"):
|
||||
if path.resolve() == this_file:
|
||||
continue # this guard names the tokens as strings
|
||||
src = path.read_text()
|
||||
for token in _DEC_MODEL_STOP_TOKENS:
|
||||
assert token not in src, f"reverted DEC model-stop-target ({token}) re-introduced in {path}"
|
||||
@@ -28,7 +28,6 @@ from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path,
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
@@ -224,7 +223,6 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController(DH)
|
||||
|
||||
while True:
|
||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||
@@ -327,10 +325,7 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs, v_ego, modelv2_send.modelV2.roadEdges)
|
||||
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
|
||||
@@ -321,16 +321,9 @@ class SelfdriveD(CruiseHelper):
|
||||
# Handle lane change
|
||||
if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange:
|
||||
direction = self.sm['modelV2'].meta.laneChangeDirection
|
||||
mdv2sp = self.sm['modelDataV2SP']
|
||||
|
||||
if (CS.leftBlindspot and direction == LaneChangeDirection.left) or \
|
||||
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
||||
(CS.rightBlindspot and direction == LaneChangeDirection.right):
|
||||
self.events.add(EventName.laneChangeBlocked)
|
||||
|
||||
elif (mdv2sp.leftLaneChangeEdgeBlock and direction == LaneChangeDirection.left) or \
|
||||
(mdv2sp.rightLaneChangeEdgeBlock and direction == LaneChangeDirection.right):
|
||||
self.events_sp.add(custom.OnroadEventSP.EventName.laneChangeRoadEdge)
|
||||
|
||||
else:
|
||||
if direction == LaneChangeDirection.left:
|
||||
self.events.add(EventName.preLaneChangeLeft)
|
||||
|
||||
@@ -15,17 +15,7 @@ class Plant:
|
||||
messaging_initialized = False
|
||||
|
||||
def __init__(self, lead_relevancy=False, speed=0.0, distance_lead=2.0,
|
||||
enabled=True, only_lead2=False, only_radar=False, e2e=False, personality=0, force_decel=False,
|
||||
lead_dRel_glitch_fn=None, e2e_accel_fn=None):
|
||||
# lead_dRel_glitch_fn(current_time, d_rel, v_rel) -> (d_rel, v_rel): optional per-step override of the
|
||||
# REPORTED radar lead dRel/vRel, independent of the true physics used to advance distance_lead/speed --
|
||||
# models a sensor/fusion glitch (what radard actually reports can diverge from the real world) without
|
||||
# corrupting the closed-loop ego dynamics the rest of the planner reacts to. None -> no override (default).
|
||||
# e2e_accel_fn(current_time, ego_speed, ego_accel) -> float: optional override of the e2e model's
|
||||
# action.desiredAcceleration (default is a mild self.acceleration+0.1 echo, which can't independently
|
||||
# disagree with the MPC enough to exercise is_e2e()-gated behavior). None -> default echo (unchanged).
|
||||
self.lead_dRel_glitch_fn = lead_dRel_glitch_fn
|
||||
self.e2e_accel_fn = e2e_accel_fn
|
||||
enabled=True, only_lead2=False, only_radar=False, e2e=False, personality=0, force_decel=False):
|
||||
self.rate = 1. / DT_MDL
|
||||
|
||||
if not Plant.messaging_initialized:
|
||||
@@ -100,9 +90,6 @@ class Plant:
|
||||
prob_lead = 0.0
|
||||
status = False
|
||||
|
||||
if self.lead_dRel_glitch_fn is not None and self.lead_relevancy:
|
||||
d_rel, v_rel = self.lead_dRel_glitch_fn(self.current_time, d_rel, v_rel)
|
||||
|
||||
lead = log.RadarState.LeadData.new_message()
|
||||
lead.dRel = float(d_rel)
|
||||
lead.yRel = 0.0
|
||||
@@ -125,10 +112,7 @@ class Plant:
|
||||
position = log.XYZTData.new_message()
|
||||
position.x = [float(x) for x in (self.speed + 0.5) * np.array(ModelConstants.T_IDXS)]
|
||||
model.modelV2.position = position
|
||||
if self.e2e_accel_fn is not None:
|
||||
model.modelV2.action.desiredAcceleration = float(self.e2e_accel_fn(self.current_time, self.speed, self.acceleration))
|
||||
else:
|
||||
model.modelV2.action.desiredAcceleration = float(self.acceleration + 0.1)
|
||||
model.modelV2.action.desiredAcceleration = float(self.acceleration + 0.1)
|
||||
velocity = log.XYZTData.new_message()
|
||||
velocity.x = [float(x) for x in (self.speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)]
|
||||
velocity.x[0] = float(self.speed) # always start at current speed
|
||||
|
||||
@@ -31,15 +31,6 @@ DESCRIPTIONS = {
|
||||
"Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " +
|
||||
"without a turn signal activated while driving over 31 mph (50 km/h)."
|
||||
),
|
||||
"AccelPersonalityEnabled": tr_noop("Enable Eco/Normal/Sport acceleration profiles, including early soft braking."),
|
||||
"AccelPersonality": tr_noop(
|
||||
"Eco accelerates gently and brakes early and soft; Sport accelerates briskly. " +
|
||||
"Hard-braking authority is always preserved."
|
||||
),
|
||||
"RadarDistance": tr_noop(
|
||||
"Hold a lead through brief radar flicker/dropout so sunnypilot does not lose and re-grab it, " +
|
||||
"smoothing the hard/late brakes that radar drop-outs cause. Braking is never reduced below stock."
|
||||
),
|
||||
"AlwaysOnDM": tr_noop("Enable driver monitoring even when sunnypilot is not engaged."),
|
||||
'RecordFront': tr_noop("Upload data from the driver facing camera and help improve the driver monitoring algorithm."),
|
||||
"IsMetric": tr_noop("Display speed in km/h instead of mph."),
|
||||
@@ -73,12 +64,6 @@ class TogglesLayout(Widget):
|
||||
"disengage_on_accelerator.png",
|
||||
False,
|
||||
),
|
||||
"RadarDistance": (
|
||||
lambda: tr("Radar Distance"),
|
||||
DESCRIPTIONS["RadarDistance"],
|
||||
"speed_limit.png",
|
||||
False,
|
||||
),
|
||||
"IsLdwEnabled": (
|
||||
lambda: tr("Enable Lane Departure Warnings"),
|
||||
DESCRIPTIONS["IsLdwEnabled"],
|
||||
@@ -121,24 +106,6 @@ class TogglesLayout(Widget):
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
|
||||
self._accel_personality_enabled = toggle_item(
|
||||
lambda: tr("Enable Acceleration Profiles"),
|
||||
lambda: tr(DESCRIPTIONS["AccelPersonalityEnabled"]),
|
||||
self._params.get_bool("AccelPersonalityEnabled"),
|
||||
callback=self._set_accel_personality_enabled,
|
||||
icon="speed_limit.png",
|
||||
)
|
||||
|
||||
self._accel_personality_setting = multiple_button_item(
|
||||
lambda: tr("Acceleration Profile"),
|
||||
lambda: tr(DESCRIPTIONS["AccelPersonality"]),
|
||||
buttons=[lambda: tr("Eco"), lambda: tr("Normal"), lambda: tr("Sport")],
|
||||
button_width=300,
|
||||
callback=self._set_accel_personality,
|
||||
selected_index=self._params.get("AccelPersonality", return_default=True),
|
||||
icon="speed_limit.png"
|
||||
)
|
||||
|
||||
self._toggles = {}
|
||||
self._locked_toggles = set()
|
||||
for param, (title, desc, icon, needs_restart) in self._toggle_defs.items():
|
||||
@@ -168,11 +135,9 @@ class TogglesLayout(Widget):
|
||||
|
||||
self._toggles[param] = toggle
|
||||
|
||||
# insert longitudinal + acceleration personality after NDOG toggle
|
||||
# insert longitudinal personality after NDOG toggle
|
||||
if param == "DisengageOnAccelerator":
|
||||
self._toggles["LongitudinalPersonality"] = self._long_personality_setting
|
||||
self._toggles["AccelPersonalityEnabled"] = self._accel_personality_enabled
|
||||
self._toggles["AccelPersonality"] = self._accel_personality_setting
|
||||
|
||||
self._update_experimental_mode_icon()
|
||||
self._scroller = Scroller(list(self._toggles.values()), line_separator=True, spacing=0)
|
||||
@@ -211,15 +176,11 @@ class TogglesLayout(Widget):
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(True)
|
||||
self._toggles["ExperimentalMode"].set_description(e2e_description)
|
||||
self._long_personality_setting.action_item.set_enabled(True)
|
||||
self._accel_personality_enabled.action_item.set_enabled(True)
|
||||
self._accel_personality_setting.action_item.set_enabled(True)
|
||||
else:
|
||||
# no long for now
|
||||
self._toggles["ExperimentalMode"].action_item.set_enabled(False)
|
||||
self._toggles["ExperimentalMode"].action_item.set_state(False)
|
||||
self._long_personality_setting.action_item.set_enabled(False)
|
||||
self._accel_personality_enabled.action_item.set_enabled(False)
|
||||
self._accel_personality_setting.action_item.set_enabled(False)
|
||||
self._params.remove("ExperimentalMode")
|
||||
|
||||
unavailable = tr("Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control.")
|
||||
@@ -286,9 +247,3 @@ class TogglesLayout(Widget):
|
||||
|
||||
def _set_longitudinal_personality(self, button_index: int):
|
||||
self._params.put("LongitudinalPersonality", button_index, block=True)
|
||||
|
||||
def _set_accel_personality(self, button_index: int):
|
||||
self._params.put("AccelPersonality", button_index, block=True)
|
||||
|
||||
def _set_accel_personality_enabled(self, state: bool):
|
||||
self._params.put_bool("AccelPersonalityEnabled", state, block=True)
|
||||
|
||||
@@ -13,7 +13,6 @@ 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
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad import OnroadViewContainerSP as AugmentedRoadView
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
@@ -119,15 +118,13 @@ class MiciMainLayout(Scroller):
|
||||
|
||||
# FIXME: these two pops can interrupt user interacting in the settings
|
||||
if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY:
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._onroad_time_delay = None
|
||||
|
||||
# When car leaves standstill, pop nav stack and scroll to onroad
|
||||
CS = ui_state.sm["carState"]
|
||||
if not CS.standstill and self._prev_standstill:
|
||||
if not gui_app.sunnypilot_ui() or self._should_auto_scroll_to_onroad():
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout))
|
||||
self._prev_standstill = CS.standstill
|
||||
|
||||
def _on_interactive_timeout(self):
|
||||
|
||||
@@ -134,6 +134,11 @@ class SteeringLayout(Widget):
|
||||
|
||||
enforce_torque_enabled = self._torque_control_toggle.action_item.get_state()
|
||||
nnlc_enabled = self._nnlc_toggle.action_item.get_state()
|
||||
if enforce_torque_enabled and nnlc_enabled:
|
||||
self._torque_control_toggle.action_item.set_state(False)
|
||||
self._nnlc_toggle.action_item.set_state(False)
|
||||
enforce_torque_enabled = False
|
||||
nnlc_enabled = False
|
||||
self._nnlc_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not enforce_torque_enabled)
|
||||
self._torque_control_toggle.action_item.set_enabled(ui_state.is_offroad() and torque_allowed and not nnlc_enabled)
|
||||
self._torque_customization_button.action_item.set_enabled(self._torque_control_toggle.action_item.get_state())
|
||||
|
||||
@@ -51,17 +51,11 @@ class LaneChangeSettingsLayout(Widget):
|
||||
description=lambda: tr("Toggle to enable a delay timer for seamless lane changes when blind spot monitoring " +
|
||||
"(BSM) detects a obstructing vehicle, ensuring safe maneuvering."),
|
||||
)
|
||||
self._road_edge_block = toggle_item_sp(
|
||||
param="RoadEdgeLaneChangeEnabled",
|
||||
title=lambda: tr("Block Lane Change: Road Edge Detection"),
|
||||
description=lambda: tr("Blocks the lane change if the model sees a road edge on your signaled side."),
|
||||
)
|
||||
|
||||
items = [
|
||||
self._lane_change_timer,
|
||||
LineSeparatorSP(40),
|
||||
self._bsm_delay,
|
||||
self._road_edge_block,
|
||||
]
|
||||
|
||||
return items
|
||||
|
||||
@@ -1,13 +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.
|
||||
"""
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
|
||||
|
||||
class MiciMainLayoutSP(MiciMainLayout):
|
||||
def _should_auto_scroll_to_onroad(self) -> bool:
|
||||
return not self._onroad_layout.is_on_info_panel()
|
||||
@@ -1,63 +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 pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroller_sp import ScrollerSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.augmented_road_view import AugmentedRoadViewSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onroad_info_panel import OnroadInfoPanel
|
||||
|
||||
CONFIDENCE_BALL_VISIBLE_RATIO = 0.4
|
||||
HORIZONTAL_SETTLE_PX = 5
|
||||
HORIZONTAL_RESET_RATIO = 0.5
|
||||
|
||||
|
||||
class OnroadViewContainerSP(ScrollerSP):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__(horizontal=False, snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
|
||||
self.road_view = AugmentedRoadViewSP(bookmark_callback=bookmark_callback)
|
||||
self.onroad_info_panel = OnroadInfoPanel(bookmark_callback=bookmark_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self.road_view,
|
||||
self.onroad_info_panel,
|
||||
])
|
||||
self._scroller.set_reset_scroll_at_show(False)
|
||||
self._scroller.set_scrolling_enabled(lambda: abs(self.rect.x) < HORIZONTAL_SETTLE_PX)
|
||||
|
||||
for child in (self.road_view, self.onroad_info_panel):
|
||||
inner_touch_valid = child._touch_valid_callback
|
||||
child.set_touch_valid_callback(
|
||||
lambda inner=inner_touch_valid: self._touch_valid() and (inner() if inner else True)
|
||||
)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle):
|
||||
super().set_rect(rect)
|
||||
self.road_view.set_rect(rect)
|
||||
self.onroad_info_panel.set_rect(rect)
|
||||
return self
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self.road_view.is_swiping_left() or self.onroad_info_panel.is_swiping_left()
|
||||
|
||||
def set_click_callback(self, callback) -> None:
|
||||
self.road_view.set_click_callback(callback)
|
||||
self.onroad_info_panel.set_click_callback(callback)
|
||||
|
||||
def is_on_info_panel(self) -> bool:
|
||||
"""True when scrolled past halfway toward onroad_info_panel (used by main layout
|
||||
to skip auto-pop-back-to-camera while user is reading the info panel)."""
|
||||
return abs(self._scroller.scroll_panel.get_offset()) > self._rect.height / 2
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
if abs(self.rect.x) > gui_app.width * HORIZONTAL_RESET_RATIO:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
vertical_offset = self._scroller.scroll_panel.get_offset()
|
||||
show_ball = abs(vertical_offset) < rect.height * CONFIDENCE_BALL_VISIBLE_RATIO
|
||||
self.road_view.set_show_confidence_ball(show_ball)
|
||||
|
||||
super()._render(rect)
|
||||
@@ -1,324 +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 pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import MousePos
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import BookmarkIcon
|
||||
|
||||
METER_TO_KM = 0.001
|
||||
METER_TO_MILE = 0.000621371
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnroadInfoPanelColors:
|
||||
white: rl.Color = rl.WHITE
|
||||
black: rl.Color = rl.BLACK
|
||||
red: rl.Color = rl.Color(255, 0, 0, 255)
|
||||
green: rl.Color = rl.Color(0, 255, 0, 255)
|
||||
grey: rl.Color = rl.Color(190, 195, 190, 255)
|
||||
light_grey: rl.Color = rl.Color(200, 200, 200, 255)
|
||||
dark_grey: rl.Color = rl.Color(100, 100, 100, 255)
|
||||
bg_dark: rl.Color = rl.Color(0, 0, 0, 255)
|
||||
card_bg: rl.Color = rl.Color(50, 50, 50, 200)
|
||||
badge_bg: rl.Color = rl.Color(60, 60, 60, 255)
|
||||
|
||||
|
||||
COLORS = OnroadInfoPanelColors()
|
||||
|
||||
|
||||
class OnroadInfoPanel(Widget):
|
||||
def __init__(self, bookmark_callback=None):
|
||||
super().__init__()
|
||||
self.speed_limit: float = 0.0
|
||||
self.speed_limit_valid: bool = False
|
||||
self.speed_limit_offset: float = 0.0
|
||||
self.next_speed_limit: float = 0.0
|
||||
self.next_speed_limit_distance: float = 0.0
|
||||
self.road_name: str = ""
|
||||
self.current_speed: float = 0.0
|
||||
self.set_speed: float = 0.0
|
||||
self.cruise_enabled: bool = False
|
||||
|
||||
self._sign_slide: float = 0.0
|
||||
|
||||
self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
self._marquee_offset: float = 0.0
|
||||
self._marquee_direction: int = 1
|
||||
self._marquee_pause_timer: float = 0.0
|
||||
self._marquee_speed: float = 40.0
|
||||
self._marquee_pause_duration: float = 1.5
|
||||
|
||||
self._alert_renderer = AlertRenderer()
|
||||
self._alert_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps)
|
||||
|
||||
self._bookmark_icon = BookmarkIcon(bookmark_callback)
|
||||
|
||||
def is_swiping_left(self) -> bool:
|
||||
return self._bookmark_icon.is_swiping_left()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||
# Mirror stock AugmentedRoadView: suppress click while bookmark gesture active
|
||||
if not self._bookmark_icon.interacting():
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _update_state(self) -> None:
|
||||
sm = ui_state.sm
|
||||
speed_conv = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
|
||||
if sm.valid["longitudinalPlanSP"]:
|
||||
lp_sp = sm["longitudinalPlanSP"]
|
||||
resolver = lp_sp.speedLimit.resolver
|
||||
self.speed_limit = resolver.speedLimit * speed_conv
|
||||
self.speed_limit_valid = resolver.speedLimitValid
|
||||
self.speed_limit_offset = resolver.speedLimitOffset * speed_conv
|
||||
|
||||
if sm.valid["liveMapDataSP"]:
|
||||
lmd = sm["liveMapDataSP"]
|
||||
self.next_speed_limit = lmd.speedLimitAhead * speed_conv
|
||||
self.next_speed_limit_distance = lmd.speedLimitAheadDistance
|
||||
self.road_name = lmd.roadName
|
||||
|
||||
if sm.updated["carState"]:
|
||||
self.current_speed = sm["carState"].vEgo * speed_conv
|
||||
|
||||
if sm.valid["carState"] and sm.valid["controlsState"]:
|
||||
self.cruise_enabled = sm["carState"].cruiseState.enabled
|
||||
v_cruise_cluster = sm["carState"].vCruiseCluster
|
||||
set_speed_kph = sm["controlsState"].vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
self.set_speed = set_speed_kph * (METER_TO_MILE / METER_TO_KM) if not ui_state.is_metric else set_speed_kph
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
self._update_state()
|
||||
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), COLORS.bg_dark)
|
||||
margin = 20
|
||||
mid_y = rect.y + rect.height / 2
|
||||
|
||||
left_x = rect.x + margin
|
||||
|
||||
if self.cruise_enabled:
|
||||
unit = tr("MAX")
|
||||
display_speed = self.set_speed
|
||||
else:
|
||||
unit = tr("km/h") if ui_state.is_metric else tr("MPH")
|
||||
display_speed = self.current_speed
|
||||
|
||||
speed_val = str(round(display_speed))
|
||||
if self.speed_limit_valid and display_speed > self.speed_limit:
|
||||
speed_color = COLORS.red
|
||||
else:
|
||||
speed_color = COLORS.white
|
||||
|
||||
rl.draw_text_ex(self._font_semi_bold, unit, rl.Vector2(left_x, mid_y - 95), 38, 0, COLORS.grey)
|
||||
rl.draw_text_ex(self._font_bold, speed_val, rl.Vector2(left_x, mid_y - 60), 110, 0, speed_color)
|
||||
|
||||
sign_width = 135
|
||||
sign_height = 135 if ui_state.is_metric else 175
|
||||
|
||||
has_next = self.next_speed_limit > 0 and self.next_speed_limit != self.speed_limit
|
||||
target_slide = 1.0 if has_next else 0.0
|
||||
slide_speed = 3.0 * rl.get_frame_time()
|
||||
if self._sign_slide < target_slide:
|
||||
self._sign_slide = min(self._sign_slide + slide_speed, target_slide)
|
||||
elif self._sign_slide > target_slide:
|
||||
self._sign_slide = max(self._sign_slide - slide_speed, target_slide)
|
||||
|
||||
next_w = int(sign_width * 0.7)
|
||||
next_h = int(sign_height * 0.7)
|
||||
next_peek = int(next_w * 0.85) + 5
|
||||
centered_x = rect.x + rect.width - sign_width - margin
|
||||
shifted_x = rect.x + rect.width - sign_width - margin - next_peek
|
||||
sign_x = centered_x + (shifted_x - centered_x) * self._sign_slide
|
||||
sign_y = rect.y + (rect.height - sign_height) / 2
|
||||
|
||||
road_y = mid_y + 55
|
||||
road_width = sign_x - left_x - margin
|
||||
self._draw_road_name(left_x, road_y, road_width)
|
||||
|
||||
if has_next and self._sign_slide > 0.01:
|
||||
next_val = str(round(self.next_speed_limit))
|
||||
dist_str = self._format_distance(self.next_speed_limit_distance)
|
||||
next_x = sign_x + sign_width - int(next_w * 0.15)
|
||||
next_y = sign_y + (sign_height - next_h) / 2
|
||||
|
||||
next_speed_color = COLORS.black
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
|
||||
else:
|
||||
self._draw_mutcd_sign(next_x, next_y, next_w, next_h, next_val, next_speed_color, is_upcoming=True)
|
||||
|
||||
dist_size = measure_text_cached(self._font_medium, dist_str, 24)
|
||||
rl.draw_text_ex(self._font_medium, dist_str, rl.Vector2(next_x + next_w / 2 - dist_size.x / 2, next_y + next_h + 4), 24, 0, COLORS.grey)
|
||||
|
||||
self._draw_speed_limit_sign(sign_x, sign_y, sign_width, sign_height)
|
||||
|
||||
if self.speed_limit_offset != 0 and self.speed_limit_valid:
|
||||
offset_val = str(abs(round(self.speed_limit_offset)))
|
||||
badge_sz = 42
|
||||
badge_x = sign_x + sign_width - badge_sz * 0.85
|
||||
badge_y = sign_y - badge_sz * 0.25
|
||||
|
||||
if ui_state.is_metric:
|
||||
badge_r = badge_sz / 2
|
||||
badge_cx = badge_x + badge_r
|
||||
badge_cy = badge_y + badge_r
|
||||
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r + 2, COLORS.dark_grey)
|
||||
rl.draw_circle(int(badge_cx), int(badge_cy), badge_r, COLORS.badge_bg)
|
||||
self._draw_text_centered(self._font_bold, offset_val, 24, rl.Vector2(badge_cx, badge_cy), COLORS.white)
|
||||
else:
|
||||
mutcd_badge_x = sign_x + sign_width - badge_sz * 0.65
|
||||
mutcd_badge_y = sign_y - badge_sz * 0.50
|
||||
badge_rect = rl.Rectangle(mutcd_badge_x, mutcd_badge_y, badge_sz, badge_sz)
|
||||
rl.draw_rectangle_rounded(badge_rect, 0.25, 10, COLORS.badge_bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.25, 10, 2, COLORS.dark_grey)
|
||||
self._draw_text_centered(self._font_bold, offset_val, 24, rl.Vector2(mutcd_badge_x + badge_sz / 2, mutcd_badge_y + badge_sz / 2), COLORS.white)
|
||||
|
||||
# SCC
|
||||
speed_size = measure_text_cached(self._font_bold, speed_val, 110)
|
||||
scc_x = left_x + speed_size.x + 30
|
||||
scc_y = mid_y - 50
|
||||
self._draw_scc_icons(scc_x, scc_y)
|
||||
|
||||
self._bookmark_icon.render(rect)
|
||||
|
||||
if ui_state.started:
|
||||
alert_obj, no_alert = self._alert_renderer.will_render()
|
||||
self._alert_alpha_filter.update(0 if no_alert else 1)
|
||||
alpha = self._alert_alpha_filter.x
|
||||
if alpha > 0.01:
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, int(150 * alpha)))
|
||||
self._alert_renderer.render(rect)
|
||||
|
||||
def _draw_scc_icons(self, x: float, y: float) -> None:
|
||||
sm = ui_state.sm
|
||||
if not sm.valid["longitudinalPlanSP"]:
|
||||
return
|
||||
scc = sm["longitudinalPlanSP"].smartCruiseControl
|
||||
|
||||
box_w, box_h = 100, 36
|
||||
gap = 6
|
||||
drawn = 0
|
||||
|
||||
for label, active in [("SCC-V", scc.vision.active), ("SCC-M", scc.map.active)]:
|
||||
if not active:
|
||||
continue
|
||||
bx = x
|
||||
by = y + drawn * (box_h + gap)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(bx, by, box_w, box_h), 0.3, 10, COLORS.green)
|
||||
self._draw_text_centered(self._font_bold, label, 20, rl.Vector2(bx + box_w / 2, by + box_h / 2), COLORS.black)
|
||||
drawn += 1
|
||||
|
||||
def _draw_speed_limit_sign(self, x: float, y: float, sign_width: float, sign_height: float) -> None:
|
||||
speed_str = str(round(self.speed_limit)) if self.speed_limit_valid and self.speed_limit > 0 else "--"
|
||||
speed_color = COLORS.black if not self.speed_limit_valid or self.current_speed <= self.speed_limit else COLORS.red
|
||||
|
||||
if ui_state.is_metric:
|
||||
self._draw_vienna_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
else:
|
||||
self._draw_mutcd_sign(x, y, sign_width, sign_height, speed_str, speed_color, is_upcoming=False)
|
||||
|
||||
def _draw_road_name(self, x: float, y: float, width: float) -> None:
|
||||
road_display = self.road_name if self.road_name else "--"
|
||||
font_size = 30
|
||||
road_size = measure_text_cached(self._font_semi_bold, road_display, font_size)
|
||||
text_width = road_size.x
|
||||
|
||||
if text_width <= width:
|
||||
self._marquee_offset = 0.0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = 0.0
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, rl.Vector2(x, y), font_size, 0, COLORS.white)
|
||||
else:
|
||||
overflow = text_width - width
|
||||
dt = rl.get_frame_time()
|
||||
|
||||
if self._marquee_pause_timer > 0:
|
||||
self._marquee_pause_timer -= dt
|
||||
else:
|
||||
self._marquee_offset += self._marquee_direction * self._marquee_speed * dt
|
||||
|
||||
if self._marquee_offset >= overflow:
|
||||
self._marquee_offset = overflow
|
||||
self._marquee_direction = -1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
elif self._marquee_offset <= 0:
|
||||
self._marquee_offset = 0
|
||||
self._marquee_direction = 1
|
||||
self._marquee_pause_timer = self._marquee_pause_duration
|
||||
|
||||
rl.begin_scissor_mode(int(x), int(y), int(width), int(road_size.y + 4))
|
||||
text_pos = rl.Vector2(x - self._marquee_offset, y)
|
||||
rl.draw_text_ex(self._font_semi_bold, road_display, text_pos, font_size, 0, COLORS.white)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
def _draw_vienna_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
center = rl.Vector2(x + width / 2, y + height / 2)
|
||||
outer_radius = min(width, height) / 2
|
||||
|
||||
rl.draw_circle_v(center, outer_radius, COLORS.white)
|
||||
ring_width = outer_radius * 0.18
|
||||
rl.draw_ring(center, outer_radius - ring_width, outer_radius, 0, 360, 36, COLORS.red)
|
||||
|
||||
font_size = outer_radius * (0.7 if len(speed_str) >= 3 else 0.9)
|
||||
text_size = measure_text_cached(self._font_bold, speed_str, int(font_size))
|
||||
text_pos = rl.Vector2(center.x - text_size.x / 2, center.y - text_size.y / 2)
|
||||
rl.draw_text_ex(self._font_bold, speed_str, text_pos, font_size, 0, speed_color)
|
||||
|
||||
def _draw_mutcd_sign(self, x: float, y: float, width: float, height: float, speed_str: str, speed_color: rl.Color, is_upcoming: bool = False) -> None:
|
||||
sign_rect = rl.Rectangle(x, y, width, height)
|
||||
rl.draw_rectangle_rounded(sign_rect, 0.35, 10, COLORS.white)
|
||||
|
||||
inset = max(4, width * 0.05)
|
||||
inner_rect = rl.Rectangle(x + inset, y + inset, width - inset * 2, height - inset * 2)
|
||||
outer_radius = 0.35 * width / 2.0
|
||||
inner_radius = outer_radius - inset
|
||||
inner_roundness = inner_radius / (inner_rect.width / 2.0)
|
||||
rl.draw_rectangle_rounded_lines_ex(inner_rect, inner_roundness, 10, 3, COLORS.black)
|
||||
|
||||
mid_x = x + width / 2
|
||||
label_size = max(18, int(width * 0.26))
|
||||
if is_upcoming:
|
||||
self._draw_text_centered(self._font_bold, tr("AHEAD"), label_size, rl.Vector2(mid_x, y + height * 0.27), COLORS.black)
|
||||
else:
|
||||
self._draw_text_centered(self._font_bold, tr("SPEED"), label_size, rl.Vector2(mid_x, y + height * 0.20), COLORS.black)
|
||||
self._draw_text_centered(self._font_bold, tr("LIMIT"), label_size, rl.Vector2(mid_x, y + height * 0.40), COLORS.black)
|
||||
|
||||
speed_font_size = int(width * 0.52) if len(speed_str) >= 3 else int(width * 0.62)
|
||||
self._draw_text_centered(self._font_bold, speed_str, speed_font_size, rl.Vector2(mid_x, y + height * 0.72), speed_color)
|
||||
|
||||
def _draw_text_centered(self, font, text, size, pos_center, color):
|
||||
sz = measure_text_cached(font, text, size)
|
||||
rl.draw_text_ex(font, text, rl.Vector2(pos_center.x - sz.x / 2, pos_center.y - sz.y / 2), size, 0, color)
|
||||
|
||||
def _format_distance(self, distance: float) -> str:
|
||||
if ui_state.is_metric:
|
||||
if distance < 50:
|
||||
return tr("Near")
|
||||
if distance >= 1000:
|
||||
return f"{distance * METER_TO_KM:.1f}" + tr("km")
|
||||
if distance < 200:
|
||||
rounded = max(10, int(distance / 10) * 10)
|
||||
else:
|
||||
rounded = int(distance / 100) * 100
|
||||
return str(rounded) + tr("m")
|
||||
else:
|
||||
distance_mi = distance * METER_TO_MILE
|
||||
if distance_mi < 0.1:
|
||||
return tr("Near")
|
||||
return f"{distance_mi:.1f}" + tr("mi")
|
||||
@@ -1,30 +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 pyray as rl
|
||||
from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView
|
||||
|
||||
|
||||
class _SuppressedConfidenceBall:
|
||||
def render(self, *_):
|
||||
pass
|
||||
|
||||
|
||||
class AugmentedRoadViewSP(AugmentedRoadView):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._show_confidence_ball: bool = True
|
||||
self._real_confidence_ball = self._confidence_ball
|
||||
self._confidence_ball = _SuppressedConfidenceBall()
|
||||
|
||||
def set_show_confidence_ball(self, show: bool) -> None:
|
||||
self._show_confidence_ball = show
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
super()._render(rect)
|
||||
if self._show_confidence_ball:
|
||||
self._real_confidence_ball.render(self.rect)
|
||||
@@ -1,34 +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 pyray as rl
|
||||
from openpilot.system.ui.lib.application import MouseEvent
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
|
||||
|
||||
|
||||
class GuiScrollPanel2SP(GuiScrollPanel2):
|
||||
"""Reject orthogonal-dominant drags so nested scrollers (outer horizontal +
|
||||
inner vertical) don't both engage on a slightly diagonal swipe.
|
||||
|
||||
Implemented as a post-super state rollback rather than reimplementing the
|
||||
PRESSED state machine — keeps stock behaviour authoritative."""
|
||||
|
||||
def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, bounds_size: float,
|
||||
content_size: float) -> None:
|
||||
pre_state = self._state
|
||||
super()._handle_mouse_event(mouse_event, bounds, bounds_size, content_size)
|
||||
|
||||
if self._state == ScrollState.MANUAL_SCROLL and pre_state == ScrollState.PRESSED and \
|
||||
self._initial_click_event is not None:
|
||||
diff_x = abs(mouse_event.pos.x - self._initial_click_event.pos.x)
|
||||
diff_y = abs(mouse_event.pos.y - self._initial_click_event.pos.y)
|
||||
along = diff_x if self._horizontal else diff_y
|
||||
anti = diff_y if self._horizontal else diff_x
|
||||
if anti > along:
|
||||
self._state = ScrollState.STEADY
|
||||
self._velocity = 0.0
|
||||
self._velocity_buffer.clear()
|
||||
@@ -1,16 +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.
|
||||
"""
|
||||
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.scroll_panel_sp import GuiScrollPanel2SP
|
||||
|
||||
|
||||
class ScrollerSP(Scroller):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
inner = self._scroller
|
||||
inner.scroll_panel = GuiScrollPanel2SP(inner._horizontal, handle_out_of_bounds=not inner._snap_items)
|
||||
@@ -179,6 +179,10 @@ class UIStateSP:
|
||||
CP = self.CP
|
||||
|
||||
if CP is not None:
|
||||
if self.params.get_bool("EnforceTorqueControl") and self.params.get_bool("NeuralNetworkLateralControl"):
|
||||
self.params.put_bool("EnforceTorqueControl", False, block=True)
|
||||
self.params.put_bool("NeuralNetworkLateralControl", False, block=True)
|
||||
|
||||
# Angle steering: no torque-based lateral controls
|
||||
if CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||
self.params.remove("EnforceTorqueControl")
|
||||
|
||||
@@ -10,9 +10,6 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.main import MiciMainLayoutSP as MiciMainLayout
|
||||
|
||||
BIG_UI = gui_app.big_ui()
|
||||
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
n_frames = img_shape[1] // 6
|
||||
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
|
||||
|
||||
npy_keys = {}
|
||||
numpy_keys = {}
|
||||
queue_keys = {}
|
||||
|
||||
for key, shape in input_shapes.items():
|
||||
@@ -196,7 +196,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
continue
|
||||
if len(shape) == 3 and shape[1] > 1:
|
||||
if key.startswith('desire'):
|
||||
npy_keys[key] = np.zeros(shape[2], dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
|
||||
queue_keys[f'{key}_q'] = Tensor(
|
||||
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
|
||||
device=device).contiguous().realize()
|
||||
@@ -205,24 +205,24 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
|
||||
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
|
||||
device=device).contiguous().realize()
|
||||
else:
|
||||
npy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
elif len(shape) == 2:
|
||||
npy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
|
||||
|
||||
if 'traffic_convention' not in npy_keys:
|
||||
if 'traffic_convention' not in numpy_keys:
|
||||
tc_shape = input_shapes.get('traffic_convention', (1, 2))
|
||||
npy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
|
||||
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
|
||||
|
||||
npy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
npy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
numpy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
|
||||
|
||||
input_queues = {
|
||||
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
|
||||
**queue_keys,
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in npy_keys.items()},
|
||||
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
|
||||
}
|
||||
return input_queues, npy_keys
|
||||
return input_queues, numpy_keys
|
||||
|
||||
|
||||
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h,
|
||||
|
||||
@@ -40,7 +40,6 @@ from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelp
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||
|
||||
@@ -63,11 +62,6 @@ def _find_driving_pkl(bundle):
|
||||
if _pkl_exists(pkl_path):
|
||||
return pkl_path
|
||||
|
||||
fallback = os.path.join(model_root, 'driving_tinygrad.pkl')
|
||||
if _pkl_exists(fallback):
|
||||
return fallback
|
||||
return None
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
frame_id: int = 0
|
||||
@@ -126,7 +120,7 @@ class ModelState(ModelStateBase):
|
||||
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
|
||||
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
|
||||
self.input_queues, self.npy = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
|
||||
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
|
||||
else:
|
||||
vision_metadata = metadata['vision']
|
||||
policy_keys = [k for k in metadata if k != 'vision']
|
||||
@@ -144,7 +138,7 @@ class ModelState(ModelStateBase):
|
||||
policy_input_shapes = first_policy_metadata['input_shapes']
|
||||
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
|
||||
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
|
||||
self.input_queues, self.npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
|
||||
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
|
||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
|
||||
@@ -184,7 +178,7 @@ class ModelState(ModelStateBase):
|
||||
|
||||
@property
|
||||
def desire_key(self) -> str:
|
||||
return next(k for k in self.npy if k.startswith('desire'))
|
||||
return next(k for k in self.numpy_inputs if k.startswith('desire'))
|
||||
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
|
||||
@@ -200,16 +194,16 @@ class ModelState(ModelStateBase):
|
||||
|
||||
desire_key = self.desire_key
|
||||
inputs[desire_key][0] = 0
|
||||
self.npy[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
|
||||
self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
|
||||
self.prev_desire[:] = inputs[desire_key]
|
||||
for key in ('traffic_convention', 'lateral_control_params'):
|
||||
if key in self.npy and key in inputs:
|
||||
self.npy[key][:] = inputs[key]
|
||||
if key in self.numpy_inputs and key in inputs:
|
||||
self.numpy_inputs[key][:] = inputs[key]
|
||||
|
||||
road_key = next(n for n in bufs if 'big' not in n)
|
||||
wide_key = next(n for n in bufs if 'big' in n)
|
||||
self.npy['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
||||
self.npy['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
||||
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
|
||||
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
|
||||
|
||||
if prepare_only:
|
||||
self._warp_enqueue(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
|
||||
@@ -237,8 +231,8 @@ class ModelState(ModelStateBase):
|
||||
if 'planplus' in outputs and 'plan' in outputs:
|
||||
outputs['plan'] = outputs['plan'] + outputs['planplus']
|
||||
|
||||
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.npy:
|
||||
buf = self.npy['prev_desired_curv']
|
||||
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.numpy_inputs:
|
||||
buf = self.numpy_inputs['prev_desired_curv']
|
||||
buf[0, :-1] = buf[0, 1:]
|
||||
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
|
||||
|
||||
@@ -330,7 +324,6 @@ def main(demo=False):
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
|
||||
DH = DesireHelper()
|
||||
RELC = RoadEdgeLaneChangeController(DH)
|
||||
meta_constants = load_meta_constants()
|
||||
|
||||
while True:
|
||||
@@ -411,7 +404,7 @@ def main(demo=False):
|
||||
'traffic_convention': traffic_convention,
|
||||
}
|
||||
|
||||
if 'lateral_control_params' in model.npy:
|
||||
if 'lateral_control_params' in model.numpy_inputs:
|
||||
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
|
||||
|
||||
mt1 = time.perf_counter()
|
||||
@@ -435,10 +428,7 @@ def main(demo=False):
|
||||
l_lane_change_prob = desire_state[log.Desire.laneChangeLeft]
|
||||
r_lane_change_prob = desire_state[log.Desire.laneChangeRight]
|
||||
lane_change_prob = l_lane_change_prob + r_lane_change_prob
|
||||
RELC.update(modelv2_send.modelV2.roadEdgeStds, modelv2_send.modelV2.laneLineProbs, v_ego, modelv2_send.modelV2.roadEdges)
|
||||
mdv2sp_send.modelDataV2SP.leftLaneChangeEdgeBlock = RELC.left_edge_detected
|
||||
mdv2sp_send.modelDataV2SP.rightLaneChangeEdgeBlock = RELC.right_edge_detected
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, RELC.left_edge_detected, RELC.right_edge_detected)
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
## Neural networks in openpilot
|
||||
To view the architecture of the ONNX networks, you can use [netron](https://netron.app/)
|
||||
|
||||
## Supercombo
|
||||
### Supercombo input format (Full size: 799906 x float32)
|
||||
* **image stream**
|
||||
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
|
||||
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
|
||||
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
|
||||
* Channel 4 represents the half-res U channel
|
||||
* Channel 5 represents the half-res V channel
|
||||
* **wide image stream**
|
||||
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
|
||||
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
|
||||
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
|
||||
* Channel 4 represents the half-res U channel
|
||||
* Channel 5 represents the half-res V channel
|
||||
* **desire**
|
||||
* one-hot encoded buffer to command model to execute certain actions, bit needs to be sent for the past 5 seconds (at 20FPS) : 100 * 8
|
||||
* **traffic convention**
|
||||
* one-hot encoded vector to tell model whether traffic is right-hand or left-hand traffic : 2
|
||||
* **feature buffer**
|
||||
* A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512
|
||||
|
||||
|
||||
### Supercombo output format (Full size: XXX x float32)
|
||||
Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more.
|
||||
|
||||
|
||||
## Driver Monitoring Model
|
||||
* .onnx model can be run with onnx runtimes
|
||||
* .dlc file is a pre-quantized model and only runs on qualcomm DSPs
|
||||
|
||||
### input format
|
||||
* single image W = 1440 H = 960 luminance channel (Y) from the planar YUV420 format:
|
||||
* full input size is 1440 * 960 = 1382400
|
||||
* normalized ranging from 0.0 to 1.0 in float32 (onnx runner) or ranging from 0 to 255 in uint8 (snpe runner)
|
||||
* camera calibration angles (roll, pitch, yaw) from liveCalibration: 3 x float32 inputs
|
||||
|
||||
### output format
|
||||
* 84 x float32 outputs = 2 + 41 * 2 ([parsing example](https://github.com/commaai/openpilot/blob/22ce4e17ba0d3bfcf37f8255a4dd1dc683fe0c38/selfdrive/modeld/models/dmonitoring.cc#L33))
|
||||
* for each person in the front seats (2 * 41)
|
||||
* face pose: 12 = 6 + 6
|
||||
* face orientation [pitch, yaw, roll] in camera frame: 3
|
||||
* face position [dx, dy] relative to image center: 2
|
||||
* normalized face size: 1
|
||||
* standard deviations for above outputs: 6
|
||||
* face visible probability: 1
|
||||
* eyes: 20 = (8 + 1) + (8 + 1) + 1 + 1
|
||||
* eye position and size, and their standard deviations: 8
|
||||
* eye visible probability: 1
|
||||
* eye closed probability: 1
|
||||
* wearing sunglasses probability: 1
|
||||
* face occluded probability: 1
|
||||
* touching wheel probability: 1
|
||||
* paying attention probability: 1
|
||||
* (deprecated) distracted probabilities: 2
|
||||
* using phone probability: 1
|
||||
* distracted probability: 1
|
||||
* common outputs 2
|
||||
* poor camera vision probability: 1
|
||||
* left hand drive probability: 1
|
||||
@@ -1,101 +0,0 @@
|
||||
// clang++ -O2 repro.cc && ./a.out
|
||||
|
||||
#include <sched.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
|
||||
static inline double millis_since_boot() {
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_BOOTTIME, &t);
|
||||
return t.tv_sec * 1000.0 + t.tv_nsec * 1e-6;
|
||||
}
|
||||
|
||||
#define MODEL_WIDTH 320
|
||||
#define MODEL_HEIGHT 640
|
||||
|
||||
// null function still breaks it
|
||||
#define input_lambda(x) x
|
||||
|
||||
// this is copied from models/dmonitoring.cc, and is the code that triggers the issue
|
||||
void inner(uint8_t *resized_buf, float *net_input_buf) {
|
||||
int resized_width = MODEL_WIDTH;
|
||||
int resized_height = MODEL_HEIGHT;
|
||||
|
||||
// one shot conversion, O(n) anyway
|
||||
// yuvframe2tensor, normalize
|
||||
for (int r = 0; r < MODEL_HEIGHT/2; r++) {
|
||||
for (int c = 0; c < MODEL_WIDTH/2; c++) {
|
||||
// Y_ul
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r] = input_lambda(resized_buf[(2*r*resized_width) + (2*c)]);
|
||||
// Y_ur
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width) + (2*c+1)]);
|
||||
// Y_dl
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + ((MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c)]);
|
||||
// Y_dr
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c+1)]);
|
||||
// U
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + (r*resized_width/2) + c]);
|
||||
// V
|
||||
net_input_buf[(c*MODEL_HEIGHT/2) + r + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + ((resized_width/2)*(resized_height/2)) + (r*resized_width/2) + c]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float trial() {
|
||||
int resized_width = MODEL_WIDTH;
|
||||
int resized_height = MODEL_HEIGHT;
|
||||
|
||||
int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v
|
||||
|
||||
// allocate the buffers
|
||||
uint8_t *resized_buf = (uint8_t*)malloc(resized_width*resized_height*3/2);
|
||||
float *net_input_buf = (float*)malloc(yuv_buf_len*sizeof(float));
|
||||
printf("allocate -- %p 0x%x -- %p 0x%lx\n", resized_buf, resized_width*resized_height*3/2, net_input_buf, yuv_buf_len*sizeof(float));
|
||||
|
||||
// test for bad buffers
|
||||
static int CNT = 20;
|
||||
float avg = 0.0;
|
||||
for (int i = 0; i < CNT; i++) {
|
||||
double s4 = millis_since_boot();
|
||||
inner(resized_buf, net_input_buf);
|
||||
double s5 = millis_since_boot();
|
||||
avg += s5-s4;
|
||||
}
|
||||
avg /= CNT;
|
||||
|
||||
// once it's bad, it's reliably bad
|
||||
if (avg > 10) {
|
||||
printf("HIT %f\n", avg);
|
||||
printf("BAD\n");
|
||||
|
||||
for (int i = 0; i < 200; i++) {
|
||||
double s4 = millis_since_boot();
|
||||
inner(resized_buf, net_input_buf);
|
||||
double s5 = millis_since_boot();
|
||||
printf("%.2f ", s5-s4);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// don't free so we get a different buffer each time
|
||||
//free(resized_buf);
|
||||
//free(net_input_buf);
|
||||
|
||||
return avg;
|
||||
}
|
||||
|
||||
int main() {
|
||||
while (true) {
|
||||
float ret = trial();
|
||||
printf("got %f\n", ret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,6 @@ class TestFindDrivingPkl:
|
||||
assert result is not None
|
||||
assert 'driving_fof_tinygrad.pkl' in result
|
||||
|
||||
def test_finds_fallback_driving_tinygrad(self, tmp_path, monkeypatch):
|
||||
(tmp_path / 'driving_tinygrad.pkl').write_bytes(b'fake')
|
||||
from openpilot.system.hardware import hw
|
||||
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
|
||||
|
||||
bundle = DummyBundle(models=[DummyModel('vision', 'nonexistent.pkl')])
|
||||
result = _find_driving_pkl(bundle)
|
||||
assert result is not None
|
||||
assert 'driving_tinygrad.pkl' in result
|
||||
|
||||
|
||||
# Init — assertion guard
|
||||
|
||||
@@ -84,8 +74,8 @@ class TestStockEquivalence:
|
||||
skip_keys = {'action_t'}
|
||||
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
|
||||
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
|
||||
assert set(state.npy.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||
f"Npy keys differ: v2={set(state.npy.keys())}, stock={set(stock_npy.keys())}"
|
||||
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
|
||||
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
|
||||
|
||||
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
|
||||
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
|
||||
@@ -188,16 +178,16 @@ class TestInputQueueCreation:
|
||||
def test_npy_contains_transforms(self, archetype_name, model_state_factory):
|
||||
arch = ARCHETYPES[archetype_name]
|
||||
state = model_state_factory(arch)
|
||||
assert 'tfm' in state.npy, f"{arch.name}: 'tfm' missing from npy"
|
||||
assert 'big_tfm' in state.npy, f"{arch.name}: 'big_tfm' missing from npy"
|
||||
assert state.npy['tfm'].shape == (3, 3)
|
||||
assert state.npy['big_tfm'].shape == (3, 3)
|
||||
assert 'tfm' in state.numpy_inputs, f"{arch.name}: 'tfm' missing from npy"
|
||||
assert 'big_tfm' in state.numpy_inputs, f"{arch.name}: 'big_tfm' missing from npy"
|
||||
assert state.numpy_inputs['tfm'].shape == (3, 3)
|
||||
assert state.numpy_inputs['big_tfm'].shape == (3, 3)
|
||||
|
||||
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
|
||||
def test_npy_contains_desire(self, archetype_name, model_state_factory):
|
||||
arch = ARCHETYPES[archetype_name]
|
||||
state = model_state_factory(arch)
|
||||
assert arch.expected_desire_key in state.npy, \
|
||||
assert arch.expected_desire_key in state.numpy_inputs, \
|
||||
f"{arch.name}: '{arch.expected_desire_key}' missing from npy"
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
clang++ -I /home/batman/one/external/tensorflow/include/ -L /home/batman/one/external/tensorflow/lib -Wl,-rpath=/home/batman/one/external/tensorflow/lib main.cc -ltensorflow
|
||||
@@ -1,69 +0,0 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include "tensorflow/c/c_api.h"
|
||||
|
||||
void* read_file(const char* path, size_t* out_len) {
|
||||
FILE* f = fopen(path, "r");
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
long f_len = ftell(f);
|
||||
rewind(f);
|
||||
|
||||
char* buf = (char*)calloc(f_len, 1);
|
||||
assert(buf);
|
||||
|
||||
size_t num_read = fread(buf, f_len, 1, f);
|
||||
fclose(f);
|
||||
|
||||
if (num_read != 1) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (out_len) {
|
||||
*out_len = f_len;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void DeallocateBuffer(void* data, size_t) {
|
||||
free(data);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
TF_Buffer* buf;
|
||||
TF_Graph* graph;
|
||||
TF_Status* status;
|
||||
char *path = argv[1];
|
||||
|
||||
// load model
|
||||
{
|
||||
size_t model_size;
|
||||
char tmp[1024];
|
||||
snprintf(tmp, sizeof(tmp), "%s.pb", path);
|
||||
printf("loading model %s\n", tmp);
|
||||
uint8_t *model_data = (uint8_t *)read_file(tmp, &model_size);
|
||||
buf = TF_NewBuffer();
|
||||
buf->data = model_data;
|
||||
buf->length = model_size;
|
||||
buf->data_deallocator = DeallocateBuffer;
|
||||
printf("loaded model of size %d\n", model_size);
|
||||
}
|
||||
|
||||
// import graph
|
||||
status = TF_NewStatus();
|
||||
graph = TF_NewGraph();
|
||||
TF_ImportGraphDefOptions *opts = TF_NewImportGraphDefOptions();
|
||||
TF_GraphImportGraphDef(graph, buf, opts, status);
|
||||
TF_DeleteImportGraphDefOptions(opts);
|
||||
TF_DeleteBuffer(buf);
|
||||
if (TF_GetCode(status) != TF_OK) {
|
||||
printf("FAIL: %s\n", TF_Message(status));
|
||||
} else {
|
||||
printf("SUCCESS\n");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import tensorflow as tf
|
||||
|
||||
with open(sys.argv[1], "rb") as f:
|
||||
graph_def = tf.compat.v1.GraphDef()
|
||||
graph_def.ParseFromString(f.read())
|
||||
#tf.io.write_graph(graph_def, '', sys.argv[1]+".try")
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
|
||||
|
||||
N = int(os.getenv("N", "5"))
|
||||
TIME = int(os.getenv("TIME", "30"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sock = messaging.sub_sock('modelV2', conflate=False, timeout=1000)
|
||||
|
||||
execution_times = []
|
||||
|
||||
for _ in range(N):
|
||||
os.environ['LOGPRINT'] = 'debug'
|
||||
managed_processes['modeld'].start()
|
||||
time.sleep(5)
|
||||
|
||||
t = []
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < TIME:
|
||||
msgs = messaging.drain_sock(sock, wait_for_one=True)
|
||||
for m in msgs:
|
||||
t.append(m.modelV2.modelExecutionTime)
|
||||
|
||||
execution_times.append(np.array(t[10:]) * 1000)
|
||||
managed_processes['modeld'].stop()
|
||||
|
||||
print("\n\n")
|
||||
print(f"ran modeld {N} times for {TIME}s each")
|
||||
for _, t in enumerate(execution_times):
|
||||
print(f"\tavg: {sum(t)/len(t):0.2f}ms, min: {min(t):0.2f}ms, max: {max(t):0.2f}ms")
|
||||
print("\n\n")
|
||||
+117
-85
@@ -6,80 +6,138 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from cereal import custom
|
||||
from openpilot.sunnypilot.models.constants import Meta, MetaTombRaider, MetaSimPose
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from pathlib import Path
|
||||
|
||||
# see the README.md for more details on the model selector versioning
|
||||
CURRENT_SELECTOR_VERSION = 15
|
||||
REQUIRED_MIN_SELECTOR_VERSION = 14
|
||||
|
||||
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
|
||||
REQUIRED_JSON_VERSION = 15
|
||||
|
||||
CUSTOM_MODEL_PATH = Paths.model_root()
|
||||
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
|
||||
|
||||
ModelManager = custom.ModelManagerSP
|
||||
_LAST_VALIDATED_RAW = None
|
||||
|
||||
|
||||
def _compute_hash(file_path: str) -> str | None:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
try:
|
||||
return hashlib.sha256(read_file_chunked(file_path)).hexdigest().lower()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
async def verify_file(file_path: str, expected_hash: str) -> bool:
|
||||
from openpilot.common.file_chunker import read_file_chunked
|
||||
try:
|
||||
data = read_file_chunked(file_path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
return hashlib.sha256(data).hexdigest().lower() == expected_hash.lower()
|
||||
file_hash = _compute_hash(file_path)
|
||||
return file_hash == expected_hash.lower() if file_hash else False
|
||||
|
||||
|
||||
def _verify_file(file_path: str, expected_hash: str) -> bool:
|
||||
file_hash = _compute_hash(file_path)
|
||||
return file_hash == expected_hash.lower() if file_hash else False
|
||||
|
||||
|
||||
def is_bundle_version_compatible(bundle: dict) -> bool:
|
||||
"""
|
||||
Checks whether the model bundle is compatible with the current selector version constraints.
|
||||
|
||||
The bundle specifies a `minimum_selector_version`, which defines the minimum selector version
|
||||
The bundle parsed from the json specifies a `minimum_selector_version`, which defines the minimum selector version
|
||||
required to load the model. This function ensures that:
|
||||
|
||||
1. The model is not too old: the bundle must require at least `REQUIRED_MIN_SELECTOR_VERSION`.
|
||||
2. The model is not too new: it must support the current selector version (`CURRENT_SELECTOR_VERSION`).
|
||||
|
||||
This allows the selector to enforce both a minimum and maximum range of supported models,
|
||||
even if a model would otherwise be compatible.
|
||||
|
||||
:param bundle: Dictionary containing `minimum_selector_version`, as defined by the model bundle.
|
||||
:type bundle: Dict
|
||||
:return: True if the selector version is within the accepted range for the bundle; otherwise False.
|
||||
:rtype: Bool
|
||||
the bundle MUST match the `REQUIRED_JSON_VERSION` set here in helpers.
|
||||
"""
|
||||
return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION)
|
||||
return bundle.get("minimumSelectorVersion", 0) == REQUIRED_JSON_VERSION
|
||||
|
||||
|
||||
def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle:
|
||||
"""Gets the active model bundle from cache"""
|
||||
if params is None:
|
||||
params = Params()
|
||||
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
|
||||
artifacts = []
|
||||
for model in getattr(bundle, 'models', []) or []:
|
||||
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)):
|
||||
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None):
|
||||
sha256 = getattr(artifact.downloadUri, 'sha256', None)
|
||||
if sha256:
|
||||
artifacts.append((artifact.fileName, sha256))
|
||||
return artifacts
|
||||
|
||||
|
||||
def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool:
|
||||
model_root = Paths.model_root()
|
||||
return all(_verify_file(os.path.join(model_root, file_name), expected_hash)
|
||||
for file_name, expected_hash in _bundle_artifacts(bundle))
|
||||
|
||||
|
||||
def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None) -> bool:
|
||||
if active_bundle is None:
|
||||
return False
|
||||
|
||||
if available_bundles is not None:
|
||||
matching_bundle = None
|
||||
for bundle in available_bundles:
|
||||
if getattr(active_bundle, 'ref', None) and getattr(bundle, 'ref', None):
|
||||
if active_bundle.ref == bundle.ref:
|
||||
matching_bundle = bundle
|
||||
break
|
||||
elif getattr(active_bundle, 'internalName', None) == getattr(bundle, 'internalName', None):
|
||||
matching_bundle = bundle
|
||||
break
|
||||
|
||||
if matching_bundle is None:
|
||||
return True
|
||||
if active_bundle.minimumSelectorVersion != matching_bundle.minimumSelectorVersion:
|
||||
return True
|
||||
|
||||
active_runner = getattr(active_bundle, 'runner', None)
|
||||
matching_runner = getattr(matching_bundle, 'runner', None)
|
||||
if active_runner is not None and matching_runner is not None:
|
||||
if getattr(active_runner, 'raw', active_runner) != getattr(matching_runner, 'raw', matching_runner):
|
||||
return True
|
||||
if set(_bundle_artifacts(active_bundle)) != set(_bundle_artifacts(matching_bundle)):
|
||||
return True
|
||||
|
||||
return not _bundle_is_valid_locally(active_bundle)
|
||||
|
||||
|
||||
def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None:
|
||||
global _LAST_VALIDATED_RAW
|
||||
|
||||
raw_bundle = params.get("ModelManager_ActiveBundle")
|
||||
if not raw_bundle:
|
||||
return
|
||||
|
||||
if raw_bundle == _LAST_VALIDATED_RAW:
|
||||
return
|
||||
|
||||
active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle)
|
||||
if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles):
|
||||
cloudlog.warning("Active model bundle invalid; resetting to default")
|
||||
params.remove("ModelManager_ActiveBundle")
|
||||
params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True)
|
||||
_LAST_VALIDATED_RAW = None
|
||||
else:
|
||||
_LAST_VALIDATED_RAW = raw_bundle
|
||||
|
||||
|
||||
def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | bytes | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
|
||||
params = params or Params()
|
||||
try:
|
||||
if (active_bundle := params.get("ModelManager_ActiveBundle") or {}) and is_bundle_version_compatible(active_bundle):
|
||||
return custom.ModelManagerSP.ModelBundle(**active_bundle)
|
||||
active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {})
|
||||
if active_bundle_dict and is_bundle_version_compatible(active_bundle_dict):
|
||||
return custom.ModelManagerSP.ModelBundle(**active_bundle_dict)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_active_model_runner(params: Params = None, force_check=False) -> int:
|
||||
if params is None:
|
||||
params = Params()
|
||||
|
||||
def get_active_model_runner(params: Params | None = None, force_check: bool = False) -> int:
|
||||
params = params or Params()
|
||||
cached_runner_type = params.get("ModelRunnerTypeCache")
|
||||
if cached_runner_type is not None and not force_check:
|
||||
return cached_runner_type
|
||||
|
||||
runner_type = custom.ModelManagerSP.Runner.stock
|
||||
|
||||
if active_bundle := get_active_bundle(params):
|
||||
runner_type = active_bundle.runner.raw
|
||||
|
||||
@@ -88,66 +146,40 @@ def get_active_model_runner(params: Params = None, force_check=False) -> int:
|
||||
|
||||
return runner_type
|
||||
|
||||
|
||||
def _get_model():
|
||||
if bundle := get_active_bundle():
|
||||
drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo)
|
||||
return drive_model
|
||||
|
||||
return None
|
||||
|
||||
def load_metadata():
|
||||
metadata_path = METADATA_PATH
|
||||
|
||||
if model := _get_model():
|
||||
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}"
|
||||
def load_metadata():
|
||||
model = _get_model()
|
||||
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
|
||||
|
||||
with open(metadata_path, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
|
||||
|
||||
def prepare_inputs(model_metadata) -> dict[str, np.ndarray]:
|
||||
# img buffers are managed in openCL transform code so we don't pass them as inputs
|
||||
inputs = {
|
||||
k: np.zeros(v, dtype=np.float32).flatten()
|
||||
for k, v in model_metadata['input_shapes'].items()
|
||||
if 'img' not in k
|
||||
def prepare_inputs(model_metadata: dict) -> dict[str, np.ndarray]:
|
||||
return {
|
||||
key: np.zeros(shape, dtype=np.float32).flatten()
|
||||
for key, shape in model_metadata['input_shapes'].items()
|
||||
if 'img' not in key
|
||||
}
|
||||
|
||||
return inputs
|
||||
|
||||
def load_meta_constants(model_metadata: dict):
|
||||
""" Loads the appropriate meta model class based on key shapes"""
|
||||
if 'sim_pose' in model_metadata['input_shapes']:
|
||||
return MetaSimPose
|
||||
|
||||
def load_meta_constants(model_metadata):
|
||||
"""
|
||||
Determines and loads the appropriate meta model class based on the metadata provided. The function checks
|
||||
specific keys and conditions within the provided metadata dictionary to identify the corresponding meta
|
||||
model class to return.
|
||||
meta_slice = model_metadata['output_slices']['meta']
|
||||
if (meta_slice.start, meta_slice.stop, meta_slice.step) == (5868, 5921, None):
|
||||
return MetaTombRaider
|
||||
|
||||
:param model_metadata: Dictionary containing metadata about the model. It includes
|
||||
details such as input shapes, output slices, and other configurations for identifying
|
||||
metadata-dependent meta model classes.
|
||||
:type model_metadata: dict
|
||||
:return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider)
|
||||
based on the conditions and metadata provided.
|
||||
:rtype: type
|
||||
"""
|
||||
meta = Meta # Default Meta
|
||||
|
||||
if 'sim_pose' in model_metadata['input_shapes'].keys():
|
||||
# Meta for models with sim_pose input
|
||||
meta = MetaSimPose
|
||||
else:
|
||||
# Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models
|
||||
meta_slice = model_metadata['output_slices']['meta']
|
||||
meta_tf_slice = slice(5868, 5921, None)
|
||||
|
||||
if (
|
||||
meta_slice.start == meta_tf_slice.start and
|
||||
meta_slice.stop == meta_tf_slice.stop and
|
||||
meta_slice.step == meta_tf_slice.step
|
||||
):
|
||||
meta = MetaTombRaider
|
||||
|
||||
return meta
|
||||
return Meta
|
||||
|
||||
|
||||
# The following method(s) are modeld helper methods
|
||||
|
||||
@@ -17,7 +17,7 @@ from openpilot.system.hardware.hw import Paths
|
||||
|
||||
from cereal import messaging, custom
|
||||
from openpilot.sunnypilot.models.fetcher import ModelFetcher
|
||||
from openpilot.sunnypilot.models.helpers import verify_file, get_active_bundle
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file
|
||||
|
||||
|
||||
class ModelManagerSP:
|
||||
@@ -239,6 +239,7 @@ class ModelManagerSP:
|
||||
while True:
|
||||
try:
|
||||
self.available_models = self.model_fetcher.get_available_bundles()
|
||||
validate_active_bundle(self.params, self.available_models)
|
||||
self.active_bundle = get_active_bundle(self.params)
|
||||
|
||||
if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None:
|
||||
@@ -252,8 +253,8 @@ class ModelManagerSP:
|
||||
self.selected_bundle = None
|
||||
|
||||
if self.params.get("ModelManager_ClearCache"):
|
||||
self.clear_model_cache()
|
||||
self.params.remove("ModelManager_ClearCache")
|
||||
self.clear_model_cache()
|
||||
self.params.remove("ModelManager_ClearCache")
|
||||
|
||||
self._report_status()
|
||||
rk.keep_time()
|
||||
|
||||
@@ -1,215 +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.
|
||||
|
||||
Acceleration Personality (ECO / NORMAL / SPORT). Tunes only MPC INPUTS, never the output:
|
||||
* positive-accel ceiling + its per-cycle open-rate -> tier-scaled take-off from a stop;
|
||||
* jerk-cost relaxation -> smooth accel/decel onset near a stop, on a fresh direction change, a
|
||||
hard-braking lead, or a fast-closing gap;
|
||||
* add-only, speed-dependent follow-gap widen on the MPC t_follow.
|
||||
Disabled => stock everywhere (byte-stock).
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from cereal import messaging
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||
NORMAL, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, A_CRUISE_MAX_V, STOCK_A_CRUISE_MAX_V, \
|
||||
RISE_RATE_BP, RISE_RATE_V, STOCK_RISE_RATE, RISE_RATE_LEAD_VREL_GATE, JERK_SCALE_BP, JERK_SCALE_V, \
|
||||
ONSET_DEADBAND, ONSET_RAMP_S, ONSET_FLOOR, RELAX_RAMP_S, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, \
|
||||
CLOSING_VREL_BP, CLOSING_FACTOR_V, TF_WIDEN_V_BP, TF_WIDEN_BASE_V, TF_WIDEN_TIER, TF_WIDEN_MAX, \
|
||||
TF_SLEW_PER_S, TF_DECEL_HOLD_A
|
||||
|
||||
|
||||
class _OnsetRelax:
|
||||
# Snaps to a tier-scaled floor on a fresh accel<->decel direction change, eases back to 1.0 over ONSET_RAMP_S.
|
||||
def __init__(self):
|
||||
self._prev_sign = 0
|
||||
self._ramp = 1.0
|
||||
|
||||
def reset(self) -> None:
|
||||
self._prev_sign = 0
|
||||
self._ramp = 1.0
|
||||
|
||||
def update(self, a_ego: float, floor: float) -> float:
|
||||
sign = 0
|
||||
if a_ego > ONSET_DEADBAND:
|
||||
sign = 1
|
||||
elif a_ego < -ONSET_DEADBAND:
|
||||
sign = -1
|
||||
|
||||
if sign != 0 and sign != self._prev_sign:
|
||||
self._ramp = floor
|
||||
self._prev_sign = sign
|
||||
else:
|
||||
self._ramp = min(1.0, self._ramp + (1.0 - floor) * (DT_MDL / ONSET_RAMP_S))
|
||||
return self._ramp
|
||||
|
||||
|
||||
class _TransientRelax:
|
||||
# Same shape as _OnsetRelax, but triggered by a level instead of a sign flip: snap to a fresh/escalating
|
||||
# factor, then ease back to 1.0 over ramp_s regardless of whether the raw factor is still low. A pinned
|
||||
# floor destabilizes the MPC's re-solve over a sustained episode, so only the onset gets softened.
|
||||
def __init__(self):
|
||||
self._was_active = False
|
||||
self._episode_floor = 1.0
|
||||
self._ramp = 1.0
|
||||
|
||||
def reset(self) -> None:
|
||||
self._was_active = False
|
||||
self._episode_floor = 1.0
|
||||
self._ramp = 1.0
|
||||
|
||||
def update(self, raw_factor: float, ramp_s: float) -> float:
|
||||
active = raw_factor < 1.0 - 1e-3
|
||||
if active and (not self._was_active or raw_factor < self._episode_floor):
|
||||
self._episode_floor = raw_factor
|
||||
self._ramp = raw_factor
|
||||
else:
|
||||
self._ramp = min(1.0, self._ramp + DT_MDL / ramp_s)
|
||||
if not active:
|
||||
self._episode_floor = 1.0
|
||||
self._was_active = active
|
||||
return self._ramp
|
||||
|
||||
|
||||
class AccelController:
|
||||
def __init__(self, CP: structs.CarParams, mpc=None, params=None):
|
||||
# CP/mpc accepted for the planner's constructor signature; unused (shapes MPC inputs only).
|
||||
self._params = params or Params()
|
||||
self._frame = 0
|
||||
self._enabled = False
|
||||
self._personality = NORMAL
|
||||
self._v_ego = 0.0
|
||||
self._a_ego = 0.0
|
||||
self._widen = 0.0 # current slewed follow-gap widen (s), add-only
|
||||
self._t_follow = 0.0 # last t_follow handed to the MPC (telemetry)
|
||||
self._onset_relax = _OnsetRelax()
|
||||
self._lead_brake_relax = _TransientRelax()
|
||||
self._closing_relax = _TransientRelax()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
self._closing_on_lead = False
|
||||
self._read_params()
|
||||
|
||||
def _read_params(self) -> None:
|
||||
self._enabled = self._params.get_bool("AccelPersonalityEnabled")
|
||||
if not self._enabled:
|
||||
self._personality = NORMAL
|
||||
return
|
||||
self._personality = get_sanitize_int_param("AccelPersonality", PERSONALITY_MIN, PERSONALITY_MAX, self._params)
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
if self._frame % int(1. / DT_MDL) == 0:
|
||||
self._read_params()
|
||||
self._v_ego = float(sm['carState'].vEgo)
|
||||
self._a_ego = float(sm['carState'].aEgo)
|
||||
|
||||
if self._enabled:
|
||||
lead = sm['radarState'].leadOne
|
||||
self._onset_factor = self._onset_relax.update(self._a_ego, ONSET_FLOOR[self._personality])
|
||||
self._lead_brake_factor = self._lead_brake_relax.update(self._get_lead_brake_factor(lead), RELAX_RAMP_S)
|
||||
self._closing_factor = self._closing_relax.update(self._get_closing_factor(lead), RELAX_RAMP_S)
|
||||
self._closing_on_lead = lead.status and lead.vRel < RISE_RATE_LEAD_VREL_GATE
|
||||
else:
|
||||
self._onset_relax.reset()
|
||||
self._lead_brake_relax.reset()
|
||||
self._closing_relax.reset()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
self._closing_on_lead = False
|
||||
|
||||
self._frame += 1
|
||||
|
||||
def _get_lead_brake_factor(self, lead) -> float:
|
||||
# Raw, uneased tier-scaled factor for THIS cycle -- fed through _TransientRelax in update(), never used
|
||||
# directly as the published factor (see the class docstring for why: a level-pinned floor destabilizes
|
||||
# the MPC's re-solve over a sustained episode).
|
||||
if not lead.status:
|
||||
return 1.0
|
||||
return float(np.interp(lead.aLeadK, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V[self._personality]))
|
||||
|
||||
def _get_closing_factor(self, lead) -> float:
|
||||
# Raw, uneased tier-scaled factor for THIS cycle -- see _get_lead_brake_factor's note.
|
||||
if not lead.status:
|
||||
return 1.0
|
||||
return float(np.interp(lead.vRel, CLOSING_VREL_BP, CLOSING_FACTOR_V[self._personality]))
|
||||
|
||||
def reset(self) -> None:
|
||||
# Drop the accumulated widen (e.g. on disengage / standstill re-init) so it re-ramps cleanly.
|
||||
self._widen = 0.0
|
||||
self._onset_relax.reset()
|
||||
self._lead_brake_relax.reset()
|
||||
self._closing_relax.reset()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
|
||||
def get_max_accel(self, v_ego: float) -> float:
|
||||
# Disabled -> stock ceiling (off == stock, independent of the NORMAL profile so NORMAL is free to differ).
|
||||
table = A_CRUISE_MAX_V[self._personality] if self._enabled else STOCK_A_CRUISE_MAX_V
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP, table))
|
||||
|
||||
def get_rise_rate(self, v_ego: float) -> float:
|
||||
# Disabled, or still closing on a lead -> stock ceiling open-rate.
|
||||
if not self._enabled or self._closing_on_lead:
|
||||
return STOCK_RISE_RATE
|
||||
return float(np.interp(v_ego, RISE_RATE_BP, RISE_RATE_V[self._personality]))
|
||||
|
||||
def get_jerk_scale(self, v_ego: float) -> float:
|
||||
# Most-relaxed of: near a stop, a fresh onset, a hard-braking lead, a fast-closing gap. Never > 1.0 (stock).
|
||||
if not self._enabled:
|
||||
return 1.0
|
||||
near_stop = float(np.interp(v_ego, JERK_SCALE_BP, JERK_SCALE_V[self._personality]))
|
||||
return min(near_stop, self._onset_factor, self._lead_brake_factor, self._closing_factor)
|
||||
|
||||
def get_t_follow(self, t_follow: float, v_ego: float) -> float:
|
||||
# MPC t_follow hook. Adds a slewed, decel-held, speed-dependent comfort widen on top of the stock
|
||||
# t_follow. Identity when disabled => byte-stock. Add-only => desired distance >= stock => brake >= stock.
|
||||
t_follow = float(t_follow)
|
||||
if not self._enabled:
|
||||
self._widen = 0.0
|
||||
self._t_follow = t_follow
|
||||
return t_follow
|
||||
|
||||
target = float(np.interp(v_ego, TF_WIDEN_V_BP, TF_WIDEN_BASE_V)) * TF_WIDEN_TIER[self._personality]
|
||||
target = min(target, TF_WIDEN_MAX)
|
||||
step = TF_SLEW_PER_S * DT_MDL
|
||||
|
||||
if self._a_ego <= TF_DECEL_HOLD_A and target < self._widen:
|
||||
pass # decel-hold: don't ease the gap in while braking
|
||||
elif target > self._widen:
|
||||
self._widen = min(target, self._widen + step) # open the gap, slewed
|
||||
else:
|
||||
self._widen = max(target, self._widen - step) # close the gap, slewed
|
||||
|
||||
self._widen = max(0.0, self._widen) # add-only guard
|
||||
self._t_follow = t_follow + self._widen
|
||||
return self._t_follow
|
||||
|
||||
# --- telemetry (published to cereal LongitudinalPlanSP.acceleration; no control effect) ---
|
||||
def enabled(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
def personality(self):
|
||||
return self._personality
|
||||
|
||||
def max_accel(self) -> float:
|
||||
return self.get_max_accel(self._v_ego)
|
||||
|
||||
def t_follow(self) -> float:
|
||||
return self._t_follow
|
||||
|
||||
def follow_widen(self) -> float:
|
||||
return self._widen
|
||||
|
||||
def widen_active(self) -> bool:
|
||||
return self._enabled and self._widen > 0.005
|
||||
@@ -1,86 +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.
|
||||
|
||||
Acceleration Personality tuning tables. The controller shapes only what the longitudinal MPC CONSUMES
|
||||
(the positive-accel ceiling + its open-rate, and an add-only follow-gap widen fed to the MPC's t_follow);
|
||||
it never post-shapes the MPC's output accel. Disabled => every getter returns the upstream stock value,
|
||||
so off == byte-stock.
|
||||
"""
|
||||
|
||||
from cereal import custom
|
||||
|
||||
AccelerationPersonality = custom.LongitudinalPlanSP.AccelerationPersonality
|
||||
ECO = AccelerationPersonality.eco
|
||||
NORMAL = AccelerationPersonality.normal
|
||||
SPORT = AccelerationPersonality.sport
|
||||
|
||||
PERSONALITY_MIN = min(AccelerationPersonality.schema.enumerants.values())
|
||||
PERSONALITY_MAX = max(AccelerationPersonality.schema.enumerants.values())
|
||||
|
||||
# --- Positive-accel ceiling (launch/cruise) + its upward open-rate ---------------------------------------
|
||||
# Disabled -> falls back to STOCK_* (byte-stock). ACCEL_MAX (opendbc) hard-caps the ceiling at 2.0 m/s^2.
|
||||
A_CRUISE_MAX_BP = [0., 10., 25., 40.] # m/s (matches upstream A_CRUISE_MAX_BP)
|
||||
STOCK_A_CRUISE_MAX_V = [1.6, 1.2, 0.8, 0.6] # upstream A_CRUISE_MAX_VALS
|
||||
STOCK_RISE_RATE = 0.05 # upstream ceiling open-rate (m/s^2 per cycle)
|
||||
A_CRUISE_MAX_V = {
|
||||
ECO: [1.55, 0.75, 0.35, 0.20],
|
||||
NORMAL: [2.00, 1.40, 0.95, 0.70],
|
||||
SPORT: [2.00, 1.70, 1.20, 0.90],
|
||||
}
|
||||
# Ceiling open-rate (m/s^2 per cycle): fast near v=0 so launch isn't delayed, tapers by v=5.
|
||||
RISE_RATE_BP = [0., 5.] # m/s
|
||||
RISE_RATE_V = {
|
||||
ECO: [0.80, 0.07],
|
||||
NORMAL: [1.00, 0.16],
|
||||
SPORT: [1.20, 0.24],
|
||||
}
|
||||
# The fast near-stop rise-rate above only looks at v_ego, so it also fires while still closing on a lead
|
||||
# that hasn't cleared yet, letting the ceiling snap open faster than the actual situation calls for. Fall
|
||||
# back to the stock rate whenever still closing (vRel this negative or more).
|
||||
RISE_RATE_LEAD_VREL_GATE = -0.5 # m/s
|
||||
|
||||
# --- Launch jerk-cost relaxation (MPC INPUT: scales the core MPC's own jerk_factor) -----------------------
|
||||
# Bounded near a stop, ramped back to 1.0 (stock) by cruise speed. v=0 knot closed-loop verified; SPORT tied
|
||||
# to NORMAL rather than pushed lower (lower destabilizes the solver at this knot).
|
||||
JERK_SCALE_BP = [0., 5.] # m/s
|
||||
JERK_SCALE_V = {
|
||||
ECO: [0.60, 1.0],
|
||||
NORMAL: [0.45, 1.0],
|
||||
SPORT: [0.45, 1.0],
|
||||
}
|
||||
|
||||
# --- Onset jerk-cost relaxation (MPC INPUT: any accel<->decel direction change, not just launch) ----------
|
||||
ONSET_DEADBAND = 0.15 # m/s^2: ignore aEgo noise this small around a zero-crossing
|
||||
ONSET_RAMP_S = 0.4 # s: ease back to stock over this long
|
||||
ONSET_FLOOR = {ECO: 0.75, NORMAL: 0.65, SPORT: 0.50}
|
||||
|
||||
# Ramp shared by the two level-triggered factors below (lead-braking, closing-rate): a sustained pinned
|
||||
# floor destabilizes the MPC's re-solve, so both ease back to stock over this window instead.
|
||||
RELAX_RAMP_S = 0.4
|
||||
|
||||
# --- Lead-braking jerk-cost relaxation (MPC INPUT: react faster to a hard-braking lead) --------------------
|
||||
LEAD_BRAKE_ALEAD_BP = [-3.0, -0.5] # m/s^2, lead's own aLeadK
|
||||
LEAD_BRAKE_FACTOR_V = {
|
||||
ECO: [0.75, 1.0],
|
||||
NORMAL: [0.60, 1.0],
|
||||
SPORT: [0.45, 1.0],
|
||||
}
|
||||
|
||||
# --- Closing-rate jerk-cost relaxation (MPC INPUT: react faster to a fast-closing gap, any cause) ----------
|
||||
CLOSING_VREL_BP = [-6.0, -1.5] # m/s, closing rate (negative = closing)
|
||||
CLOSING_FACTOR_V = {
|
||||
ECO: [0.75, 1.0],
|
||||
NORMAL: [0.60, 1.0],
|
||||
SPORT: [0.45, 1.0],
|
||||
}
|
||||
|
||||
# --- Follow-gap widen (add-only, fed to the MPC t_follow) ------------------------------------------------
|
||||
TF_WIDEN_V_BP = [14.0, 28.0] # m/s: widen ramps in across this band, flat above
|
||||
TF_WIDEN_BASE_V = [0.0, 0.30] # s: base follow-time added at the band ends (pre-tier)
|
||||
TF_WIDEN_TIER = {ECO: 1.30, NORMAL: 1.00, SPORT: 0.50}
|
||||
TF_WIDEN_MAX = 0.45 # s: absolute cap on the added gap
|
||||
TF_SLEW_PER_S = 0.50 # s per second: max rate the widen may open/close
|
||||
TF_DECEL_HOLD_A = -0.20 # m/s^2: at/below this a_ego the widen won't shrink
|
||||
@@ -1,505 +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.
|
||||
|
||||
AccelController is an INPUT shaper for the longitudinal MPC: a per-tier positive-accel ceiling + open-rate
|
||||
(launch), and an ADD-ONLY, slewed, decel-held follow-gap widen fed to the MPC t_follow. It never shapes the
|
||||
MPC output, so these tests pin: off == byte-stock; tier ordering; and the t_follow invariants (add-only,
|
||||
zero below the gate, slew-bounded, decel-hold, capped).
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||
ECO, NORMAL, SPORT, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, RISE_RATE_V, \
|
||||
STOCK_A_CRUISE_MAX_V, STOCK_RISE_RATE, JERK_SCALE_BP, JERK_SCALE_V, ONSET_DEADBAND, ONSET_RAMP_S, \
|
||||
ONSET_FLOOR, RELAX_RAMP_S, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, CLOSING_VREL_BP, CLOSING_FACTOR_V, \
|
||||
TF_WIDEN_V_BP, TF_WIDEN_BASE_V, TF_WIDEN_TIER, TF_WIDEN_MAX, TF_SLEW_PER_S, TF_DECEL_HOLD_A, AccelerationPersonality
|
||||
|
||||
_EPS = 1e-6
|
||||
_TF_STOCK = 1.45 # a representative stock t_follow (standard personality); the widen is add-only on top
|
||||
_SLEW_STEP = TF_SLEW_PER_S * DT_MDL
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, store=None):
|
||||
self.store = dict(store or {})
|
||||
|
||||
def get_bool(self, key):
|
||||
return bool(self.store.get(key, False))
|
||||
|
||||
def get(self, key, return_default=False):
|
||||
return int(self.store.get(key, 1))
|
||||
|
||||
def put(self, key, val, block=False):
|
||||
self.store[key] = val
|
||||
|
||||
|
||||
def make_lead(status=False, aLeadK=0.0, vRel=0.0):
|
||||
return SimpleNamespace(status=status, aLeadK=aLeadK, vRel=vRel)
|
||||
|
||||
|
||||
def make_sm(v_ego=20.0, a_ego=0.0, lead=None):
|
||||
return {'carState': SimpleNamespace(vEgo=v_ego, aEgo=a_ego),
|
||||
'radarState': SimpleNamespace(leadOne=lead or make_lead())}
|
||||
|
||||
|
||||
def make_controller(enabled=True, personality=NORMAL):
|
||||
store = {"AccelPersonalityEnabled": enabled, "AccelPersonality": int(personality)}
|
||||
ctrl = AccelController(CP=SimpleNamespace(), mpc=SimpleNamespace(), params=FakeParams(store))
|
||||
ctrl.update(make_sm())
|
||||
return ctrl
|
||||
|
||||
|
||||
def settle(ctrl, v_ego, a_ego=0.0, t_follow=_TF_STOCK, n=400):
|
||||
ctrl.update(make_sm(v_ego=v_ego, a_ego=a_ego))
|
||||
out = t_follow
|
||||
for _ in range(n):
|
||||
out = ctrl.get_t_follow(t_follow, v_ego)
|
||||
return out
|
||||
|
||||
|
||||
# --- Profiles / off == stock ------------------------------------------------------------------------------
|
||||
|
||||
def test_enum_source_parity():
|
||||
assert (ECO, NORMAL, SPORT) == (AccelerationPersonality.eco, AccelerationPersonality.normal, AccelerationPersonality.sport)
|
||||
assert (PERSONALITY_MIN, PERSONALITY_MAX) == (0, 2)
|
||||
|
||||
|
||||
def test_disabled_forces_normal_and_stock_ceiling():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
assert ctrl.personality() == NORMAL
|
||||
assert not ctrl.enabled()
|
||||
for v in (0.0, 10.0, 25.0, 40.0):
|
||||
assert ctrl.get_max_accel(v) == pytest.approx(np.interp(v, A_CRUISE_MAX_BP, STOCK_A_CRUISE_MAX_V))
|
||||
# off == stock, regardless of v_ego (the speed-dependent open-rate table is bypassed entirely when disabled)
|
||||
for v in (0.0, 5.0, 20.0, 40.0):
|
||||
assert ctrl.get_rise_rate(v) == STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_disabled_t_follow_is_identity():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
for v in (2.0, 10.0, 20.0, 30.0):
|
||||
assert ctrl.get_t_follow(_TF_STOCK, v) == pytest.approx(_TF_STOCK)
|
||||
assert ctrl.follow_widen() == 0.0
|
||||
assert not ctrl.widen_active()
|
||||
|
||||
|
||||
def test_stock_ceiling_matches_upstream():
|
||||
# off must equal upstream get_max_accel table so the feature is byte-stock when disabled.
|
||||
assert STOCK_A_CRUISE_MAX_V == [1.6, 1.2, 0.8, 0.6]
|
||||
assert A_CRUISE_MAX_BP == [0., 10., 25., 40.]
|
||||
assert STOCK_RISE_RATE == 0.05
|
||||
|
||||
|
||||
def test_ceiling_ordering_eco_le_normal_le_sport():
|
||||
eco = make_controller(personality=ECO)
|
||||
nrm = make_controller(personality=NORMAL)
|
||||
spt = make_controller(personality=SPORT)
|
||||
for v in (0.0, 10.0, 25.0, 40.0):
|
||||
assert eco.get_max_accel(v) <= nrm.get_max_accel(v) + _EPS
|
||||
assert nrm.get_max_accel(v) <= spt.get_max_accel(v) + _EPS
|
||||
# strictly distinct where the tables diverge (mid speed)
|
||||
assert make_controller(personality=ECO).get_max_accel(25.0) < make_controller(personality=SPORT).get_max_accel(25.0)
|
||||
|
||||
|
||||
def test_rise_rate_ordering_and_above_stock():
|
||||
# ordering holds at both knots: near a stop (v=0) and at the steady-state speed (v=5)
|
||||
assert RISE_RATE_V[ECO][0] < RISE_RATE_V[NORMAL][0] < RISE_RATE_V[SPORT][0]
|
||||
assert RISE_RATE_V[ECO][1] < RISE_RATE_V[NORMAL][1] < RISE_RATE_V[SPORT][1]
|
||||
# every tier opens the ceiling faster than stock at both knots (fast take-off, never slower than stock)
|
||||
assert RISE_RATE_V[ECO][0] > STOCK_RISE_RATE
|
||||
assert RISE_RATE_V[ECO][1] > STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_rise_rate_fast_near_stop_tapers_to_steady_state():
|
||||
# Near a stop (v=0) the open-rate must be large/non-binding (NOT the old flat 0.07/0.16/0.24) so launch
|
||||
# is never delayed. At/above the v=5 knot it must match the old flat, telemetry-verified steady-state
|
||||
# values exactly, so cruise/resume behavior at speed is unchanged.
|
||||
for personality, steady_state in ((ECO, 0.07), (NORMAL, 0.16), (SPORT, 0.24)):
|
||||
ctrl = make_controller(personality=personality)
|
||||
assert ctrl.get_rise_rate(0.0) >= 0.5
|
||||
assert ctrl.get_rise_rate(0.0) > steady_state
|
||||
assert ctrl.get_rise_rate(5.0) == pytest.approx(steady_state)
|
||||
assert ctrl.get_rise_rate(20.0) == pytest.approx(steady_state) # flat above the v=5 knot
|
||||
|
||||
|
||||
def test_rise_rate_falls_back_to_stock_while_closing_on_a_lead():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=2.0, lead=make_lead(status=True, vRel=-2.0)))
|
||||
assert ctrl.get_rise_rate(2.0) == STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_rise_rate_unaffected_by_a_departing_or_absent_lead():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=2.0, lead=make_lead(status=True, vRel=0.5)))
|
||||
assert ctrl.get_rise_rate(2.0) != STOCK_RISE_RATE
|
||||
ctrl.update(make_sm(v_ego=2.0, lead=make_lead(status=False)))
|
||||
assert ctrl.get_rise_rate(2.0) != STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_rise_rate_lead_gate_disabled_is_still_stock():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=2.0, lead=make_lead(status=True, vRel=-2.0)))
|
||||
assert ctrl.get_rise_rate(2.0) == STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_normal_is_distinct_from_stock():
|
||||
nrm = make_controller(personality=NORMAL)
|
||||
# enabled NORMAL differs from stock (so NORMAL is a real profile, not a stock alias)
|
||||
assert nrm.get_max_accel(25.0) != pytest.approx(np.interp(25.0, A_CRUISE_MAX_BP, STOCK_A_CRUISE_MAX_V))
|
||||
assert nrm.get_rise_rate(0.0) != STOCK_RISE_RATE
|
||||
assert nrm.get_rise_rate(5.0) != STOCK_RISE_RATE
|
||||
|
||||
|
||||
def test_eco_ceiling_matches_lowered_table():
|
||||
# ECO's cruise/resume-range ceiling was lowered (launch knot at v=0 unchanged at 1.55).
|
||||
eco = make_controller(personality=ECO)
|
||||
for v, expected in zip(A_CRUISE_MAX_BP, (1.55, 0.75, 0.35, 0.20), strict=True):
|
||||
assert eco.get_max_accel(v) == pytest.approx(expected)
|
||||
|
||||
|
||||
# --- jerk-scale: launch jerk-cost relaxation (MPC input, feeds long_mpc.set_weights) ----------------------
|
||||
|
||||
def test_jerk_scale_disabled_is_stock():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
for v in (0.0, 2.5, 5.0, 20.0):
|
||||
assert ctrl.get_jerk_scale(v) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_jerk_scale_relaxed_near_stop_flat_at_speed():
|
||||
for personality, relaxed in ((ECO, 0.60), (NORMAL, 0.45), (SPORT, 0.45)):
|
||||
ctrl = make_controller(personality=personality)
|
||||
assert ctrl.get_jerk_scale(0.0) == pytest.approx(relaxed)
|
||||
assert ctrl.get_jerk_scale(5.0) == pytest.approx(1.0) # back to stock by the v=5 knot
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0) # flat above the knot
|
||||
|
||||
|
||||
def test_jerk_scale_never_exceeds_stock():
|
||||
# relaxation only ever LOWERS the jerk cost (more responsive), never raises it above 1.0 (stock)
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for v in np.linspace(0.0, 40.0, 20):
|
||||
assert ctrl.get_jerk_scale(float(v)) <= 1.0 + _EPS
|
||||
|
||||
|
||||
def test_jerk_scale_tier_ordering_at_stop():
|
||||
# NOT a strict SPORT<NORMAL<ECO ordering here (unlike every other tier table in this file) -- verified via
|
||||
# a closed-loop MPC harness that pushing the v=0 knot lower than ~0.45 is counterproductive (the MPC
|
||||
# back-loads the ramp instead of front-loading it), so SPORT is pinned to NORMAL's value instead of being
|
||||
# pushed lower for tier-consistency. See the JERK_SCALE_V comment in constants.py.
|
||||
eco = make_controller(personality=ECO).get_jerk_scale(0.0)
|
||||
nrm = make_controller(personality=NORMAL).get_jerk_scale(0.0)
|
||||
spt = make_controller(personality=SPORT).get_jerk_scale(0.0)
|
||||
assert spt == pytest.approx(nrm)
|
||||
assert nrm < eco
|
||||
|
||||
|
||||
def test_jerk_scale_table_matches_constants():
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for v in (0.0, 2.0, 5.0, 15.0):
|
||||
assert ctrl.get_jerk_scale(v) == pytest.approx(np.interp(v, JERK_SCALE_BP, JERK_SCALE_V[personality]))
|
||||
|
||||
|
||||
def test_jerk_scale_v0_knot_stays_out_of_the_counterproductive_zone():
|
||||
# Regression guard for the verified finding: pushing the v=0 knot below ~0.45 stops helping and starts
|
||||
# hurting (measured via a closed-loop MPC harness -- 0.30 came back slower than stock in every scenario
|
||||
# tested, and below ~0.15 the solver itself destabilizes). No tier's launch floor should regress into that
|
||||
# zone even if someone re-tunes ECO/NORMAL/SPORT independently later.
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
assert JERK_SCALE_V[personality][0] >= 0.40
|
||||
|
||||
|
||||
# --- onset relax: fresh accel<->decel direction change, any speed ------------------------------------------
|
||||
|
||||
def test_onset_disabled_is_stock():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=2.0))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_onset_no_effect_at_steady_state():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
for _ in range(10):
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=0.0))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0) # within the deadband, no direction to flip from
|
||||
|
||||
|
||||
def test_onset_drops_to_floor_on_fresh_direction_change():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0)) # establish "accelerating"
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0)) # fresh flip to decelerating
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(ONSET_FLOOR[NORMAL])
|
||||
|
||||
|
||||
def test_onset_eases_back_to_stock_over_ramp_s():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(ONSET_FLOOR[NORMAL])
|
||||
n = int(ONSET_RAMP_S / DT_MDL)
|
||||
for _ in range(n):
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0)) # sustained decel, no further flip
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0, abs=1e-3)
|
||||
|
||||
|
||||
def test_onset_never_relaxes_below_its_own_floor():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
for _ in range(50):
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0 if _ % 2 == 0 else -1.2)) # repeated same-direction wiggle
|
||||
assert ctrl.get_jerk_scale(20.0) >= ONSET_FLOOR[SPORT] - _EPS
|
||||
|
||||
|
||||
def test_onset_tier_ordering():
|
||||
ordered = []
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0))
|
||||
ordered.append(ctrl.get_jerk_scale(20.0))
|
||||
assert ordered[2] < ordered[1] < ordered[0] # SPORT relaxes most, ECO least
|
||||
|
||||
|
||||
def test_onset_ignores_deadband_noise():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=0.3))
|
||||
for _ in range(10):
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=ONSET_DEADBAND - 0.02)) # tiny wiggle, never crosses -deadband
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0, abs=1e-3)
|
||||
|
||||
|
||||
# --- lead-braking relax: hard-braking lead relaxes jerk cost regardless of speed ---------------------------
|
||||
|
||||
def test_lead_brake_no_lead_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=False, aLeadK=-5.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_lead_brake_relaxes_with_lead_decel():
|
||||
for personality, floor in ((ECO, 0.75), (NORMAL, 0.60), (SPORT, 0.45)):
|
||||
ctrl = make_controller(personality=personality)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, aLeadK=-3.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(floor)
|
||||
|
||||
|
||||
def test_lead_brake_gentle_lead_decel_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, aLeadK=-0.2)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0) # above the -0.5 gate -> no relax
|
||||
|
||||
|
||||
def test_lead_brake_matches_constants_table():
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for a_lead in (-0.5, -1.5, -3.0):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, aLeadK=a_lead)))
|
||||
expected = np.interp(a_lead, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V[personality])
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(expected)
|
||||
|
||||
|
||||
# --- closing-rate relax: fast-closing gap relaxes jerk cost proactively, any cause -------------------------
|
||||
|
||||
def test_closing_no_lead_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=False, vRel=-8.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_relaxes_with_fast_closing_lead():
|
||||
for personality, floor in ((ECO, 0.75), (NORMAL, 0.60), (SPORT, 0.45)):
|
||||
ctrl = make_controller(personality=personality)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(floor)
|
||||
|
||||
|
||||
def test_closing_slow_closing_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-0.5)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0) # above the -1.5 gate -> no relax
|
||||
|
||||
|
||||
def test_closing_opening_gap_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=3.0))) # lead pulling away
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_matches_constants_table():
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for v_rel in (-1.5, -3.0, -6.0):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=v_rel)))
|
||||
expected = np.interp(v_rel, CLOSING_VREL_BP, CLOSING_FACTOR_V[personality])
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_closing_disabled_is_stock():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_fires_before_a_ego_moves():
|
||||
# The whole point: on the VERY FIRST cycle a fast-closing lead appears, before a_ego has had any chance to
|
||||
# react (still 0.0, so onset-relax is untouched) -- the closing factor alone must already be relaxed.
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=0.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(CLOSING_FACTOR_V[NORMAL][0])
|
||||
|
||||
|
||||
# --- transient relax: closing/lead-brake factors dip then ease back, never hold a sustained low floor ------
|
||||
# Closed-loop-verified (selfdrive/test/longitudinal_maneuvers-based): holding jerk_scale at a fixed low value
|
||||
# for the duration of a sustained closing/braking episode destabilizes the MPC's real-time-iteration re-solve
|
||||
# into an oscillation (30+ m/s^3 jerk vs 0 with the factor disabled, for an identical scenario). These pin
|
||||
# the fix -- a sustained, UNCHANGING severe closing rate must ease back toward stock, not stay pinned.
|
||||
|
||||
def test_closing_factor_eases_back_despite_sustained_severe_vrel():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(CLOSING_FACTOR_V[SPORT][0])
|
||||
for _ in range(int(RELAX_RAMP_S / DT_MDL)):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0))) # same severity, every cycle
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0, abs=1e-3) # eased back despite no improvement
|
||||
|
||||
|
||||
def test_lead_brake_factor_eases_back_despite_sustained_hard_braking_lead():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, aLeadK=-3.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(LEAD_BRAKE_FACTOR_V[SPORT][0])
|
||||
for _ in range(int(RELAX_RAMP_S / DT_MDL)):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, aLeadK=-3.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0, abs=1e-3)
|
||||
|
||||
|
||||
def test_closing_factor_resnaps_on_escalation_not_just_first_onset():
|
||||
# A worsening (not just sustained) closing rate must not be ignored because we're mid-ramp-back --
|
||||
# re-snap to the newly-lower floor.
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-2.0))) # mild, partial relax
|
||||
partial = ctrl.get_jerk_scale(20.0)
|
||||
assert 1.0 - _EPS > partial > CLOSING_FACTOR_V[SPORT][0] + _EPS
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0))) # escalates to the full floor
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(CLOSING_FACTOR_V[SPORT][0])
|
||||
|
||||
|
||||
def test_closing_factor_reset_clears_transient_state():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
for _ in range(5):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) < 1.0 - _EPS
|
||||
ctrl.reset()
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=False)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
# --- combined: get_jerk_scale takes the most-relaxed of all three factors ----------------------------------
|
||||
|
||||
def test_combined_takes_most_relaxed_factor():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
# onset flip (floor=0.65) + hard-braking lead (floor=0.60 at aLeadK=-3.0) -> min of the two, near-stop is 1.0 at v=20
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0, lead=make_lead(status=True, aLeadK=-3.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(min(ONSET_FLOOR[NORMAL], LEAD_BRAKE_FACTOR_V[NORMAL][0]))
|
||||
|
||||
|
||||
def test_reset_clears_onset_and_lead_brake_state():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0, lead=make_lead(status=True, aLeadK=-3.0, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) < 1.0 - _EPS
|
||||
ctrl.reset()
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
# --- t_follow: add-only speed widen -----------------------------------------------------------------------
|
||||
|
||||
def test_t_follow_zero_below_gate():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
out = settle(ctrl, v_ego=TF_WIDEN_V_BP[0] - 1.0) # below the widen onset
|
||||
assert out == pytest.approx(_TF_STOCK)
|
||||
assert ctrl.follow_widen() == pytest.approx(0.0, abs=1e-6)
|
||||
|
||||
|
||||
def test_t_follow_widens_at_speed():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
out = settle(ctrl, v_ego=TF_WIDEN_V_BP[1] + 5.0) # flat-widen region, above the band
|
||||
expected = _TF_STOCK + TF_WIDEN_BASE_V[1] * TF_WIDEN_TIER[NORMAL]
|
||||
assert out == pytest.approx(expected, abs=1e-3)
|
||||
assert ctrl.widen_active()
|
||||
|
||||
|
||||
def test_t_follow_add_only_random_walk():
|
||||
rng = np.random.default_rng(0)
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for _ in range(500):
|
||||
v = float(rng.uniform(0.0, 40.0))
|
||||
a = float(rng.uniform(-3.0, 1.5))
|
||||
ctrl.update(make_sm(v_ego=v, a_ego=a))
|
||||
out = ctrl.get_t_follow(_TF_STOCK, v)
|
||||
assert out >= _TF_STOCK - _EPS # never tighter than the stock gap => brake >= stock
|
||||
assert ctrl.follow_widen() <= TF_WIDEN_MAX + _EPS # widen capped
|
||||
|
||||
|
||||
def test_t_follow_tier_ordering_at_speed():
|
||||
v = TF_WIDEN_V_BP[1] + 5.0
|
||||
eco = settle(make_controller(personality=ECO), v_ego=v)
|
||||
nrm = settle(make_controller(personality=NORMAL), v_ego=v)
|
||||
spt = settle(make_controller(personality=SPORT), v_ego=v)
|
||||
assert eco > nrm > spt # ECO roomiest, SPORT tightest
|
||||
|
||||
|
||||
def test_t_follow_slew_bounded():
|
||||
ctrl = make_controller(personality=ECO)
|
||||
ctrl.update(make_sm(v_ego=35.0, a_ego=0.0)) # big target widen, start from 0
|
||||
prev = 0.0
|
||||
for _ in range(50):
|
||||
ctrl.get_t_follow(_TF_STOCK, 35.0)
|
||||
assert ctrl.follow_widen() - prev <= _SLEW_STEP + _EPS # opens no faster than the slew cap
|
||||
prev = ctrl.follow_widen()
|
||||
|
||||
|
||||
def test_t_follow_decel_hold_does_not_shrink_gap():
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
settle(ctrl, v_ego=35.0, a_ego=0.0) # open the gap fully
|
||||
held = ctrl.follow_widen()
|
||||
assert held > 0.1
|
||||
# now braking (a_ego below the hold threshold) while speed drops into the zero-widen region
|
||||
for _ in range(50):
|
||||
ctrl.update(make_sm(v_ego=8.0, a_ego=TF_DECEL_HOLD_A - 1.0))
|
||||
ctrl.get_t_follow(_TF_STOCK, 8.0)
|
||||
assert ctrl.follow_widen() >= held - _EPS # gap does not ease in while braking
|
||||
# once no longer braking, the gap eases back toward the (zero) target
|
||||
for _ in range(200):
|
||||
ctrl.update(make_sm(v_ego=8.0, a_ego=0.0))
|
||||
ctrl.get_t_follow(_TF_STOCK, 8.0)
|
||||
assert ctrl.follow_widen() == pytest.approx(0.0, abs=1e-3)
|
||||
|
||||
|
||||
def test_reset_clears_widen():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
settle(ctrl, v_ego=35.0)
|
||||
assert ctrl.follow_widen() > 0.0
|
||||
ctrl.reset()
|
||||
assert ctrl.follow_widen() == 0.0
|
||||
|
||||
|
||||
def test_out_of_range_personality_clamps():
|
||||
ctrl = make_controller(personality=99)
|
||||
assert ctrl.personality() == PERSONALITY_MAX
|
||||
|
||||
|
||||
def test_max_accel_uses_stored_v_ego():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=0.0))
|
||||
assert ctrl.max_accel() == pytest.approx(ctrl.get_max_accel(0.0))
|
||||
@@ -1,47 +1,17 @@
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
|
||||
class WMACConstants:
|
||||
TRAJECTORY_SIZE = 33
|
||||
PARAM_READ_FRAMES = max(1, int(round(1.0 / DT_MDL)))
|
||||
# Lead detection parameters
|
||||
LEAD_WINDOW_SIZE = 6 # Stable detection window
|
||||
LEAD_PROB = 0.45 # Balanced threshold for lead detection
|
||||
|
||||
EMERGENCY_HOLD_FRAMES = max(1, int(round(0.75 / DT_MDL)))
|
||||
MIN_MODE_DURATION = {'acc': max(1, int(round(0.6 / DT_MDL))), 'blended': max(1, int(round(0.5 / DT_MDL)))}
|
||||
ENTER_BLENDED_FRAMES = max(1, int(round(0.4 / DT_MDL)))
|
||||
EXIT_BLENDED_FRAMES = max(1, int(round(0.35 / DT_MDL)))
|
||||
STANDSTILL_FRAMES = max(1, int(round(0.2 / DT_MDL)))
|
||||
TRANSITION_SMOOTH_FRAMES = max(1, int(round(0.5 / DT_MDL)))
|
||||
# Slow down detection parameters
|
||||
SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable
|
||||
SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios
|
||||
|
||||
LEAD_PROB = 0.45
|
||||
LEAD_EXIT_PROB = 0.25
|
||||
LEAD_RISE_RATE = 1.0
|
||||
LEAD_FALL_RATE = 0.35
|
||||
RADAR_LEAD_ACC_PROB = 0.5
|
||||
RADAR_LEAD_ACC_EXIT_PROB = 0.4
|
||||
RADAR_LEAD_ACC_RISE_RATE = 1.0
|
||||
RADAR_LEAD_ACC_FALL_RATE = 0.25
|
||||
RADAR_LEAD_ACC_MAX_DREL = 80.0
|
||||
RADAR_LEAD_ACC_MAX_TTC = 6.0
|
||||
RADAR_LEAD_ACC_MIN_CLOSING_SPEED = -0.5
|
||||
|
||||
SLOW_DOWN_PROB = 0.5
|
||||
SLOW_DOWN_EXIT_PROB = 0.4
|
||||
SLOW_DOWN_RISE_RATE = 0.65
|
||||
SLOW_DOWN_FALL_RATE = 0.15
|
||||
# Optimized slow down distance curve - smooth and progressive
|
||||
SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.]
|
||||
SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.]
|
||||
URGENT_SLOW_DOWN_PROB = 0.85
|
||||
|
||||
MODEL_DECEL_START = -0.5
|
||||
MODEL_DECEL_RANGE = 2.0
|
||||
ENDPOINT_URGENCY_GAIN = 1.3
|
||||
CRITICAL_ENDPOINT_FACTOR = 0.3
|
||||
CRITICAL_URGENCY_GAIN = 1.5
|
||||
SPEED_URGENCY_MIN = 25.0
|
||||
SPEED_URGENCY_RANGE = 80.0
|
||||
|
||||
SLOWNESS_PROB = 0.55
|
||||
SLOWNESS_EXIT_PROB = 0.45
|
||||
SLOWNESS_RISE_RATE = 0.35
|
||||
SLOWNESS_FALL_RATE = 0.5
|
||||
SLOWNESS_CRUISE_OFFSET = 1.025
|
||||
# Slowness detection parameters
|
||||
SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection
|
||||
SLOWNESS_PROB = 0.55 # Clear threshold for slowness
|
||||
SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset
|
||||
|
||||
@@ -6,121 +6,129 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
# Version = 2025-6-30
|
||||
|
||||
from cereal import messaging
|
||||
from opendbc.car import structs
|
||||
from numpy import interp
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||
from typing import Literal
|
||||
|
||||
from cereal import messaging
|
||||
from numpy import interp
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||
# d-e2e, from modeldata.h
|
||||
TRAJECTORY_SIZE = 33
|
||||
SET_MODE_TIMEOUT = 15
|
||||
|
||||
# Define the valid mode types
|
||||
ModeType = Literal['acc', 'blended']
|
||||
|
||||
|
||||
def clip01(value: float) -> float:
|
||||
return max(0.0, min(1.0, float(value)))
|
||||
class SmoothKalmanFilter:
|
||||
"""Enhanced Kalman filter with smoothing for stable decision making."""
|
||||
|
||||
def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01,
|
||||
alpha=1.0, smoothing_factor=0.85):
|
||||
self.x = initial_value
|
||||
self.P = 1.0
|
||||
self.R = measurement_noise
|
||||
self.Q = process_noise
|
||||
self.alpha = alpha
|
||||
self.smoothing_factor = smoothing_factor
|
||||
self.initialized = False
|
||||
self.history = []
|
||||
self.max_history = 10
|
||||
self.confidence = 0.0
|
||||
|
||||
class SmoothedSignal:
|
||||
def __init__(self, rise_rate: float, fall_rate: float, initial_value: float = 0.0):
|
||||
self.rise_rate = clip01(rise_rate)
|
||||
self.fall_rate = clip01(fall_rate)
|
||||
self.value = clip01(initial_value)
|
||||
def add_data(self, measurement):
|
||||
if len(self.history) >= self.max_history:
|
||||
self.history.pop(0)
|
||||
self.history.append(measurement)
|
||||
|
||||
def update(self, measurement: float) -> float:
|
||||
measurement = clip01(measurement)
|
||||
rate = self.rise_rate if measurement > self.value else self.fall_rate
|
||||
self.value += (measurement - self.value) * rate
|
||||
return self.value
|
||||
if not self.initialized:
|
||||
self.x = measurement
|
||||
self.initialized = True
|
||||
self.confidence = 0.1
|
||||
return
|
||||
|
||||
def reset(self, value: float = 0.0) -> None:
|
||||
self.value = clip01(value)
|
||||
self.P = self.alpha * self.P + self.Q
|
||||
|
||||
K = self.P / (self.P + self.R)
|
||||
effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1
|
||||
|
||||
class HysteresisSignal:
|
||||
def __init__(self, enter_threshold: float, exit_threshold: float, rise_rate: float, fall_rate: float):
|
||||
self.enter_threshold = clip01(enter_threshold)
|
||||
self.exit_threshold = clip01(exit_threshold)
|
||||
self.filter = SmoothedSignal(rise_rate, fall_rate)
|
||||
self.active = False
|
||||
innovation = measurement - self.x
|
||||
self.x = self.x + effective_K * innovation
|
||||
self.P = (1 - effective_K) * self.P
|
||||
|
||||
def update(self, measurement: float) -> bool:
|
||||
value = self.filter.update(measurement)
|
||||
threshold = self.exit_threshold if self.active else self.enter_threshold
|
||||
self.active = value > threshold
|
||||
return self.active
|
||||
if abs(innovation) < 0.1:
|
||||
self.confidence = min(1.0, self.confidence + 0.05)
|
||||
else:
|
||||
self.confidence = max(0.1, self.confidence - 0.02)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.filter.reset()
|
||||
self.active = False
|
||||
def get_value(self):
|
||||
return self.x if self.initialized else None
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.filter.value
|
||||
def get_confidence(self):
|
||||
return self.confidence
|
||||
|
||||
def reset_data(self):
|
||||
self.initialized = False
|
||||
self.history = []
|
||||
self.confidence = 0.0
|
||||
|
||||
|
||||
class ModeTransitionManager:
|
||||
"""Manages smooth transitions between driving modes with hysteresis."""
|
||||
|
||||
def __init__(self):
|
||||
self.current_mode: ModeType = 'acc'
|
||||
self.mode_confidence = {'acc': 1.0, 'blended': 0.0}
|
||||
self.transition_timeout = 0
|
||||
self.min_mode_duration = 10
|
||||
self.mode_duration = 0
|
||||
self._pending_mode: ModeType = 'acc'
|
||||
self._pending_count = 0
|
||||
self._blended_hold_frames = 0
|
||||
self.frames_since_switch = WMACConstants.TRANSITION_SMOOTH_FRAMES
|
||||
self.last_switch_was_immediate = False
|
||||
self.emergency_override = False
|
||||
|
||||
def request_mode(self, mode: ModeType, immediate: bool = False, hold_frames: int = 0, cancel_hold: bool = False) -> None:
|
||||
if immediate:
|
||||
self._blended_hold_frames = max(self._blended_hold_frames, hold_frames)
|
||||
self._pending_mode = mode
|
||||
self._pending_count = 0
|
||||
self._switch_mode(mode, immediate=True)
|
||||
def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False):
|
||||
# Emergency override for critical situations (stops, collisions)
|
||||
if emergency:
|
||||
self.emergency_override = True
|
||||
self.current_mode = mode
|
||||
self.transition_timeout = SET_MODE_TIMEOUT
|
||||
self.mode_duration = 0
|
||||
return
|
||||
|
||||
if cancel_hold and mode == 'acc':
|
||||
self._blended_hold_frames = 0
|
||||
self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence)
|
||||
for m in self.mode_confidence:
|
||||
if m != mode:
|
||||
self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05)
|
||||
|
||||
if self._blended_hold_frames > 0:
|
||||
mode = 'blended'
|
||||
|
||||
if mode == self.current_mode:
|
||||
self._pending_mode = mode
|
||||
self._pending_count = 0
|
||||
# Require minimum duration in current mode (unless emergency)
|
||||
if self.mode_duration < self.min_mode_duration and not self.emergency_override:
|
||||
return
|
||||
|
||||
if mode != self._pending_mode:
|
||||
self._pending_mode = mode
|
||||
self._pending_count = 1
|
||||
else:
|
||||
self._pending_count += 1
|
||||
# Hysteresis: higher threshold for mode changes
|
||||
confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response
|
||||
|
||||
if self.mode_duration < WMACConstants.MIN_MODE_DURATION[self.current_mode]:
|
||||
return
|
||||
if self.mode_confidence[mode] > confidence_threshold:
|
||||
if mode != self.current_mode and self.transition_timeout == 0:
|
||||
self.transition_timeout = SET_MODE_TIMEOUT
|
||||
self.current_mode = mode
|
||||
self.mode_duration = 0
|
||||
|
||||
required_count = WMACConstants.ENTER_BLENDED_FRAMES if mode == 'blended' else WMACConstants.EXIT_BLENDED_FRAMES
|
||||
if self._pending_count >= required_count:
|
||||
self._switch_mode(mode, immediate=False)
|
||||
|
||||
def update(self) -> None:
|
||||
if self._blended_hold_frames > 0:
|
||||
self._blended_hold_frames -= 1
|
||||
def update(self):
|
||||
if self.transition_timeout > 0:
|
||||
self.transition_timeout -= 1
|
||||
self.mode_duration += 1
|
||||
self.frames_since_switch += 1
|
||||
|
||||
# Reset emergency override after some time
|
||||
if self.emergency_override and self.mode_duration > 20:
|
||||
self.emergency_override = False
|
||||
|
||||
# Gradual confidence decay
|
||||
for mode in self.mode_confidence:
|
||||
self.mode_confidence[mode] *= 0.98
|
||||
|
||||
def get_mode(self) -> ModeType:
|
||||
return self.current_mode
|
||||
|
||||
def _switch_mode(self, mode: ModeType, immediate: bool) -> None:
|
||||
if mode == self.current_mode:
|
||||
return
|
||||
|
||||
self.current_mode = mode
|
||||
self.mode_duration = 0
|
||||
self._pending_mode = mode
|
||||
self._pending_count = 0
|
||||
self.frames_since_switch = 0
|
||||
self.last_switch_was_immediate = immediate
|
||||
|
||||
|
||||
class DynamicExperimentalController:
|
||||
def __init__(self, CP: structs.CarParams, mpc, params=None):
|
||||
@@ -134,33 +142,35 @@ class DynamicExperimentalController:
|
||||
|
||||
self._mode_manager = ModeTransitionManager()
|
||||
|
||||
self._lead_tracker = HysteresisSignal(
|
||||
enter_threshold=WMACConstants.LEAD_PROB,
|
||||
exit_threshold=WMACConstants.LEAD_EXIT_PROB,
|
||||
rise_rate=WMACConstants.LEAD_RISE_RATE,
|
||||
fall_rate=WMACConstants.LEAD_FALL_RATE,
|
||||
)
|
||||
self._radar_acc_lead_tracker = HysteresisSignal(
|
||||
enter_threshold=WMACConstants.RADAR_LEAD_ACC_PROB,
|
||||
exit_threshold=WMACConstants.RADAR_LEAD_ACC_EXIT_PROB,
|
||||
rise_rate=WMACConstants.RADAR_LEAD_ACC_RISE_RATE,
|
||||
fall_rate=WMACConstants.RADAR_LEAD_ACC_FALL_RATE,
|
||||
)
|
||||
self._slow_down_tracker = HysteresisSignal(
|
||||
enter_threshold=WMACConstants.SLOW_DOWN_PROB,
|
||||
exit_threshold=WMACConstants.SLOW_DOWN_EXIT_PROB,
|
||||
rise_rate=WMACConstants.SLOW_DOWN_RISE_RATE,
|
||||
fall_rate=WMACConstants.SLOW_DOWN_FALL_RATE,
|
||||
)
|
||||
self._slowness_tracker = HysteresisSignal(
|
||||
enter_threshold=WMACConstants.SLOWNESS_PROB,
|
||||
exit_threshold=WMACConstants.SLOWNESS_EXIT_PROB,
|
||||
rise_rate=WMACConstants.SLOWNESS_RISE_RATE,
|
||||
fall_rate=WMACConstants.SLOWNESS_FALL_RATE,
|
||||
# Smooth filters for stable decision making with faster response for critical scenarios
|
||||
self._lead_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.15,
|
||||
process_noise=0.05,
|
||||
alpha=1.02,
|
||||
smoothing_factor=0.8
|
||||
)
|
||||
|
||||
self._slow_down_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.1,
|
||||
process_noise=0.1,
|
||||
alpha=1.05,
|
||||
smoothing_factor=0.7
|
||||
)
|
||||
|
||||
self._slowness_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.1,
|
||||
process_noise=0.06,
|
||||
alpha=1.015,
|
||||
smoothing_factor=0.92
|
||||
)
|
||||
|
||||
self._mpc_fcw_filter = SmoothKalmanFilter(
|
||||
measurement_noise=0.2,
|
||||
process_noise=0.1,
|
||||
alpha=1.1,
|
||||
smoothing_factor=0.5
|
||||
)
|
||||
self._has_lead_filtered = False
|
||||
self._has_radar_acc_lead = False
|
||||
self._has_slow_down = False
|
||||
self._has_slowness = False
|
||||
self._has_mpc_fcw = False
|
||||
@@ -169,14 +179,13 @@ class DynamicExperimentalController:
|
||||
self._has_standstill = False
|
||||
self._mpc_fcw_crash_cnt = 0
|
||||
self._standstill_count = 0
|
||||
|
||||
# debug
|
||||
self._endpoint_x = float('inf')
|
||||
self._expected_distance = 0.0
|
||||
self._trajectory_valid = False
|
||||
self._raw_urgency = 0.0
|
||||
|
||||
def _read_params(self) -> None:
|
||||
if self._frame % WMACConstants.PARAM_READ_FRAMES == 0:
|
||||
if self._frame % int(1. / DT_MDL) == 0:
|
||||
self._enabled = self._params.get_bool("DynamicExperimentalControl")
|
||||
|
||||
def mode(self) -> str:
|
||||
@@ -188,32 +197,8 @@ class DynamicExperimentalController:
|
||||
def active(self) -> bool:
|
||||
return self._active
|
||||
|
||||
def has_radar_acc_lead(self) -> bool:
|
||||
# Same criterion _desired_mode() uses to force 'acc' mode: a near or fast-closing radar lead is trusted
|
||||
# enough that the e2e model's own opinion should never blend in over pure MPC. Computed every cycle
|
||||
# regardless of the DEC param/active() state (see _update_calculations, called unconditionally from
|
||||
# update()), so callers can use this as a lead-safety baseline independent of whether DEC itself is on.
|
||||
return not self._CP.radarUnavailable and self._has_radar_acc_lead
|
||||
|
||||
def is_urgent(self) -> bool:
|
||||
# Same "immediate" criterion _desired_mode() uses to decide a mode switch can't wait: an FCW-flagged MPC,
|
||||
# or a model slow-down whose smoothed hysteresis has latched AND whose raw severity clears the urgent
|
||||
# threshold. Public so callers outside the mode-switch decision (e.g. smoothing_transition) can reuse the
|
||||
# identical, already-tuned definition of "this is not a routine moment" instead of inventing a second one.
|
||||
return self._has_mpc_fcw or (self._has_slow_down and self._raw_urgency > WMACConstants.URGENT_SLOW_DOWN_PROB)
|
||||
|
||||
def smoothing_transition(self) -> bool:
|
||||
# True continuously while blended and NOT currently urgent -- not just for a short window right after the
|
||||
# switch. The e2e model's own action.desiredAcceleration can otherwise drop sharply frame-to-frame at any
|
||||
# point while steady-state in blended (not only at the switch instant), with zero jerk shaping from
|
||||
# either this fork's MPC-side relax (jerk_scale never touches the e2e path) or the old switch-scoped-only
|
||||
# version of this guard. Checking is_urgent() every cycle (not just "was the switch immediate") also
|
||||
# covers a new emergency that appears AFTER a routine switch, which the old snapshot-at-switch-time
|
||||
# check couldn't. Never true during a genuine emergency at any point, so it never delays/softens a real
|
||||
# stop the model or FCW is trying to make happen sooner.
|
||||
return self._mode_manager.current_mode == 'blended' and not self.is_urgent()
|
||||
|
||||
def set_mpc_fcw_crash_cnt(self) -> None:
|
||||
"""Set MPC FCW crash count"""
|
||||
self._mpc_fcw_crash_cnt = self._mpc.crash_cnt
|
||||
|
||||
def _update_calculations(self, sm: messaging.SubMaster) -> None:
|
||||
@@ -225,109 +210,179 @@ class DynamicExperimentalController:
|
||||
self._v_cruise_kph = car_state.vCruise
|
||||
self._has_standstill = car_state.standstill
|
||||
|
||||
# standstill detection
|
||||
if self._has_standstill:
|
||||
self._standstill_count = min(WMACConstants.STANDSTILL_FRAMES * 3, self._standstill_count + 1)
|
||||
self._standstill_count = min(20, self._standstill_count + 1)
|
||||
else:
|
||||
self._standstill_count = max(0, self._standstill_count - 1)
|
||||
|
||||
self._has_lead_filtered = self._lead_tracker.update(float(lead_one.status))
|
||||
self._has_radar_acc_lead = self._radar_acc_lead_tracker.update(self._radar_acc_lead_score(lead_one))
|
||||
self._has_mpc_fcw = self._mpc_fcw_crash_cnt > 0
|
||||
# Lead detection
|
||||
self._lead_filter.add_data(float(lead_one.status))
|
||||
lead_value = self._lead_filter.get_value() or 0.0
|
||||
self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB
|
||||
|
||||
# MPC FCW detection
|
||||
fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0
|
||||
self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0))
|
||||
self._has_mpc_fcw = fcw_filtered_value > 0.5
|
||||
|
||||
# Slow down detection
|
||||
self._calculate_slow_down(md)
|
||||
|
||||
if self._standstill_count > WMACConstants.STANDSTILL_FRAMES or self._has_slow_down:
|
||||
self._slowness_tracker.reset()
|
||||
self._has_slowness = False
|
||||
else:
|
||||
# Slowness detection
|
||||
if not (self._standstill_count > 5) and not self._has_slow_down:
|
||||
current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET))
|
||||
self._has_slowness = self._slowness_tracker.update(current_slowness)
|
||||
self._slowness_filter.add_data(current_slowness)
|
||||
slowness_value = self._slowness_filter.get_value() or 0.0
|
||||
|
||||
def _calculate_slow_down(self, md) -> None:
|
||||
# Hysteresis for slowness
|
||||
threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1)
|
||||
self._has_slowness = slowness_value > threshold
|
||||
|
||||
def _calculate_slow_down(self, md):
|
||||
"""Calculate urgency based on trajectory endpoint vs expected distance."""
|
||||
|
||||
# Reset to safe defaults
|
||||
urgency = 0.0
|
||||
self._endpoint_x = float('inf')
|
||||
self._expected_distance = 0.0
|
||||
self._trajectory_valid = False
|
||||
|
||||
urgency = self._model_action_urgency(md)
|
||||
position_valid = len(md.position.x) == WMACConstants.TRAJECTORY_SIZE
|
||||
#Require exact trajectory size
|
||||
position_valid = len(md.position.x) == TRAJECTORY_SIZE
|
||||
orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE
|
||||
|
||||
if position_valid:
|
||||
self._trajectory_valid = True
|
||||
self._endpoint_x = md.position.x[WMACConstants.TRAJECTORY_SIZE - 1]
|
||||
self._expected_distance = interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST)
|
||||
urgency = max(urgency, self._endpoint_urgency(self._endpoint_x, self._expected_distance))
|
||||
if not (position_valid and orientation_valid):
|
||||
# Invalid trajectory - this itself might indicate a stop scenario
|
||||
# Apply moderate urgency for incomplete trajectories at speed
|
||||
if self._v_ego_kph > 20.0:
|
||||
urgency = 0.3
|
||||
|
||||
self._raw_urgency = clip01(urgency)
|
||||
self._has_slow_down = self._slow_down_tracker.update(self._raw_urgency)
|
||||
self._urgency = self._slow_down_tracker.value
|
||||
self._slow_down_filter.add_data(urgency)
|
||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||
self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB
|
||||
self._urgency = urgency_filtered
|
||||
return
|
||||
|
||||
def _radar_acc_lead_score(self, lead_one) -> float:
|
||||
if not lead_one.status:
|
||||
return 0.0
|
||||
# We have a valid full trajectory
|
||||
self._trajectory_valid = True
|
||||
|
||||
d_rel = float(getattr(lead_one, 'dRel', float('inf')))
|
||||
v_rel = float(getattr(lead_one, 'vRel', 0.0))
|
||||
if d_rel <= WMACConstants.RADAR_LEAD_ACC_MAX_DREL:
|
||||
return 1.0
|
||||
if v_rel <= WMACConstants.RADAR_LEAD_ACC_MIN_CLOSING_SPEED and d_rel / max(-v_rel, 0.1) <= WMACConstants.RADAR_LEAD_ACC_MAX_TTC:
|
||||
return 1.0
|
||||
return 0.0
|
||||
# Use the exact endpoint (33rd point, index 32)
|
||||
endpoint_x = md.position.x[TRAJECTORY_SIZE - 1]
|
||||
self._endpoint_x = endpoint_x
|
||||
|
||||
def _model_action_urgency(self, md) -> float:
|
||||
action = getattr(md, 'action', None)
|
||||
if action is None:
|
||||
return 0.0
|
||||
# Get expected distance based on current speed using tuned constants
|
||||
expected_distance = interp(self._v_ego_kph,
|
||||
WMACConstants.SLOW_DOWN_BP,
|
||||
WMACConstants.SLOW_DOWN_DIST)
|
||||
self._expected_distance = expected_distance
|
||||
|
||||
urgency = 1.0 if getattr(action, 'shouldStop', False) else 0.0
|
||||
desired_accel = getattr(action, 'desiredAcceleration', 0.0)
|
||||
if desired_accel < WMACConstants.MODEL_DECEL_START:
|
||||
urgency = max(urgency, min(1.0, (WMACConstants.MODEL_DECEL_START - desired_accel) / WMACConstants.MODEL_DECEL_RANGE))
|
||||
return urgency
|
||||
# Calculate urgency based on trajectory shortage
|
||||
if endpoint_x < expected_distance:
|
||||
shortage = expected_distance - endpoint_x
|
||||
shortage_ratio = shortage / expected_distance
|
||||
|
||||
def _endpoint_urgency(self, endpoint_x: float, expected_distance: float) -> float:
|
||||
if endpoint_x >= expected_distance:
|
||||
return 0.0
|
||||
# Base urgency on shortage ratio
|
||||
urgency = min(1.0, shortage_ratio * 2.0)
|
||||
|
||||
shortage_ratio = (expected_distance - endpoint_x) / expected_distance
|
||||
urgency = min(1.0, shortage_ratio * WMACConstants.ENDPOINT_URGENCY_GAIN)
|
||||
# Increase urgency for very short trajectories (imminent stops)
|
||||
critical_distance = expected_distance * 0.3
|
||||
if endpoint_x < critical_distance:
|
||||
urgency = min(1.0, urgency * 2.0)
|
||||
|
||||
if endpoint_x < expected_distance * WMACConstants.CRITICAL_ENDPOINT_FACTOR:
|
||||
urgency = min(1.0, urgency * WMACConstants.CRITICAL_URGENCY_GAIN)
|
||||
# Speed-based urgency adjustment
|
||||
if self._v_ego_kph > 25.0:
|
||||
speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0
|
||||
urgency = min(1.0, urgency * speed_factor)
|
||||
|
||||
if self._v_ego_kph > WMACConstants.SPEED_URGENCY_MIN:
|
||||
speed_factor = 1.0 + (self._v_ego_kph - WMACConstants.SPEED_URGENCY_MIN) / WMACConstants.SPEED_URGENCY_RANGE
|
||||
urgency = min(1.0, urgency * speed_factor)
|
||||
# Apply filtering but with less smoothing for stops
|
||||
self._slow_down_filter.add_data(urgency)
|
||||
urgency_filtered = self._slow_down_filter.get_value() or 0.0
|
||||
|
||||
return urgency
|
||||
# Update state with lower threshold for better stop detection
|
||||
self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8)
|
||||
self._urgency = urgency_filtered
|
||||
|
||||
def _desired_mode(self) -> tuple[ModeType, bool]:
|
||||
if not self._CP.radarUnavailable and self._has_radar_acc_lead:
|
||||
return 'acc', False
|
||||
def _radarless_mode(self) -> None:
|
||||
"""Radarless mode decision logic with emergency handling."""
|
||||
|
||||
# EMERGENCY: MPC FCW - immediate blended mode
|
||||
if self._has_mpc_fcw:
|
||||
return 'blended', True
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
return
|
||||
|
||||
standstill = self._standstill_count > WMACConstants.STANDSTILL_FRAMES
|
||||
urgent_slow_down = self._has_slow_down and self._raw_urgency > WMACConstants.URGENT_SLOW_DOWN_PROB
|
||||
# Standstill: use blended
|
||||
if self._standstill_count > 3:
|
||||
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||
return
|
||||
|
||||
if self._CP.radarUnavailable:
|
||||
if standstill or self._has_slow_down:
|
||||
return 'blended', urgent_slow_down
|
||||
return 'acc', False
|
||||
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||
if self._has_slow_down:
|
||||
if self._urgency > 0.7:
|
||||
# Emergency: immediate blended mode for high urgency stops
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
else:
|
||||
# Normal: blended with urgency-based confidence
|
||||
confidence = min(1.0, self._urgency * 1.5)
|
||||
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||
return
|
||||
|
||||
if standstill or self._has_slow_down:
|
||||
return 'blended', urgent_slow_down
|
||||
# Driving slow: use ACC (but not if actively slowing down)
|
||||
if self._has_slowness and not self._has_slow_down:
|
||||
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||
return
|
||||
|
||||
return 'acc', False
|
||||
# Default: ACC
|
||||
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||
|
||||
def _radar_mode(self) -> None:
|
||||
"""Radar mode with emergency handling."""
|
||||
|
||||
# EMERGENCY: MPC FCW - immediate blended mode
|
||||
if self._has_mpc_fcw:
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
return
|
||||
|
||||
# If lead detected and not in standstill: always use ACC
|
||||
if self._has_lead_filtered and not (self._standstill_count > 3):
|
||||
self._mode_manager.request_mode('acc', confidence=1.0)
|
||||
return
|
||||
|
||||
# Slow down scenarios: emergency for high urgency, normal for lower urgency
|
||||
if self._has_slow_down:
|
||||
if self._urgency > 0.7:
|
||||
# Emergency: immediate blended mode for high urgency stops
|
||||
self._mode_manager.request_mode('blended', confidence=1.0, emergency=True)
|
||||
else:
|
||||
# Normal: blended with urgency-based confidence
|
||||
confidence = min(1.0, self._urgency * 1.3)
|
||||
self._mode_manager.request_mode('blended', confidence=confidence)
|
||||
return
|
||||
|
||||
# Standstill: use blended
|
||||
if self._standstill_count > 3:
|
||||
self._mode_manager.request_mode('blended', confidence=0.9)
|
||||
return
|
||||
|
||||
# Driving slow: use ACC (but not if actively slowing down)
|
||||
if self._has_slowness and not self._has_slow_down:
|
||||
self._mode_manager.request_mode('acc', confidence=0.8)
|
||||
return
|
||||
|
||||
# Default: ACC
|
||||
self._mode_manager.request_mode('acc', confidence=0.7)
|
||||
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
self._read_params()
|
||||
|
||||
self.set_mpc_fcw_crash_cnt()
|
||||
|
||||
self._update_calculations(sm)
|
||||
|
||||
mode, immediate = self._desired_mode()
|
||||
self._mode_manager.request_mode(mode, immediate=immediate, hold_frames=WMACConstants.EMERGENCY_HOLD_FRAMES,
|
||||
cancel_hold=self._has_radar_acc_lead)
|
||||
self._mode_manager.update()
|
||||
if self._CP.radarUnavailable:
|
||||
self._radarless_mode()
|
||||
else:
|
||||
self._radar_mode()
|
||||
|
||||
self._mode_manager.update()
|
||||
self._active = sm['selfdriveState'].experimentalMode and self._enabled
|
||||
self._frame += 1
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||
|
||||
class MockLeadOne:
|
||||
def __init__(self, status=0.0):
|
||||
self.status = status
|
||||
|
||||
class MockRadarState:
|
||||
def __init__(self, status=0.0):
|
||||
self.leadOne = MockLeadOne(status=status)
|
||||
|
||||
class MockCarState:
|
||||
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
||||
self.vEgo = vEgo
|
||||
self.vCruise = vCruise
|
||||
self.standstill = standstill
|
||||
|
||||
class MockModelData:
|
||||
def __init__(self, valid=True):
|
||||
size = 33 if valid else 10 # incomplete if invalid
|
||||
self.position = type("Pos", (), {"x": [0.0] * size})()
|
||||
self.orientation = type("Ori", (), {"x": [0.0] * size})()
|
||||
|
||||
class MockSelfDriveState:
|
||||
def __init__(self, experimentalMode=False):
|
||||
self.experimentalMode = experimentalMode
|
||||
|
||||
class MockParams:
|
||||
def get_bool(self, name):
|
||||
return True
|
||||
|
||||
@pytest.fixture
|
||||
def default_sm():
|
||||
sm = {
|
||||
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||
'radarState': MockRadarState(status=1.0),
|
||||
'modelV2': MockModelData(valid=True),
|
||||
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||
}
|
||||
return sm
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cp():
|
||||
class CP:
|
||||
radarUnavailable = False
|
||||
return CP()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mpc():
|
||||
class MPC:
|
||||
crash_cnt = 0
|
||||
return MPC()
|
||||
|
||||
# Fake Kalman Filter that always returns a given value
|
||||
class FakeKalman:
|
||||
def __init__(self, value=1.0):
|
||||
self.value = value
|
||||
def add_data(self, v): pass
|
||||
def get_value(self): return self.value
|
||||
def get_confidence(self): return 1.0
|
||||
def reset_data(self): pass
|
||||
|
||||
def test_initial_mode_is_acc(mock_cp, mock_mpc):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['carState'].standstill = True
|
||||
for _ in range(10):
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
mock_mpc.crash_cnt = 1 # simulate FCW
|
||||
for _ in range(2):
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
|
||||
# Force conditions to simulate slowdown
|
||||
controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown
|
||||
controller._v_ego_kph = 35.0
|
||||
default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory
|
||||
|
||||
for _ in range(3):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller.mode() == "blended"
|
||||
@@ -1,352 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, HysteresisSignal
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants
|
||||
|
||||
|
||||
class MockLeadOne:
|
||||
def __init__(self, status=0.0, dRel=30.0, vRel=0.0):
|
||||
self.status = status
|
||||
self.dRel = dRel
|
||||
self.vRel = vRel
|
||||
|
||||
|
||||
class MockRadarState:
|
||||
def __init__(self, status=0.0, dRel=30.0, vRel=0.0):
|
||||
self.leadOne = MockLeadOne(status=status, dRel=dRel, vRel=vRel)
|
||||
|
||||
|
||||
class MockCarState:
|
||||
def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False):
|
||||
self.vEgo = vEgo
|
||||
self.vCruise = vCruise
|
||||
self.standstill = standstill
|
||||
|
||||
|
||||
class MockAction:
|
||||
def __init__(self, desiredAcceleration=0.0, shouldStop=False):
|
||||
self.desiredAcceleration = desiredAcceleration
|
||||
self.shouldStop = shouldStop
|
||||
|
||||
|
||||
class MockModelData:
|
||||
def __init__(self, valid=True, endpoint_x=200.0, orientation_valid=None, desired_acceleration=0.0, should_stop=False):
|
||||
position_size = 33 if valid else 10
|
||||
orientation_size = position_size if orientation_valid is None else (33 if orientation_valid else 10)
|
||||
position_x = [0.0] * position_size
|
||||
if position_x:
|
||||
position_x[-1] = endpoint_x
|
||||
self.position = type("Pos", (), {"x": position_x})()
|
||||
self.orientation = type("Ori", (), {"x": [0.0] * orientation_size})()
|
||||
self.acceleration = type("Accel", (), {"x": [0.0] * position_size})()
|
||||
self.action = MockAction(desired_acceleration, should_stop)
|
||||
|
||||
|
||||
class MockSelfDriveState:
|
||||
def __init__(self, experimentalMode=False):
|
||||
self.experimentalMode = experimentalMode
|
||||
|
||||
|
||||
class MockParams:
|
||||
def get_bool(self, name):
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_sm():
|
||||
sm = {
|
||||
'carState': MockCarState(vEgo=10.0, vCruise=20.0),
|
||||
'radarState': MockRadarState(status=1.0),
|
||||
'modelV2': MockModelData(valid=True),
|
||||
'selfdriveState': MockSelfDriveState(experimentalMode=True),
|
||||
}
|
||||
return sm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cp():
|
||||
class CP:
|
||||
radarUnavailable = False
|
||||
return CP()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mpc():
|
||||
class MPC:
|
||||
crash_cnt = 0
|
||||
return MPC()
|
||||
|
||||
|
||||
def test_initial_mode_is_acc(mock_cp, mock_mpc):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['carState'].standstill = True
|
||||
for _ in range(20):
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
mock_mpc.crash_cnt = 1
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_smoothing_transition_false_while_never_switched(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "acc"
|
||||
assert not controller.smoothing_transition()
|
||||
|
||||
|
||||
def test_smoothing_transition_true_right_after_routine_switch_and_stays_true(mock_cp, mock_mpc, default_sm):
|
||||
# a standstill-driven switch is routine (not FCW/immediate) -- smoothing must engage at the switch and,
|
||||
# since the moment stays non-urgent, keep applying continuously (no more time-boxed lapse) as long as we
|
||||
# stay in blended and nothing urgent shows up -- see test_smoothing_transition_turns_off_when_urgency_rises
|
||||
# for the case that DOES turn it off.
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['carState'].standstill = True
|
||||
saw_switch_with_smoothing = False
|
||||
prev_mode = controller.mode()
|
||||
for _ in range(20):
|
||||
controller.update(default_sm)
|
||||
if controller.mode() == "blended" and prev_mode == "acc":
|
||||
saw_switch_with_smoothing = controller.smoothing_transition()
|
||||
break
|
||||
prev_mode = controller.mode()
|
||||
assert saw_switch_with_smoothing
|
||||
for _ in range(WMACConstants.TRANSITION_SMOOTH_FRAMES + 2):
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert controller.smoothing_transition() # still non-urgent -- no time-based lapse anymore
|
||||
|
||||
|
||||
def test_smoothing_transition_turns_off_when_urgency_rises_while_already_blended(mock_cp, mock_mpc, default_sm):
|
||||
# The gap this fixes: entering blended for a ROUTINE reason (standstill) must not permanently exempt a
|
||||
# LATER genuine emergency that appears while already blended -- the old last_switch_was_immediate snapshot
|
||||
# only looked at how we entered, never re-checked. is_urgent() re-checks every cycle instead.
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['carState'].standstill = True
|
||||
for _ in range(20):
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert controller.smoothing_transition()
|
||||
assert not controller.is_urgent()
|
||||
|
||||
mock_mpc.crash_cnt = 1 # a new FCW appears, still in blended (standstill keeps it there)
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended" # no mode switch needed/observed -- already blended
|
||||
assert controller.is_urgent()
|
||||
assert not controller.smoothing_transition() # must turn off immediately, same cycle
|
||||
|
||||
|
||||
def test_is_urgent_true_on_fcw(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
mock_mpc.crash_cnt = 1
|
||||
controller.update(default_sm)
|
||||
assert controller.is_urgent()
|
||||
|
||||
|
||||
def test_is_urgent_false_for_routine_slowdown_below_threshold(mock_cp, mock_mpc, default_sm):
|
||||
# a mild slow-down (not near the URGENT_SLOW_DOWN_PROB severity) must not read as urgent.
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, desired_acceleration=-0.6) # just past MODEL_DECEL_START
|
||||
controller.update(default_sm)
|
||||
assert controller._raw_urgency < WMACConstants.URGENT_SLOW_DOWN_PROB
|
||||
assert not controller.is_urgent()
|
||||
|
||||
|
||||
def test_smoothing_transition_false_for_emergency_switch(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
mock_mpc.crash_cnt = 1
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
assert not controller.smoothing_transition() # immediate/emergency switch must never be smoothed
|
||||
|
||||
|
||||
def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_valid_position_with_missing_orientation_can_trigger_slowdown(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0, orientation_valid=False)
|
||||
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._trajectory_valid
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_incomplete_position_does_not_trigger_slowdown(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=False, endpoint_x=0.0)
|
||||
|
||||
for _ in range(3):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert not controller._trajectory_valid
|
||||
assert not controller._has_slow_down
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_slowdown_hysteresis_prevents_threshold_chatter():
|
||||
signal = HysteresisSignal(enter_threshold=0.5, exit_threshold=0.4, rise_rate=1.0, fall_rate=1.0)
|
||||
|
||||
assert signal.update(0.55)
|
||||
assert signal.update(0.45)
|
||||
assert not signal.update(0.35)
|
||||
|
||||
|
||||
def test_model_should_stop_triggers_blended_without_valid_trajectory(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=False, should_stop=True)
|
||||
|
||||
controller.update(default_sm)
|
||||
|
||||
assert not controller._trajectory_valid
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_radar_lead_keeps_acc_over_model_slowdown(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
|
||||
for _ in range(3):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_slow_down
|
||||
assert controller._has_radar_acc_lead
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_far_radar_lead_allows_blended_until_acc_relevant(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_lead_filtered
|
||||
assert not controller._has_radar_acc_lead
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
|
||||
def test_relevant_radar_lead_smoothly_returns_to_acc(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
controller.update(default_sm)
|
||||
assert controller.mode() == "blended"
|
||||
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=45.0, vRel=0.0)
|
||||
for _ in range(20):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_radar_acc_lead
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_closing_far_radar_lead_returns_to_acc(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=-25.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
|
||||
for _ in range(20):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_radar_acc_lead
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_radar_lead_keeps_acc_over_fcw_and_standstill(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||
default_sm['carState'].standstill = True
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0, should_stop=True)
|
||||
mock_mpc.crash_cnt = 1
|
||||
|
||||
for _ in range(10):
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_lead_filtered
|
||||
assert controller._has_mpc_fcw
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_lead_flicker_hold_prevents_one_frame_mode_flip(mock_cp, mock_mpc, default_sm):
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0)
|
||||
controller.update(default_sm)
|
||||
|
||||
default_sm['radarState'] = MockRadarState(status=0.0)
|
||||
default_sm['modelV2'] = MockModelData(valid=True, endpoint_x=0.0)
|
||||
controller.update(default_sm)
|
||||
|
||||
assert controller._has_lead_filtered
|
||||
assert controller.mode() == "acc"
|
||||
|
||||
|
||||
def test_has_radar_acc_lead_true_for_near_lead(mock_cp, mock_mpc, default_sm):
|
||||
# within RADAR_LEAD_ACC_MAX_DREL -- available regardless of whether DEC's own param/active() is on, since
|
||||
# _update_calculations runs every cycle unconditionally.
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=40.0, vRel=0.0)
|
||||
controller.update(default_sm)
|
||||
assert controller.has_radar_acc_lead()
|
||||
|
||||
|
||||
def test_has_radar_acc_lead_false_for_far_slow_lead(mock_cp, mock_mpc, default_sm):
|
||||
# beyond MAX_DREL and not closing fast enough for the TTC gate -- correctly not trusted as an ACC-safe lead.
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=120.0, vRel=0.0)
|
||||
controller.update(default_sm)
|
||||
assert not controller.has_radar_acc_lead()
|
||||
|
||||
|
||||
def test_has_radar_acc_lead_false_when_radar_unavailable(mock_cp, mock_mpc, default_sm):
|
||||
mock_cp.radarUnavailable = True
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=40.0, vRel=0.0)
|
||||
controller.update(default_sm)
|
||||
assert not controller.has_radar_acc_lead()
|
||||
|
||||
|
||||
def test_has_radar_acc_lead_independent_of_dec_param(mock_cp, mock_mpc, default_sm):
|
||||
# DEC disabled (param False) must not affect this -- it's a lead-safety baseline other callers rely on
|
||||
# regardless of whether DEC itself is on.
|
||||
class MockParamsOff:
|
||||
def get_bool(self, name):
|
||||
return False
|
||||
controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParamsOff())
|
||||
default_sm['radarState'] = MockRadarState(status=1.0, dRel=40.0, vRel=0.0)
|
||||
controller.update(default_sm)
|
||||
assert not controller.enabled()
|
||||
assert not controller.active()
|
||||
assert controller.has_radar_acc_lead()
|
||||
@@ -9,8 +9,6 @@ from cereal import messaging, custom
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.radar_distance.radar_distance import RadarDistanceController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||
@@ -22,40 +20,12 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState
|
||||
LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource
|
||||
|
||||
# Bounds how fast output_a_target may drop while blended and NOT currently urgent (continuous -- not just a
|
||||
# short window right after the switch; see DynamicExperimentalController.smoothing_transition()/is_urgent()).
|
||||
# Never active during a genuine emergency (FCW, or a model slow-down past the urgent-severity threshold) at
|
||||
# any point, whether that was true at the moment of the switch or only became true later while still blended.
|
||||
TRANSITION_MAX_DROP_PER_CYCLE = 0.15 # m/s^2 per cycle
|
||||
|
||||
|
||||
class _E2ETransitionGuard:
|
||||
# Without this, the e2e model's own action.desiredAcceleration -- which the core MPC-side jerk_scale never
|
||||
# shapes (it only touches the MPC's own solution, not this raw model path) -- can drop sharply frame-to-
|
||||
# frame at any point while blended, not only at the instant DEC switches into it, producing a discontinuous
|
||||
# brake with zero jerk shaping. Only ever limits a DOWNWARD move; never delays a rise.
|
||||
def __init__(self):
|
||||
self._last = None
|
||||
|
||||
def reset(self) -> None:
|
||||
self._last = None
|
||||
|
||||
def apply(self, output_a_target: float, smoothing_active: bool) -> float:
|
||||
if not smoothing_active or self._last is None:
|
||||
self._last = output_a_target
|
||||
return output_a_target
|
||||
limited = max(output_a_target, self._last - TRANSITION_MAX_DROP_PER_CYCLE)
|
||||
self._last = limited
|
||||
return limited
|
||||
|
||||
|
||||
class LongitudinalPlannerSP:
|
||||
def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP, mpc):
|
||||
self.events_sp = EventsSP()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.dec = DynamicExperimentalController(CP, mpc)
|
||||
self.accel = AccelController(CP, mpc)
|
||||
self.radar_distance = RadarDistanceController(CP)
|
||||
self.scc = SmartCruiseControl()
|
||||
self.resolver = SpeedLimitResolver()
|
||||
self.sla = SpeedLimitAssist(CP, CP_SP)
|
||||
@@ -65,24 +35,13 @@ class LongitudinalPlannerSP:
|
||||
|
||||
self.output_v_target = 0.
|
||||
self.output_a_target = 0.
|
||||
self._e2e_transition_guard = _E2ETransitionGuard()
|
||||
|
||||
def is_e2e(self, sm: messaging.SubMaster) -> bool:
|
||||
experimental_mode = sm['selfdriveState'].experimentalMode
|
||||
if not experimental_mode:
|
||||
return False
|
||||
|
||||
# A near/fast-closing radar lead always routes to pure MPC, regardless of whether DEC itself is on --
|
||||
# this baseline is not something the DEC toggle should be able to bypass (dec.has_radar_acc_lead() is
|
||||
# computed every cycle independent of dec.active()). DEC's own toggle only gates the OTHER blended
|
||||
# triggers (standstill, slow-down, FCW) below.
|
||||
if self.dec.has_radar_acc_lead():
|
||||
return False
|
||||
|
||||
if not self.dec.active():
|
||||
return True
|
||||
return experimental_mode
|
||||
|
||||
return self.dec.mode() == "blended"
|
||||
return experimental_mode and self.dec.mode() == "blended"
|
||||
|
||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||
CS = sm['carState']
|
||||
@@ -117,16 +76,8 @@ class LongitudinalPlannerSP:
|
||||
def update(self, sm: messaging.SubMaster) -> None:
|
||||
self.events_sp.clear()
|
||||
self.dec.update(sm)
|
||||
self.accel.update(sm)
|
||||
self.radar_distance.update(sm)
|
||||
self.e2e_alerts_helper.update(sm, self.events_sp)
|
||||
|
||||
def smooth_radarstate(self, radarstate):
|
||||
return self.radar_distance.smooth_radarstate(radarstate)
|
||||
|
||||
def smooth_e2e_transition(self, output_a_target: float) -> float:
|
||||
return self._e2e_transition_guard.apply(output_a_target, self.dec.smoothing_transition())
|
||||
|
||||
def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None:
|
||||
plan_sp_send = messaging.new_message('longitudinalPlanSP')
|
||||
|
||||
@@ -187,17 +138,4 @@ class LongitudinalPlannerSP:
|
||||
e2eAlerts.greenLightAlert = self.e2e_alerts_helper.green_light_alert
|
||||
e2eAlerts.leadDepartAlert = self.e2e_alerts_helper.lead_depart_alert
|
||||
|
||||
# Acceleration Personality (telemetry only; brakeNeed/decelTarget/smoothActive repurposed for the
|
||||
# input-shaping design -- see cereal custom.capnp Acceleration).
|
||||
acceleration = longitudinalPlanSP.acceleration
|
||||
acceleration.personality = self.accel.personality()
|
||||
acceleration.enabled = self.accel.enabled()
|
||||
acceleration.maxAccel = float(self.accel.max_accel())
|
||||
acceleration.brakeNeed = float(self.accel.follow_widen()) # follow-gap widen added on top of stock (s)
|
||||
acceleration.decelTarget = float(self.accel.t_follow()) # t_follow handed to the MPC (s)
|
||||
acceleration.smoothActive = self.accel.widen_active() # follow-gap widen currently active
|
||||
acceleration.bypassed = False # unused (no output shaping / bypass anymore)
|
||||
acceleration.leadUnstable = bool(self.radar_distance.lead_unstable())
|
||||
|
||||
|
||||
pm.send('longitudinalPlanSP', plan_sp_send)
|
||||
|
||||
@@ -1,449 +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.
|
||||
|
||||
RadarDistance conditions the lead the longitudinal MPC follows on a noisy (TSS2-class) radar. It NEVER
|
||||
reports a farther-or-faster lead than reality, so braking is always >= stock. Four mechanisms:
|
||||
* jump-guard: reject a same-cycle FARTHER dRel jump on a lead that never dropped status (a vision/radar
|
||||
fusion transient during lead acquisition -- e.g. a cut-in whose vision distance estimate briefly
|
||||
disagrees with a solid radar track) by holding the last-trusted, closer reading instead of snapping back
|
||||
out. A closer jump of any size always passes immediately -- this only ever delays relief, never a brake;
|
||||
* flicker-hold: keep a just-dropped, recently-sustained lead alive (dead-reckoned) through a brief radar
|
||||
dropout so the MPC does not lose and re-grab it (which reads as a phantom release then a catch-up brake);
|
||||
* churn/noise smoother: a short EMA on a lead's dRel/vLead/vRel so the MPC stops hunting the gap (removes
|
||||
the follow-jitter that reads as rubber-banding and, on the sensor side, as a lead-detection "lurch").
|
||||
Covers two DISTINCT same-physical-object noise signatures: trackId churn (id flips frame-to-frame but the
|
||||
kinematics stay coherent -- one real lead getting re-labeled) and same-track noise (id stays constant but
|
||||
vLead itself is bimodal/bouncing -- one real lead with a noisy fusion/Doppler velocity read). Both are
|
||||
safe to EMA because the id evidence pins them to a SINGLE physical object; a bimodal vLead WITH the id
|
||||
also changing is left alone (ambiguous -- could be two really-different real objects) so this can never
|
||||
average two real tracks together. dRel is asymmetric -- closer accepted immediately, only farther is
|
||||
EMA-lagged -- so it can't hold a steadily-closing lead farther-than-true; vLead/vRel stay symmetric;
|
||||
* stop-gap: near a (near-)stopped lead at low speed report dRel a touch closer so the MPC's own smooth stop
|
||||
settles farther back (the Prius TSS2 stock crawl creeps in to ~1.5 m). Monotone (closer => brake >= stock).
|
||||
Overridden off by sustained lead motion (even slow creep) so it can't suppress a real, growing gap during
|
||||
a launch. Never runs on a held (jump-guard or flicker-hold) lead, since a hold's vLead/dRel are stale;
|
||||
Also publishes a read-only lead-instability flag (telemetry). Disabled => byte-stock passthrough always.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
from opendbc.car import structs
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
HOLD_MAX_FRAMES = 20 # ~1.0s flicker-hold cap, since the last sustained lead
|
||||
SUSTAIN_FRAMES = 2
|
||||
DROPOUT_DREL = 1.0
|
||||
FCW_PROB_CAP = 0.9
|
||||
MIN_HELD_DREL = 0.5
|
||||
|
||||
LOW_SPEED_PASSTHROUGH_V = 5.0 # m/s: below this, no flicker-hold (holding a stale lead near a stop would
|
||||
# delay the launch); the churn smoother still runs down to CREEP_PASSTHROUGH_V
|
||||
CREEP_PASSTHROUGH_V = 1.0 # m/s: below this, full byte-stock passthrough (protect the stock stop distance)
|
||||
|
||||
SWITCH_DREL = 4.0 # m, dRel jump = a track switch (used by the instability detector + jump-guard).
|
||||
|
||||
# Jump-guard: a same-cycle dRel jump this far FARTHER, on a lead that never dropped status, is treated as a
|
||||
# fusion transient (not a real sudden separation) and held at the last-trusted value for a bounded number of
|
||||
# frames. Self-heals fast so a genuinely-departing lead is never held stale for long.
|
||||
JUMP_GUARD_MAX_HOLD = 10 # frames (~0.5s)
|
||||
|
||||
# How many frames a _JumpGuard/_LeadSmoother reference can go without a step()/update() call (below the speed
|
||||
# gate that call is skipped entirely) before it's discarded as stale rather than diffed/EMA'd against. Must be
|
||||
# small next to a real stop (the bug this guards against ran 3200+ frames/160s) but comfortably bigger than an
|
||||
# incidental single skipped cycle from v_ego dithering right at LOW_SPEED_PASSTHROUGH_V/CREEP_PASSTHROUGH_V --
|
||||
# a flat ">1" here defeated the same-cycle farther-jump guard on every dithered cycle and let a real jitter
|
||||
# glitch leak into the smoother's EMA for up to ~1s.
|
||||
REFERENCE_STALE_FRAMES = 20 # ~1.0s
|
||||
|
||||
# Lead-instability detector (telemetry only): flags a bimodal/bouncing radar lead.
|
||||
STABILITY_WINDOW = 5 # frames (~0.25s @ 20Hz)
|
||||
VLEAD_SPREAD = 4.0 # m/s, vLead range over the window above which the lead is "unstable"
|
||||
ID_CHURN_WINDOW = 10 # frames (~0.5s) for radarTrackId-churn detection (steady lead, flipping ids)
|
||||
ID_CHURN = 3 # trackId switches in the window above which the lead is "unstable" (follow-hunting)
|
||||
|
||||
# Churn smoother: during trackId churn the per-track dRel/vRel jitter makes the MPC hunt the follow gap. A
|
||||
# short SYMMETRIC EMA on the churning lead removes the jitter so the MPC sees a steady lead. Active ONLY
|
||||
# during churn (NOT bimodal vLead -> never averages two real tracks). Bounded symmetric lag ~LEAD_SMOOTH_TAU.
|
||||
LEAD_SMOOTH_TAU = 0.5 # s, EMA time constant
|
||||
LEAD_SMOOTH_HOLD = 20 # frames (~1s): keep smoothing through brief churn gaps (churn toggles on/off)
|
||||
|
||||
# Stop-gap: near a (near-)stopped lead at low speed, report dRel up to STOP_GAP_M closer so the MPC's own
|
||||
# smooth stop terminates that much farther back (stock Prius crawl-creeps to ~1.5 m). Monotone (only reports
|
||||
# closer => brake >= stock). Ramps in below the regime dRel and out as the lead starts moving; releases on
|
||||
# launch as ego speed rises past STOP_GAP_VEGO.
|
||||
STOP_GAP_M = 2.5 # m: max dRel reduction = added standstill gap
|
||||
STOP_GAP_VEGO = 8.0 # m/s: only below this ego speed
|
||||
STOP_GAP_VLEAD = 1.5 # m/s: only behind a (near-)stopped lead; ramps out as vLead rises to this
|
||||
STOP_GAP_REGIME_DREL = 12.0 # m: bias ramps in below this dRel
|
||||
STOP_GAP_RAMP_BAND = 2.0 # m: ramp-in band (full offset below REGIME_DREL - RAMP_BAND)
|
||||
STOP_GAP_MIN_DREL = 2.0 # m: never report a lead closer than this
|
||||
|
||||
# Stop-gap creep override: a lead creeping forward slowly can sit under STOP_GAP_VLEAD for many seconds
|
||||
# without crossing it, so the bias keeps suppressing a real, growing gap. Sustained motion this long overrides
|
||||
# the bias off regardless of how slow. The counter decays (not just holds) on a sub-threshold frame, so it
|
||||
# takes sustained motion, not noise straddling the threshold, to reach the cap.
|
||||
STOP_GAP_CREEP_V = 0.03 # m/s: a truly-stopped lead reads exactly 0.0; treat anything above this as motion
|
||||
STOP_GAP_CREEP_HOLD_S = 1.5 # s: this much sustained motion overrides the bias off
|
||||
STOP_GAP_CREEP_HOLD_FRAMES = int(STOP_GAP_CREEP_HOLD_S / DT_MDL)
|
||||
|
||||
|
||||
class _BiasedLead:
|
||||
__slots__ = ('status', 'dRel', 'yRel', 'vRel', 'vLead', 'vLeadK', 'aLeadK', 'aLeadTau', 'modelProb')
|
||||
|
||||
def __init__(self, src, dRel):
|
||||
self.status = src.status
|
||||
self.dRel = dRel
|
||||
self.yRel = src.yRel
|
||||
self.vRel = src.vRel
|
||||
self.vLead = src.vLead
|
||||
self.vLeadK = src.vLeadK
|
||||
self.aLeadK = src.aLeadK
|
||||
self.aLeadTau = src.aLeadTau
|
||||
self.modelProb = src.modelProb
|
||||
|
||||
|
||||
class _SmoothedLead:
|
||||
__slots__ = ('status', 'dRel', 'yRel', 'vRel', 'vLead', 'vLeadK', 'aLeadK', 'aLeadTau', 'modelProb')
|
||||
|
||||
def __init__(self, src, dRel, vLead, vRel):
|
||||
self.status = src.status
|
||||
self.dRel = dRel
|
||||
self.yRel = src.yRel
|
||||
self.vRel = vRel
|
||||
self.vLead = vLead
|
||||
self.vLeadK = vLead
|
||||
self.aLeadK = src.aLeadK
|
||||
self.aLeadTau = src.aLeadTau
|
||||
self.modelProb = src.modelProb
|
||||
|
||||
|
||||
class _HeldLead:
|
||||
__slots__ = ('status', 'dRel', 'yRel', 'vRel', 'vLead', 'vLeadK', 'aLeadK', 'aLeadTau', 'modelProb')
|
||||
|
||||
def __init__(self, dRel, vRel, vLead, aLeadK, aLeadTau, modelProb):
|
||||
self.status = True
|
||||
self.dRel = dRel
|
||||
self.vRel = vRel
|
||||
self.vLead = vLead
|
||||
self.vLeadK = vLead
|
||||
self.aLeadK = aLeadK
|
||||
self.aLeadTau = aLeadTau
|
||||
self.modelProb = modelProb
|
||||
self.yRel = 0.0
|
||||
|
||||
|
||||
class _RadarStateProxy:
|
||||
__slots__ = ('leadOne', 'leadTwo')
|
||||
|
||||
def __init__(self, lead_one, lead_two):
|
||||
self.leadOne = lead_one
|
||||
self.leadTwo = lead_two
|
||||
|
||||
|
||||
class _LeadSmoother:
|
||||
# EMA on a noisy same-physical-object lead's dRel/vLead/vRel (jitter removal; see _LeadStability for what
|
||||
# qualifies as "same object"). A hold keeps it active through brief noise gaps (the trigger toggles on/off);
|
||||
# passthrough + reset only after the hold lapses. dRel is ASYMMETRIC: a closer raw reading is accepted
|
||||
# immediately (never delay awareness of closer -- the file's own invariant), only a FARTHER raw reading is
|
||||
# EMA-lagged (reject noise in that direction). Without this, a lead that's genuinely closing steadily while
|
||||
# noisy (even briefly) gets held farther-than-true for the full LEAD_SMOOTH_HOLD window, then snaps -- a
|
||||
# false-relief-then-correction that itself becomes a hard brake.
|
||||
def __init__(self):
|
||||
self._d = None
|
||||
self._vl = None
|
||||
self._vr = None
|
||||
self._hold = 0
|
||||
self._last_frame = 0
|
||||
|
||||
def reset(self):
|
||||
self._d = None
|
||||
self._vl = None
|
||||
self._vr = None
|
||||
self._hold = 0
|
||||
self._last_frame = 0
|
||||
|
||||
def update(self, lead, noisy: bool, frame: int):
|
||||
# A held lead's dRel/vLead is a stale extrapolation -- feeding it into the EMA would both hide that from
|
||||
# downstream (wraps it into a _SmoothedLead) and pollute _d/_vl/_vr, lagging the real value's recovery.
|
||||
if isinstance(lead, _HeldLead):
|
||||
self.reset()
|
||||
return lead
|
||||
# update() is only called above CREEP_PASSTHROUGH_V (see smooth_radarstate), so _hold and the EMA state
|
||||
# (_d/_vl/_vr) freeze for the entire duration of any full standstill. Resuming and EMA-ing a real, opening
|
||||
# lead against that frozen _d as if no time had passed lags dRel toward the stale, closer pre-stop value
|
||||
# for up to LEAD_SMOOTH_TAU-ish seconds -- same bug class as _JumpGuard/_LeadHold's frame-based staleness
|
||||
# fixes. Treat a gap since the last call larger than REFERENCE_STALE_FRAMES (not a flat 1) as no state at
|
||||
# all, so an incidental single skipped cycle from v_ego dithering right at the gate doesn't itself wipe the
|
||||
# EMA's short-term jitter-suppression memory.
|
||||
if self._last_frame and frame - self._last_frame > REFERENCE_STALE_FRAMES:
|
||||
self.reset()
|
||||
self._last_frame = frame
|
||||
self._hold = LEAD_SMOOTH_HOLD if noisy else self._hold - 1
|
||||
if self._hold <= 0 or not lead.status:
|
||||
self.reset()
|
||||
return lead
|
||||
if self._d is None:
|
||||
self._d, self._vl, self._vr = lead.dRel, lead.vLead, lead.vRel
|
||||
return lead
|
||||
a = DT_MDL / LEAD_SMOOTH_TAU
|
||||
self._d = lead.dRel if lead.dRel < self._d else self._d + (lead.dRel - self._d) * a
|
||||
self._vl += (lead.vLead - self._vl) * a
|
||||
self._vr += (lead.vRel - self._vr) * a
|
||||
return _SmoothedLead(lead, self._d, self._vl, self._vr)
|
||||
|
||||
|
||||
class _JumpGuard:
|
||||
# Rejects a same-cycle FARTHER dRel jump on a lead that never dropped status (a vision/radar fusion
|
||||
# transient, e.g. a cut-in whose vision distance estimate briefly disagrees with a solid radar track before
|
||||
# the match locks on) by holding the last-trusted reading, extrapolated by its own vRel, for a bounded
|
||||
# number of frames. A CLOSER jump of any size always passes through immediately -- this can only ever delay
|
||||
# relief, never a brake -- and it self-heals after JUMP_GUARD_MAX_HOLD frames if the jump was real.
|
||||
# modelProb is capped at FCW_PROB_CAP on the held lead (same as _LeadHold's flicker-hold, below) -- a held
|
||||
# reading is no longer confirmed fresh, so it must not carry enough confidence to trip the stock FCW gate.
|
||||
def __init__(self):
|
||||
self._last = None
|
||||
self._last_frame = 0
|
||||
self._hold = 0
|
||||
self._grace_used = False
|
||||
|
||||
def reset(self):
|
||||
self._last = None
|
||||
self._last_frame = 0
|
||||
self._hold = 0
|
||||
self._grace_used = False
|
||||
|
||||
def step(self, raw, frame):
|
||||
if not raw.status:
|
||||
self.reset()
|
||||
return raw
|
||||
|
||||
# _last is only a valid reference to diff against if it's recent. smooth_radarstate() stops calling step()
|
||||
# below LOW_SPEED_PASSTHROUGH_V (see _LeadHold), so after any low-speed gap (a full stop, a slow zone)
|
||||
# _last can be arbitrarily many real seconds old. Diffing a fresh reading against a stale one as if it were
|
||||
# a same-cycle transient rejects a real, large, entirely legitimate change (e.g. a lead that pulled away
|
||||
# during the gap) as a fusion glitch and holds a phantom lead -- measured causing a hard, unwarranted brake
|
||||
# on a real route (launch from a stop after the lead had long since moved on). Treat a stale _last as no
|
||||
# reference at all: pass raw through and re-baseline. REFERENCE_STALE_FRAMES (not a flat 1) so an
|
||||
# incidental single skipped cycle from v_ego dithering right at the gate doesn't itself defeat the guard.
|
||||
stale = self._last is not None and (frame - self._last_frame) > REFERENCE_STALE_FRAMES
|
||||
|
||||
if not stale and self._last is not None and (raw.dRel - self._last[0]) > SWITCH_DREL and self._hold < JUMP_GUARD_MAX_HOLD:
|
||||
dRel0, vRel0, vLead0, aLeadK0, aLeadTau0, prob0 = self._last
|
||||
self._hold += 1
|
||||
held_dRel = max(MIN_HELD_DREL, dRel0 - max(-vRel0, 0.0) * DT_MDL)
|
||||
self._last = (held_dRel, vRel0, vLead0, aLeadK0, aLeadTau0, prob0)
|
||||
self._last_frame = frame
|
||||
return _HeldLead(held_dRel, vRel0, vLead0, aLeadK0, aLeadTau0, min(prob0, FCW_PROB_CAP))
|
||||
|
||||
# Hold cap reached on a lead that was closing: self-healing straight onto raw here would adopt a farther
|
||||
# reading than the trajectory already tracked, i.e. report a farther lead than reality for at least one
|
||||
# more cycle. Take exactly one bounded extra cycle at the last-held value first -- never a second, so this
|
||||
# can't turn into an indefinite hold on a lead that genuinely departed.
|
||||
if (not stale and self._hold >= JUMP_GUARD_MAX_HOLD and not self._grace_used and self._last is not None and
|
||||
self._last[1] < 0.0 and (raw.dRel - self._last[0]) > SWITCH_DREL):
|
||||
dRel0, vRel0, vLead0, aLeadK0, aLeadTau0, prob0 = self._last
|
||||
self._grace_used = True
|
||||
self._last_frame = frame
|
||||
return _HeldLead(dRel0, vRel0, vLead0, aLeadK0, aLeadTau0, min(prob0, FCW_PROB_CAP))
|
||||
|
||||
self._hold = 0
|
||||
self._grace_used = False
|
||||
self._last = (raw.dRel, raw.vRel, raw.vLead, raw.aLeadK, raw.aLeadTau, raw.modelProb)
|
||||
self._last_frame = frame
|
||||
return raw
|
||||
|
||||
|
||||
class _LeadHold:
|
||||
# step() takes the caller's absolute frame counter rather than counting its own calls: below
|
||||
# LOW_SPEED_PASSTHROUGH_V the caller stops calling step() at all (see smooth_radarstate), and a
|
||||
# self-incrementing counter would then stay frozen at whatever it was for however long that lasts -- on
|
||||
# resume it would read as "just a few frames since the last real sighting" no matter how much real time
|
||||
# (a full stop, a slow zone) actually passed, and could hand HOLD_MAX_FRAMES worth of stale credit to a
|
||||
# sighting from arbitrarily long ago. Comparing against the caller's frame counter makes the elapsed-frames
|
||||
# check correct regardless of how many cycles were skipped in between.
|
||||
def __init__(self):
|
||||
self._last = None
|
||||
self._sustained = 0
|
||||
self._real_frame = 0
|
||||
self._armed = False
|
||||
self._held_dRel = 0.0
|
||||
self._holding = False # true once this hold episode has been reseeded from a real reading
|
||||
|
||||
def reset(self):
|
||||
self.__init__()
|
||||
|
||||
def step(self, raw, frame):
|
||||
if raw.status and raw.dRel > DROPOUT_DREL:
|
||||
self._last = (raw.dRel, raw.vRel, raw.vLead, raw.aLeadK, raw.aLeadTau, raw.modelProb)
|
||||
self._sustained += 1
|
||||
if self._sustained >= SUSTAIN_FRAMES:
|
||||
self._real_frame = frame
|
||||
self._armed = True
|
||||
self._holding = False # back on a real sighting -- the next dropout starts a fresh hold episode
|
||||
return raw
|
||||
|
||||
self._sustained = 0
|
||||
since_real = frame - self._real_frame
|
||||
if self._armed and self._last is not None and since_real <= HOLD_MAX_FRAMES:
|
||||
dRel0, vRel0, vLead0, aLeadK0, aLeadTau0, prob0 = self._last
|
||||
# Reseed _held_dRel from the real last-known value exactly once per hold episode, on whichever call
|
||||
# first starts holding -- NOT on since_real==1: since_real is elapsed REAL frames (see class docstring),
|
||||
# so any low-speed gap in between (step() skipped) makes since_real > 1 on the very first dropout call
|
||||
# actually made, and comparing it to 1 would silently skip the reseed -- leaving _held_dRel at its
|
||||
# stale/init value (0.0), which the next line's floor then clamps to MIN_HELD_DREL: a fabricated
|
||||
# near-bumper phantom lead, not a dead-reckoned extrapolation of the real one.
|
||||
if not self._holding:
|
||||
self._held_dRel = dRel0
|
||||
self._holding = True
|
||||
self._held_dRel = max(MIN_HELD_DREL, self._held_dRel - max(-vRel0, 0.0) * DT_MDL)
|
||||
return _HeldLead(self._held_dRel, vRel0, vLead0, min(aLeadK0, 0.0), aLeadTau0, min(prob0, FCW_PROB_CAP))
|
||||
|
||||
self._armed = False
|
||||
self._holding = False
|
||||
return raw
|
||||
|
||||
|
||||
class _LeadStability:
|
||||
# Read-only monitor: flags an unstable leadOne -- bimodal/bouncing vLead, dRel track-switch jumps, or
|
||||
# radarTrackId churn (a steady lead flipping track ids -> vRel jitter -> follow-hunting). Telemetry only.
|
||||
# Also derives same_track_noise: a bimodal/bouncing vLead while radarTrackId sat CONSTANT the whole window
|
||||
# -- i.e. the id evidence pins the noise to one physical object (a Doppler/fusion-noisy velocity read on one
|
||||
# real lead), so it is safe to feed the smoother (see _LeadSmoother). A bimodal vLead WITH the id also
|
||||
# changing stays outside same_track_noise (could be two really-different real objects at different speeds)
|
||||
# and is left unmitigated, same as before. dRel track-jumps are deliberately excluded here: while status
|
||||
# stays True (this class's own precondition), a repeated FARTHER dRel jump is already absorbed by
|
||||
# _JumpGuard upstream (same SWITCH_DREL threshold), so adding it here would just double up on the same
|
||||
# signal rather than covering a real gap.
|
||||
def __init__(self):
|
||||
self._v = deque(maxlen=STABILITY_WINDOW)
|
||||
self._d = deque(maxlen=STABILITY_WINDOW)
|
||||
self._id = deque(maxlen=ID_CHURN_WINDOW)
|
||||
self.unstable = False
|
||||
self.churn = False
|
||||
self.same_track_noise = False
|
||||
|
||||
def reset(self):
|
||||
self._v.clear()
|
||||
self._d.clear()
|
||||
self._id.clear()
|
||||
self.unstable = False
|
||||
self.churn = False
|
||||
self.same_track_noise = False
|
||||
|
||||
def update(self, lead, v_ego: float) -> None:
|
||||
if not lead.status or v_ego < CREEP_PASSTHROUGH_V:
|
||||
self.reset()
|
||||
return
|
||||
self._v.append(float(lead.vLead))
|
||||
self._d.append(float(lead.dRel))
|
||||
self._id.append(int(getattr(lead, 'radarTrackId', -1)))
|
||||
if len(self._v) < STABILITY_WINDOW:
|
||||
self.unstable = False
|
||||
return
|
||||
v_spread = max(self._v) - min(self._v)
|
||||
d_jumps = sum(abs(b - a) > SWITCH_DREL for a, b in zip(self._d, list(self._d)[1:], strict=False))
|
||||
ids = list(self._id)
|
||||
id_churn = sum(1 for a, b in zip(ids, ids[1:], strict=False) if a != b and a > 0 and b > 0)
|
||||
recent_ids = ids[-STABILITY_WINDOW:]
|
||||
same_track = recent_ids[0] > 0 and len(set(recent_ids)) == 1
|
||||
self.churn = id_churn >= ID_CHURN and v_spread <= VLEAD_SPREAD # steady lead, flipping ids (not bimodal)
|
||||
self.same_track_noise = same_track and v_spread > VLEAD_SPREAD
|
||||
self.unstable = v_spread > VLEAD_SPREAD or d_jumps >= 2 or self.churn
|
||||
|
||||
|
||||
class RadarDistanceController:
|
||||
def __init__(self, CP: structs.CarParams, params=None):
|
||||
# CP accepted for the planner's constructor signature; unused.
|
||||
self._params = params or Params()
|
||||
self._frame = 0
|
||||
self._v_ego = 0.0
|
||||
self._enabled = self._params.get_bool("RadarDistance")
|
||||
self._jump_guard = _JumpGuard()
|
||||
self._one = _LeadHold()
|
||||
self._two = _LeadHold()
|
||||
self._stability = _LeadStability()
|
||||
self._smoother = _LeadSmoother()
|
||||
self._creep_frames = 0
|
||||
self._creep_released = False
|
||||
|
||||
def _read_params(self) -> None:
|
||||
enabled = self._params.get_bool("RadarDistance")
|
||||
if not enabled and self._enabled:
|
||||
self._jump_guard.reset()
|
||||
self._one.reset()
|
||||
self._two.reset()
|
||||
self._smoother.reset()
|
||||
self._creep_frames = 0
|
||||
self._creep_released = False
|
||||
self._enabled = enabled
|
||||
|
||||
def update(self, sm) -> None:
|
||||
if self._frame % int(1. / DT_MDL) == 0:
|
||||
self._read_params()
|
||||
self._v_ego = float(sm['carState'].vEgo)
|
||||
|
||||
def enabled(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
def lead_unstable(self) -> bool:
|
||||
return self._stability.unstable
|
||||
|
||||
def _stop_gap_bias(self, lead):
|
||||
# Report a (near-)stopped lead up to STOP_GAP_M closer at low speed so the MPC's own smooth stop ends
|
||||
# that much farther back. Monotone (only ever reports closer). No-op outside the regime.
|
||||
in_regime = (lead.status and lead.vLead <= STOP_GAP_VLEAD and
|
||||
self._v_ego <= STOP_GAP_VEGO and lead.dRel > STOP_GAP_MIN_DREL)
|
||||
if not in_regime:
|
||||
self._creep_frames = 0
|
||||
self._creep_released = False
|
||||
return lead
|
||||
|
||||
# A held (stale) lead's near-zero vLead can still satisfy the regime check on a lead that's departed --
|
||||
# skip biasing it, but freeze the creep latch rather than resetting it (an unrelated jump-guard glitch
|
||||
# shouldn't undo motion the lead already earned).
|
||||
if isinstance(lead, _HeldLead):
|
||||
return lead
|
||||
|
||||
if lead.vLead > STOP_GAP_CREEP_V:
|
||||
self._creep_frames = min(self._creep_frames + 1, STOP_GAP_CREEP_HOLD_FRAMES)
|
||||
if self._creep_frames >= STOP_GAP_CREEP_HOLD_FRAMES:
|
||||
self._creep_released = True
|
||||
else:
|
||||
# decay: a sub-threshold frame undoes one frame of "motion" credit, so only SUSTAINED motion (not
|
||||
# cumulative noise straddling the threshold) can reach the cap.
|
||||
self._creep_frames = max(self._creep_frames - 1, 0)
|
||||
if self._creep_released: # once latched: sticky -- a single-frame near-zero blip
|
||||
return lead # afterward (e.g. sensor noise mid-creep) can't re-suppress it
|
||||
|
||||
d_ramp = min(max((STOP_GAP_REGIME_DREL - lead.dRel) / STOP_GAP_RAMP_BAND, 0.0), 1.0)
|
||||
v_ramp = min(max((STOP_GAP_VLEAD - lead.vLead) / STOP_GAP_VLEAD, 0.0), 1.0)
|
||||
offset = STOP_GAP_M * d_ramp * v_ramp
|
||||
if offset < 0.05:
|
||||
return lead
|
||||
return _BiasedLead(lead, max(lead.dRel - offset, STOP_GAP_MIN_DREL))
|
||||
|
||||
def smooth_radarstate(self, radarstate):
|
||||
self._frame += 1 # step()'s elapsed-frames basis; see _LeadHold
|
||||
self._stability.update(radarstate.leadOne, self._v_ego) # telemetry, runs every cycle
|
||||
if not self._enabled:
|
||||
return radarstate # off: byte-stock passthrough
|
||||
two = radarstate.leadTwo
|
||||
noisy = self._stability.churn or self._stability.same_track_noise
|
||||
if self._v_ego >= LOW_SPEED_PASSTHROUGH_V:
|
||||
one = self._jump_guard.step(radarstate.leadOne, self._frame) # reject a same-cycle farther-jump transient ...
|
||||
one = self._one.step(one, self._frame) # ... + flicker-hold ...
|
||||
two = self._two.step(radarstate.leadTwo, self._frame)
|
||||
one = self._smoother.update(one, noisy, self._frame) # ... + same-object de-jitter (anti follow-hunt)
|
||||
elif self._v_ego >= CREEP_PASSTHROUGH_V:
|
||||
# creep band: de-jitter ONLY (symmetric EMA), no flicker-hold (a stale held lead would delay launch)
|
||||
one = self._smoother.update(radarstate.leadOne, noisy, self._frame)
|
||||
else:
|
||||
one = radarstate.leadOne # full standstill: no hold/smoothing
|
||||
one = self._stop_gap_bias(one) # low-speed near-stopped: settle farther back
|
||||
if one is radarstate.leadOne and two is radarstate.leadTwo:
|
||||
return radarstate # nothing changed -> byte-stock object
|
||||
return _RadarStateProxy(one, two)
|
||||
@@ -1,801 +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.
|
||||
|
||||
RadarDistance is a pure lead DE-NOISER: flicker-hold + churn smoother + instability telemetry, and nothing
|
||||
else (no dRel biasing). These tests pin: off / low-speed == byte-stock (stock stop distance); the hold is
|
||||
obstacle-monotone (brake >= stock) and bounded; the churn smoother de-jitters only a track-flipping lead;
|
||||
and the instability flag is telemetry that runs regardless of the gate.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.radar_distance.radar_distance import \
|
||||
RadarDistanceController, HOLD_MAX_FRAMES, FCW_PROB_CAP, LOW_SPEED_PASSTHROUGH_V, CREEP_PASSTHROUGH_V, \
|
||||
DROPOUT_DREL, STOP_GAP_MIN_DREL, STOP_GAP_VEGO, STOP_GAP_VLEAD, STOP_GAP_REGIME_DREL, SWITCH_DREL, \
|
||||
JUMP_GUARD_MAX_HOLD, STOP_GAP_CREEP_V, STOP_GAP_CREEP_HOLD_FRAMES
|
||||
|
||||
COMFORT_BRAKE = 2.5
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self, store=None):
|
||||
self.store = dict(store or {})
|
||||
|
||||
def get_bool(self, key):
|
||||
return bool(self.store.get(key, False))
|
||||
|
||||
|
||||
def lead(status=True, dRel=40.0, vRel=-2.0, vLead=18.0, aLeadK=0.0, aLeadTau=1.5, modelProb=0.95, radarTrackId=-1):
|
||||
return SimpleNamespace(status=status, dRel=dRel, yRel=0.0, vRel=vRel, vLead=vLead, vLeadK=vLead,
|
||||
aLeadK=aLeadK, aLeadTau=aLeadTau, modelProb=modelProb, radarTrackId=radarTrackId)
|
||||
|
||||
|
||||
def rs(one, two=None):
|
||||
return SimpleNamespace(leadOne=one, leadTwo=two or lead(status=False, dRel=0.0, modelProb=0.0))
|
||||
|
||||
|
||||
def obstacle(ld):
|
||||
return ld.dRel + ld.vLead ** 2 / (2 * COMFORT_BRAKE)
|
||||
|
||||
|
||||
def ctrl(enabled=True, v_ego=10.0):
|
||||
c = RadarDistanceController(CP=SimpleNamespace(), params=FakeParams({'RadarDistance': enabled}))
|
||||
c._v_ego = v_ego # above the low-speed gate so the hold + smoother run
|
||||
return c
|
||||
|
||||
|
||||
def churn_frames(n, d_a=40.0, d_b=42.0, vLead=18.0):
|
||||
# a steady lead whose radarTrackId flips every frame (dRel jitters with it) -> the churn detector fires and
|
||||
# the smoother should de-jitter dRel. vLead is steady so it is NOT flagged bimodal (never averages 2 tracks).
|
||||
for i in range(n):
|
||||
even = i % 2 == 0
|
||||
yield lead(dRel=d_a if even else d_b, vLead=vLead, vRel=-1.0, radarTrackId=1 if even else 2)
|
||||
|
||||
|
||||
# --- off / low-speed == byte-stock ------------------------------------------------------------------------
|
||||
|
||||
def test_disabled_is_identity():
|
||||
c = ctrl(enabled=False)
|
||||
r = rs(lead())
|
||||
assert c.smooth_radarstate(r) is r # byte-stock passthrough
|
||||
|
||||
|
||||
def test_valid_lead_passthrough():
|
||||
c = ctrl()
|
||||
one = lead(dRel=40.0)
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne is one # clean lead, no churn -> unchanged
|
||||
|
||||
|
||||
def test_full_standstill_outside_stopgap_is_passthrough():
|
||||
# Full standstill (< CREEP_PASSTHROUGH_V), lead OUTSIDE the stop-gap regime (far): no hold, no smoothing,
|
||||
# no bias -> the EXACT raw radarstate object (byte-identical). The stop-gap only engages inside its regime.
|
||||
c = ctrl(v_ego=CREEP_PASSTHROUGH_V - 0.5)
|
||||
r = rs(lead(dRel=STOP_GAP_REGIME_DREL + 8.0, vLead=0.5))
|
||||
assert c.smooth_radarstate(r) is r
|
||||
|
||||
|
||||
def test_creep_dejitters_churn_but_no_hold():
|
||||
# Creep band [CREEP, LOW_SPEED): the churn smoother runs (de-jitter -> smooth stop-and-go), but the
|
||||
# flicker-hold does NOT (a dropped/departed lead must not be held, or launch would be delayed).
|
||||
# vLead>STOP_GAP_VLEAD so the stop-gap stays out and this isolates the EMA.
|
||||
c = ctrl(v_ego=(CREEP_PASSTHROUGH_V + LOW_SPEED_PASSTHROUGH_V) / 2)
|
||||
out = None
|
||||
for f in churn_frames(30, d_a=6.0, d_b=8.0, vLead=3.0):
|
||||
out = c.smooth_radarstate(rs(f))
|
||||
assert 6.0 < out.leadOne.dRel < 8.0 # jitter smoothed
|
||||
# a dropout in the creep band is NOT held -> raw passes through (no stale lead)
|
||||
drop = rs(lead(status=False, dRel=0.0, modelProb=0.0))
|
||||
assert c.smooth_radarstate(drop) is drop
|
||||
|
||||
|
||||
def test_creep_clean_lead_passthrough():
|
||||
# creep band, steady moving lead (no churn, outside stop-gap regime) -> exact raw object (unbiased)
|
||||
c = ctrl(v_ego=(CREEP_PASSTHROUGH_V + LOW_SPEED_PASSTHROUGH_V) / 2)
|
||||
r = rs(lead(dRel=4.0, vLead=2.5, radarTrackId=3))
|
||||
assert c.smooth_radarstate(r) is r
|
||||
|
||||
|
||||
# --- stop-gap (settle farther back from a near-stopped lead) ----------------------------------------------
|
||||
|
||||
def test_stop_gap_pulls_stopped_lead_closer():
|
||||
c = ctrl(v_ego=2.0)
|
||||
one = lead(dRel=6.0, vLead=0.0, vRel=-1.0)
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne.dRel < 6.0 # reported closer -> MPC stops farther back
|
||||
assert obstacle(out.leadOne) <= obstacle(one) + 1e-6 # brake >= stock (obstacle never farther)
|
||||
|
||||
|
||||
def test_stop_gap_monotone_never_farther():
|
||||
c = ctrl(v_ego=3.0)
|
||||
for d in (4.0, 6.0, 9.0, 11.0):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vLead=0.0)))
|
||||
assert out.leadOne.dRel <= d + 1e-6
|
||||
|
||||
|
||||
def test_stop_gap_min_floor():
|
||||
c = ctrl(v_ego=2.0)
|
||||
out = c.smooth_radarstate(rs(lead(dRel=STOP_GAP_MIN_DREL + 0.5, vLead=0.0)))
|
||||
assert out.leadOne.dRel >= STOP_GAP_MIN_DREL - 1e-6
|
||||
|
||||
|
||||
def test_stop_gap_off_when_disabled():
|
||||
c = ctrl(enabled=False, v_ego=2.0)
|
||||
r = rs(lead(dRel=6.0, vLead=0.0))
|
||||
assert c.smooth_radarstate(r) is r # disabled -> stock stop distance
|
||||
|
||||
|
||||
def test_stop_gap_moving_lead_no_change():
|
||||
c = ctrl(v_ego=2.0)
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=STOP_GAP_VLEAD + 1.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(6.0) # lead moving -> not a stop
|
||||
|
||||
|
||||
def test_stop_gap_high_speed_no_change():
|
||||
c = ctrl(v_ego=STOP_GAP_VEGO + 2.0)
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=0.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(6.0) # above the stop regime -> unbiased
|
||||
|
||||
|
||||
def test_stop_gap_far_lead_no_change():
|
||||
c = ctrl(v_ego=2.0)
|
||||
d = STOP_GAP_REGIME_DREL + 5.0
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vLead=0.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(d) # beyond the ramp-in regime -> unbiased
|
||||
|
||||
|
||||
# --- stop-gap creep override (sustained lead motion releases the bias even below STOP_GAP_VLEAD) ----------
|
||||
|
||||
def test_stop_gap_creep_releases_after_sustained_motion():
|
||||
# route 550a71ee4c7a7fbe/000004a4--c9c4691959, t~1126-1138: a lead crept forward at 0.3-0.6 m/s (well under
|
||||
# STOP_GAP_VLEAD) for 9+ seconds; without the override the bias suppressed the whole real gap growth the
|
||||
# entire time, producing a 9+ second launch delay. Real dRel grows steadily 4.0 -> 4.5m over this window.
|
||||
c = ctrl(v_ego=0.0)
|
||||
d = 4.0
|
||||
out = None
|
||||
for i in range(STOP_GAP_CREEP_HOLD_FRAMES + 5):
|
||||
d += 0.05
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vLead=0.4)))
|
||||
if i < STOP_GAP_CREEP_HOLD_FRAMES - 1:
|
||||
assert out.leadOne.dRel < d - 1e-6 # still suppressed while creep hasn't sustained yet
|
||||
assert out.leadOne.dRel == pytest.approx(d) # released after STOP_GAP_CREEP_HOLD_FRAMES of motion
|
||||
|
||||
|
||||
def test_stop_gap_creep_release_is_sticky_through_a_momentary_zero_blip():
|
||||
# A single-frame return to exactly 0.0 (real sensor behavior mid-creep, not just noise) must not re-arm the
|
||||
# bias once sustained creep has already released it -- this was the actual bug: the real route's lead
|
||||
# dipped to vLead=0.00 for one frame mid-launch and the bias briefly re-suppressed the gap right as a result.
|
||||
c = ctrl(v_ego=0.0)
|
||||
d = 4.0
|
||||
for _ in range(STOP_GAP_CREEP_HOLD_FRAMES):
|
||||
d += 0.05
|
||||
c.smooth_radarstate(rs(lead(dRel=d, vLead=0.4)))
|
||||
d += 0.05
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vLead=0.0))) # exact-zero blip
|
||||
assert out.leadOne.dRel == pytest.approx(d) # still released, not re-suppressed
|
||||
d += 0.05
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vLead=0.3))) # creep resumes
|
||||
assert out.leadOne.dRel == pytest.approx(d)
|
||||
|
||||
|
||||
def test_stop_gap_creep_counter_resets_on_a_genuine_new_stop():
|
||||
# Leaving the bias regime entirely (lead departs, or ego speeds past STOP_GAP_VEGO) must re-arm the
|
||||
# override so a LATER, unrelated near-stop encounter isn't permanently exempted by a stale latch.
|
||||
c = ctrl(v_ego=0.0)
|
||||
d = 4.0
|
||||
for _ in range(STOP_GAP_CREEP_HOLD_FRAMES):
|
||||
d += 0.05
|
||||
c.smooth_radarstate(rs(lead(dRel=d, vLead=0.4)))
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))) # lead lost -> regime exit
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=0.0))) # fresh near-stop encounter
|
||||
assert out.leadOne.dRel < 6.0 # bias re-armed, active again
|
||||
|
||||
|
||||
def test_stop_gap_creep_below_threshold_never_releases():
|
||||
c = ctrl(v_ego=0.0)
|
||||
out = None
|
||||
for _ in range(STOP_GAP_CREEP_HOLD_FRAMES + 10):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=STOP_GAP_CREEP_V * 0.5)))
|
||||
assert out.leadOne.dRel < 6.0 - 1e-6 # vLead never exceeds the creep threshold -> stays biased
|
||||
|
||||
|
||||
def test_stop_gap_creep_intermittent_noise_never_releases():
|
||||
# route 550a71ee4c7a7fbe/000004b6--d4a8ac3352, t~678-690s: a genuinely-stopped lead's vLead noise blipped
|
||||
# above STOP_GAP_CREEP_V on roughly half the frames (never below zero motion overall, never sustained) --
|
||||
# the old monotonic-only counter still accumulated those blips to the cap over enough frames and falsely
|
||||
# latched the bias off mid-stop, producing a same-cycle jump in the reported gap. Alternating strictly
|
||||
# above/below the threshold every frame, for far longer than STOP_GAP_CREEP_HOLD_FRAMES, must never release.
|
||||
c = ctrl(v_ego=0.0)
|
||||
out = None
|
||||
for i in range(STOP_GAP_CREEP_HOLD_FRAMES * 4):
|
||||
vLead = STOP_GAP_CREEP_V + 0.05 if i % 2 == 0 else 0.0
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=vLead)))
|
||||
assert out.leadOne.dRel < 6.0 - 1e-6 # never sustained -> never releases, even after 4x the hold
|
||||
|
||||
|
||||
def test_stop_gap_creep_sustained_after_intermittent_noise_still_releases():
|
||||
# the decay must not make the override permanently harder to reach -- real sustained motion right after a
|
||||
# noisy patch still releases within the normal HOLD window (decay only undoes noise, doesn't add a penalty).
|
||||
c = ctrl(v_ego=0.0)
|
||||
for i in range(STOP_GAP_CREEP_HOLD_FRAMES):
|
||||
vLead = STOP_GAP_CREEP_V + 0.05 if i % 2 == 0 else 0.0
|
||||
c.smooth_radarstate(rs(lead(dRel=6.0, vLead=vLead)))
|
||||
out = None
|
||||
for _ in range(STOP_GAP_CREEP_HOLD_FRAMES + 5):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=6.0, vLead=STOP_GAP_CREEP_V + 0.05)))
|
||||
assert out.leadOne.dRel == pytest.approx(6.0) # sustained motion still releases in bounded time
|
||||
|
||||
|
||||
def test_stop_gap_does_not_double_bias_a_jump_guard_held_lead():
|
||||
# route 550a71ee4c7a7fbe/000004bc--d9e0efd5ac, t~1563.4-1563.9: a lead sitting near-stopped (vLead~0,
|
||||
# inside the stop-gap regime) departs fast enough that a single raw dRel jump exceeds jump-guard's
|
||||
# SWITCH_DREL. Jump-guard holds its OLD, stale (near-zero vLead0) reading rather than the new, fast, real
|
||||
# one -- and that held vLead0 STILL satisfies the stop-gap regime check (it looks near-stopped), so without
|
||||
# the skip, stop-gap piles a SECOND closer-bias on top of an already-stale value. On the real route this
|
||||
# compounding dropped the reported gap far enough (raw ~16m -> ~2-5m) to fool the MPC's own forward-solve
|
||||
# into a spurious FCW during a real launch. Fixed: stop-gap must skip a held (stale) lead entirely.
|
||||
v_ego = (LOW_SPEED_PASSTHROUGH_V + STOP_GAP_VEGO) / 2 # in-band for jump-guard hold AND stop-gap regime
|
||||
c = ctrl(v_ego=v_ego)
|
||||
dRel0, vRel0, vLead0 = 7.5, -0.1, 0.2 # near-stopped baseline, trusted
|
||||
c.smooth_radarstate(rs(lead(dRel=dRel0, vRel=vRel0, vLead=vLead0)))
|
||||
jumped = c.smooth_radarstate(rs(lead(dRel=dRel0 + SWITCH_DREL + 1.0, vRel=6.5, vLead=6.8))) # real, fast departure
|
||||
expected_held = dRel0 - max(-vRel0, 0.0) * DT_MDL # jump-guard's own extrapolation -- no further bias
|
||||
assert jumped.leadOne.dRel == pytest.approx(expected_held, abs=1e-6)
|
||||
|
||||
|
||||
def test_smoother_does_not_launder_a_jump_guard_hold_during_churn():
|
||||
# A churn episode (real radarTrackId flapping, steady kinematics) actively engaging the smoother right as a
|
||||
# jump-guard hold begins must not let the smoother wrap the held lead into a _SmoothedLead -- that would hide
|
||||
# "this is stale" from stop-gap's held-lead check and let a second closer-bias stack on top.
|
||||
v_ego = (LOW_SPEED_PASSTHROUGH_V + STOP_GAP_VEGO) / 2 # in-band for jump-guard hold AND stop-gap regime
|
||||
c = ctrl(v_ego=v_ego)
|
||||
d = 9.0
|
||||
for i in range(8):
|
||||
tid = 1 if i % 2 == 0 else 2
|
||||
c.smooth_radarstate(rs(lead(dRel=d, vRel=-6.3, vLead=0.2, radarTrackId=tid)))
|
||||
dRel0 = d
|
||||
d -= 0.15
|
||||
assert c.lead_unstable() # churn primed the smoother
|
||||
vRel0 = -6.3
|
||||
jumped = c.smooth_radarstate(rs(lead(dRel=dRel0 + SWITCH_DREL + 1.0, vRel=6.5, vLead=6.8, radarTrackId=1)))
|
||||
expected_held = dRel0 - max(-vRel0, 0.0) * DT_MDL
|
||||
assert type(jumped.leadOne).__name__ == '_HeldLead'
|
||||
assert jumped.leadOne.dRel == pytest.approx(expected_held, abs=1e-6)
|
||||
|
||||
|
||||
def test_stop_gap_creep_latch_survives_an_unrelated_jump_guard_hold():
|
||||
# A creep-release already earned (sustained real motion) must not be wiped by an unrelated jump-guard hold
|
||||
# that happens to land on the same lead -- the hold is a one-cycle fusion transient, not evidence the lead
|
||||
# stopped moving again.
|
||||
v_ego = (LOW_SPEED_PASSTHROUGH_V + STOP_GAP_VEGO) / 2 # in-band for jump-guard hold AND stop-gap regime
|
||||
c = ctrl(v_ego=v_ego)
|
||||
d = 4.0
|
||||
for _ in range(STOP_GAP_CREEP_HOLD_FRAMES + 2):
|
||||
d += 0.05
|
||||
c.smooth_radarstate(rs(lead(dRel=d, vRel=-6.0, vLead=0.4)))
|
||||
assert c._creep_released
|
||||
dRel0 = d
|
||||
held = c.smooth_radarstate(rs(lead(dRel=dRel0 + SWITCH_DREL + 1.0, vRel=6.5, vLead=6.8)))
|
||||
assert type(held.leadOne).__name__ == '_HeldLead' # bias correctly skipped on the held cycle
|
||||
assert c._creep_released # but the earned latch must survive the glitch
|
||||
d += 0.05
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vRel=-0.4, vLead=0.4)))
|
||||
assert out.leadOne.dRel == pytest.approx(d) # creep resumes unbiased, no re-suppression
|
||||
|
||||
|
||||
def test_low_speed_override_lead_passthrough():
|
||||
# radard low_speed_override emits a real closest-track lead with modelProb=0.0. It must be honored, not
|
||||
# rejected in favor of a stale farther held lead (which would under-brake / stop too close).
|
||||
c = ctrl()
|
||||
one = lead(status=True, dRel=2.5, vRel=0.0, vLead=0.0, modelProb=0.0)
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne is one
|
||||
|
||||
|
||||
# --- jump-guard (reject a same-cycle farther fusion transient) --------------------------------------------
|
||||
|
||||
def test_jump_guard_holds_farther_transient():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=27.92, vRel=-5.60, vLead=24.47, radarTrackId=1058)))
|
||||
out = c.smooth_radarstate(rs(lead(dRel=38.88, vRel=-3.19, vLead=26.91, radarTrackId=-1)))
|
||||
assert out.leadOne.dRel < 30.0 # farther jump rejected, held near the trusted value
|
||||
assert out.leadOne.status is True
|
||||
|
||||
|
||||
def test_jump_guard_passes_closer_jump_immediately():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-2.0, vLead=18.0)))
|
||||
out = c.smooth_radarstate(rs(lead(dRel=27.0, vRel=-5.0, vLead=15.0))) # big CLOSER jump
|
||||
assert out.leadOne.dRel == pytest.approx(27.0) # closer always passes through -- never delays a brake
|
||||
|
||||
|
||||
def test_jump_guard_replays_real_route_whiplash():
|
||||
# route 550a71ee4c7a7fbe/00000498--0704864d6a, t~402.2-402.8: a merging lead's vision distance estimate
|
||||
# whiplashed 27.92 -> 38.88 -> 37.69 -> 37.20 -> 26.84 for ~0.3s while a solid radar track sat at ~27m the
|
||||
# whole time. The guard should smooth the farther excursion into a monotone converge toward the real value.
|
||||
c = ctrl()
|
||||
raw = [
|
||||
(74.18, -4.05, 25.77, -1), (53.21, -3.55, 26.33, -1), (47.42, -3.23, 26.67, -1),
|
||||
(42.64, -3.50, 26.42, -1), (43.22, -3.49, 26.49, -1), (40.03, -3.04, 26.96, -1),
|
||||
(39.50, -3.29, 26.74, -1), (27.92, -5.60, 24.47, 1058), (38.88, -3.19, 26.91, -1),
|
||||
(37.69, -3.09, 27.04, -1), (37.20, -2.77, 27.39, -1), (26.84, -5.80, 24.37, 1058),
|
||||
]
|
||||
out = None
|
||||
for dRel, vRel, vLead, tid in raw:
|
||||
out = c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=vLead, radarTrackId=tid)))
|
||||
assert out.leadOne.dRel == pytest.approx(26.84) # real value recovered exactly once raw resumes reporting it
|
||||
# peak reported dRel during the excursion never revisits the raw 38.88 spike
|
||||
seen = []
|
||||
c = ctrl()
|
||||
for dRel, vRel, vLead, tid in raw:
|
||||
seen.append(c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=vLead, radarTrackId=tid))).leadOne.dRel)
|
||||
assert max(seen[8:11]) < 30.0 # the 3 farther-jump frames are all held near ~27m, not ~37-39m
|
||||
|
||||
|
||||
def test_jump_guard_self_heals_after_cap():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=20.0, vRel=-1.0, vLead=19.0)))
|
||||
for _ in range(JUMP_GUARD_MAX_HOLD):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0)))
|
||||
assert out.leadOne.dRel < 40.0 # held while under the cap
|
||||
# cap just reached on a lead that was closing -- one bounded grace cycle before accepting a farther raw value
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0)))
|
||||
assert out.leadOne.dRel < 40.0 # grace cycle: still held, not yet accepted
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(40.0) # grace spent -> accepts the real (departing) value
|
||||
|
||||
|
||||
def test_jump_guard_self_heals_immediately_when_not_closing():
|
||||
# The grace cycle only protects a lead that was closing when the cap was hit -- a lead that was already
|
||||
# steady/opening (vRel >= 0) self-heals on the very first cap-exceeding frame, same as before this fix.
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=20.0, vRel=0.5, vLead=19.0)))
|
||||
for _ in range(JUMP_GUARD_MAX_HOLD):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=0.5, vLead=19.0)))
|
||||
assert out.leadOne.dRel < 40.0
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=0.5, vLead=19.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(40.0) # no grace needed -> heals immediately, unchanged behavior
|
||||
|
||||
|
||||
def test_jump_guard_grace_is_used_at_most_once_per_hold_episode():
|
||||
# The grace cycle must be bounded -- a lead that keeps reading farther after the grace is spent must not
|
||||
# get a second grace before genuinely accepting the new value (else a departed lead could be held forever).
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=20.0, vRel=-1.0, vLead=19.0)))
|
||||
for _ in range(JUMP_GUARD_MAX_HOLD):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0)))
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0))) # grace cycle, spent
|
||||
out = c.smooth_radarstate(rs(lead(dRel=70.0, vRel=-1.0, vLead=19.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(70.0) # grace already spent this episode -> accepts immediately
|
||||
|
||||
|
||||
def test_jump_guard_replays_real_route_dropout_catchup():
|
||||
# route 550a71ee4c7a7fbe/000004c6--ed1b6d7f95, t~1337.9-1338.5: a spurious closer misread (31.08 -> 24.94)
|
||||
# passes through immediately (closer always does), poisoning the guard's anchor. The lead's real, continuing
|
||||
# trajectory (~31m, closing) then reads as a farther jump against that bad anchor and gets held for the full
|
||||
# cap. Without the grace cycle, the guard self-healed straight onto a farther transitional misread (56.52)
|
||||
# right as a real dropout began, and _LeadHold then flicker-held THAT value through the whole dropout --
|
||||
# reporting a lead ~2x farther and opening instead of closing, easing the MPC off right before a real
|
||||
# catch-up brake. The grace cycle must keep the held value close to the real trajectory across this handoff.
|
||||
c = ctrl(v_ego=14.4)
|
||||
raw = [
|
||||
(30.62, -0.45, 1), (38.36, -3.33, -1), (38.20, -3.42, -1), (38.08, -3.45, -1), (37.88, -3.53, -1),
|
||||
(37.72, -3.58, -1), (47.53, 0.35, -1), (24.94, -1.90, -1), (31.44, -3.72, -1), (31.20, -3.97, -1),
|
||||
(31.08, -3.95, 2), (74.32, 3.65, 3), (74.52, 3.70, 3), (74.92, 3.85, 3), (75.12, 3.88, 3),
|
||||
(75.28, 3.90, 3), (75.64, 3.95, 3), (75.64, 3.95, 3), (56.52, -2.09, -1),
|
||||
]
|
||||
out = None
|
||||
for dRel, vRel, tid in raw:
|
||||
out = c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=10.5, radarTrackId=tid)))
|
||||
assert out.leadOne.dRel < 30.0 # grace cycle: still held near the real trajectory
|
||||
dropout_held = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||
assert dropout_held.status is True
|
||||
assert dropout_held.dRel < 30.0 # flicker-hold seeds from the grace-held value, not 56.52
|
||||
|
||||
|
||||
def test_jump_guard_resets_on_dropout():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=20.0, vRel=-1.0, vLead=19.0)))
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vRel=-1.0, vLead=19.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(40.0) # a real dropout in between is not a same-cycle jump
|
||||
|
||||
|
||||
def test_jump_guard_off_when_disabled():
|
||||
c = ctrl(enabled=False)
|
||||
c.smooth_radarstate(rs(lead(dRel=27.92, vRel=-5.60, vLead=24.47)))
|
||||
r = rs(lead(dRel=38.88, vRel=-3.19, vLead=26.91))
|
||||
assert c.smooth_radarstate(r) is r # disabled -> raw passthrough, no guard
|
||||
|
||||
|
||||
def test_jump_guard_replays_real_route_sub_threshold_bounce():
|
||||
# route 550a71ee4c7a7fbe/000004b4--2bd66184db, t~976.08-976.48: dRel bounced 17.70 -> 12.32 -> ... -> 17.15
|
||||
# -> 12.04m across ~0.4s while vRel stayed -0.8 to -2.4 m/s -- physically impossible for one real object
|
||||
# at that closing speed (5m in ~0.1s would need ~50 m/s, not ~1-2). This is the case that motivated
|
||||
# lowering SWITCH_DREL from 8.0 to 4.0: the farther excursion (12.24 -> 17.15, a 4.91m jump) sailed through
|
||||
# unguarded at the old threshold, producing a false-relief-then-correction whipsaw. A closer jump (e.g.
|
||||
# 17.70 -> 12.32) always passes immediately regardless of threshold -- that invariant is untouched here.
|
||||
c = ctrl()
|
||||
raw = [
|
||||
(19.12, -2.32, 9.22, -0.67, -1), (17.95, -2.06, 9.39, -0.58, -1), (18.06, -1.90, 9.49, -0.60, -1),
|
||||
(17.70, -1.84, 9.44, -0.52, -1), (12.32, -1.20, 10.01, -0.02, 2449), (12.12, -1.40, 9.75, -1.60, 2427),
|
||||
(12.56, -1.20, 9.87, -1.45, 2427), (12.24, -1.05, 9.92, -1.29, 2427), (17.15, -2.39, 8.53, -0.85, -1),
|
||||
(12.04, -0.82, 10.02, -0.97, 2427), (12.04, -0.82, 9.94, -0.85, 2427), (11.80, -0.85, 9.81, -0.78, 2427),
|
||||
]
|
||||
out = None
|
||||
seen = []
|
||||
for dRel, vRel, vLead, aLeadK, tid in raw:
|
||||
out = c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=vLead, aLeadK=aLeadK, radarTrackId=tid)))
|
||||
seen.append(out.leadOne.dRel)
|
||||
assert seen[4] == pytest.approx(12.32) # the initial closer jump (17.70->12.32) passes immediately
|
||||
assert seen[8] < 14.0 # the 12.24->17.15 farther excursion is held, not passed
|
||||
assert out.leadOne.dRel == pytest.approx(11.80) # recovers exactly once raw resumes reporting close values
|
||||
|
||||
|
||||
def test_jump_guard_hold_caps_model_prob_for_fcw():
|
||||
# route 550a71ee4c7a7fbe/000004bc--d9e0efd5ac, t~1563.5: a real, high-confidence (modelProb 0.999) lead
|
||||
# departs and gets held near-stationary by the guard mid-launch. The stock crash_cnt FCW gate fires on
|
||||
# radarState.leadOne.modelProb > 0.9 -- a held (stale, no longer confirmed-fresh) reading must not carry
|
||||
# enough confidence on its own to satisfy that gate, matching _LeadHold's existing flicker-hold cap.
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=15.72, vRel=1.80, vLead=6.79, modelProb=0.999)))
|
||||
held = c.smooth_radarstate(rs(lead(dRel=15.72 + SWITCH_DREL + 1.0, vRel=6.5, vLead=6.8, modelProb=0.999))).leadOne
|
||||
assert held.modelProb <= FCW_PROB_CAP
|
||||
|
||||
|
||||
def test_jump_guard_boundary_not_triggered():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-2.0, vLead=18.0)))
|
||||
out = c.smooth_radarstate(rs(lead(dRel=30.0 + SWITCH_DREL - 0.1, vRel=-2.0, vLead=18.0)))
|
||||
assert out.leadOne.dRel == pytest.approx(30.0 + SWITCH_DREL - 0.1) # under threshold -> passes through
|
||||
|
||||
|
||||
def test_jump_guard_does_not_hold_a_stale_reference_after_an_extended_low_speed_gap():
|
||||
# Mirrors _LeadHold's identical bug (see test_hold_does_not_resurrect_a_stale_lead_after_an_extended_low_speed_gap):
|
||||
# _jump_guard.step() is also only called above LOW_SPEED_PASSTHROUGH_V, so its _last reference used to
|
||||
# freeze for the entire duration of any low-speed period with no elapsed-time awareness. Route
|
||||
# 550a71ee4c7a7fbe/000004dc--c8c0867520, t~407.1: a lead tracked at dRel=11.72 while decelerating into a
|
||||
# stop froze there through a ~160s standstill. On relaunch the real lead (now dRel=23.16, opening) was
|
||||
# diffed against that 160s-stale reference as if it were a same-cycle transient, held as a fabricated
|
||||
# closing lead, and fed the MPC a phantom near-collision course that produced a real, unwarranted hard
|
||||
# brake on a real drive.
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
c.smooth_radarstate(rs(lead(dRel=11.72, vRel=-2.33, vLead=2.72))) # last reading before decelerating to a stop
|
||||
c._v_ego = CREEP_PASSTHROUGH_V - 0.5 # full stop: jump-guard.step() never called
|
||||
for _ in range(HOLD_MAX_FRAMES * 10): # far longer than any hold cap -- a real gap
|
||||
c.smooth_radarstate(rs(lead(dRel=3.68, vRel=0.0, vLead=0.0))) # the lead is stopped just ahead
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0 # relaunch: real lead now far + opening
|
||||
out = c.smooth_radarstate(rs(lead(dRel=23.16, vRel=4.72, vLead=9.77))).leadOne
|
||||
assert out.dRel == pytest.approx(23.16) # fresh reading passes through, not held as a phantom jump
|
||||
assert out.vRel == pytest.approx(4.72)
|
||||
|
||||
|
||||
def test_jump_guard_survives_a_single_incidental_gap_without_losing_protection():
|
||||
# Regression: a flat ">1 frame gap -> stale" threshold discarded the trusted reference (and so skipped the
|
||||
# SWITCH_DREL check entirely) after ANY single skipped call -- not just a real multi-second stop. That
|
||||
# happens on every cycle of ordinary v_ego dithering right at LOW_SPEED_PASSTHROUGH_V (a car crawling near
|
||||
# 5 m/s in stop-and-go traffic), permanently voiding the same-cycle fusion-transient guard for as long as the
|
||||
# dithering continues and letting a real glitch straight through. A single one-frame dip (a momentary v_ego
|
||||
# dip below the gate, then immediately back above it) must NOT be treated as stale -- the original farther-
|
||||
# jump rejection must still fire right after it, same as it would with no gap at all.
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-2.0, vLead=18.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V - 1.0 # one incidental dip below the gate
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0 # immediately back above it
|
||||
glitch = rs(lead(dRel=30.0 + SWITCH_DREL + 1.0, vRel=6.0, vLead=20.0)) # same-cycle fusion transient
|
||||
out = c.smooth_radarstate(glitch).leadOne
|
||||
assert out.dRel < 32.0 # still held near ~30, glitch did not leak through
|
||||
|
||||
|
||||
# --- flicker-hold -----------------------------------------------------------------------------------------
|
||||
|
||||
def test_holds_after_sustained_dropout():
|
||||
c = ctrl()
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-4.0, vLead=16.0)))
|
||||
held = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||
assert held.status is True
|
||||
assert held.dRel < 30.0 # dead-reckoned closer
|
||||
assert held.dRel == pytest.approx(30.0 - 4.0 * 0.05, abs=1e-6)
|
||||
|
||||
|
||||
def test_no_hold_without_sustained_lead():
|
||||
c = ctrl()
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0))) # single frame < SUSTAIN_FRAMES
|
||||
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
assert out.leadOne.status is False # no hold armed
|
||||
|
||||
|
||||
def test_hold_does_not_resurrect_a_stale_lead_after_an_extended_low_speed_gap():
|
||||
# Below LOW_SPEED_PASSTHROUGH_V the hold is never stepped at all (see smooth_radarstate), so an elapsed-
|
||||
# frames check must be based on real cycles, not "cycles since step() was last called" -- otherwise resuming
|
||||
# above the gate looks like no time passed no matter how long the low-speed period actually was, and a hold
|
||||
# armed on a real lead long before the gap can resurrect as if it were still fresh.
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-3.0, vLead=5.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V - 1.0 # below the gate: step() stops being called on the hold
|
||||
for _ in range(HOLD_MAX_FRAMES * 3):
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0 # back above the gate, lead still gone
|
||||
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
assert out.leadOne.status is False # must not resurrect the old hold
|
||||
|
||||
|
||||
def test_hold_survives_a_brief_low_speed_dip_within_the_cap():
|
||||
# A short dip below the gate (well under HOLD_MAX_FRAMES real cycles) is the case flicker-hold exists for --
|
||||
# it must still bridge, same as a same-speed dropout of the same real duration would. dRel must stay close
|
||||
# to the real last-known value (a genuine dead-reckoned extrapolation) -- NOT collapse toward MIN_HELD_DREL,
|
||||
# which is what a broken reseed would produce (see test_hold_reseeds_correctly_after_any_low_speed_gap).
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-3.0, vLead=5.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V - 1.0
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0
|
||||
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
assert out.leadOne.status is True
|
||||
assert out.leadOne.dRel < 30.0
|
||||
assert out.leadOne.dRel > 29.0 # dead-reckoned from 30.0, not collapsed to MIN_HELD_DREL
|
||||
|
||||
|
||||
def test_hold_reseeds_correctly_after_any_low_speed_gap():
|
||||
# Regression: _held_dRel used to be reseeded to the real last-known value only when since_real == 1. Once
|
||||
# since_real became elapsed-REAL-frames (not a self-incrementing call counter), any skipped low-speed frame
|
||||
# made since_real > 1 on the very first dropout call actually made, silently skipping the reseed -- leaving
|
||||
# _held_dRel at its stale/init value (0.0), which the very next line's floor clamps to MIN_HELD_DREL: a
|
||||
# fabricated near-bumper phantom lead fed straight to the MPC, not a dead-reckoned extrapolation of the real
|
||||
# one. A single one-frame low-speed dip (the shortest possible gap, ~0.05s) is enough to trigger it.
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=50.0, vRel=-3.0, vLead=15.0)))
|
||||
c._v_ego = CREEP_PASSTHROUGH_V - 0.5
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))) # exactly ONE low-speed frame
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0
|
||||
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||
assert out.status is True
|
||||
assert out.dRel > 45.0 # must be near the real ~50m, not a fabricated 0.5m
|
||||
|
||||
|
||||
def test_releases_after_hold_cap():
|
||||
c = ctrl()
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-2.0)))
|
||||
drop = rs(lead(status=False, dRel=0.0, modelProb=0.0))
|
||||
for _ in range(HOLD_MAX_FRAMES):
|
||||
assert c.smooth_radarstate(drop).leadOne.status is True
|
||||
assert c.smooth_radarstate(drop).leadOne.status is False # released after the cap
|
||||
|
||||
|
||||
def test_obstacle_monotone_during_hold():
|
||||
c = ctrl()
|
||||
for _ in range(3):
|
||||
real = lead(dRel=30.0, vRel=-3.0, vLead=15.0)
|
||||
c.smooth_radarstate(rs(real))
|
||||
base = obstacle(real)
|
||||
drop = rs(lead(status=False, dRel=0.0, modelProb=0.0))
|
||||
prev = base
|
||||
for _ in range(HOLD_MAX_FRAMES):
|
||||
held = c.smooth_radarstate(drop).leadOne
|
||||
assert obstacle(held) <= prev + 1e-6 # never reports a farther obstacle -> brake >= stock
|
||||
prev = obstacle(held)
|
||||
|
||||
|
||||
def test_fcw_prob_capped_and_aleadk_not_positive():
|
||||
c = ctrl()
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, aLeadK=1.5, modelProb=0.99)))
|
||||
held = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0))).leadOne
|
||||
assert held.modelProb <= FCW_PROB_CAP
|
||||
assert held.aLeadK <= 0.0
|
||||
|
||||
|
||||
def test_flicker_does_not_reset_wall_clock():
|
||||
c = ctrl()
|
||||
for _ in range(3):
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-2.0)))
|
||||
# alternating drop/reacquire must not refill the hold budget: after > HOLD_MAX_FRAMES wall time it releases
|
||||
for i in range(HOLD_MAX_FRAMES + 4):
|
||||
frame = rs(lead(status=False, dRel=0.0, modelProb=0.0)) if i % 2 else rs(lead(dRel=0.5)) # dRel<=DROPOUT: not real
|
||||
c.smooth_radarstate(frame)
|
||||
out = c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
assert out.leadOne.status is False
|
||||
assert DROPOUT_DREL == 1.0
|
||||
|
||||
|
||||
# --- churn smoother ---------------------------------------------------------------------------------------
|
||||
|
||||
def test_churn_smoother_removes_jitter():
|
||||
c = ctrl()
|
||||
out = None
|
||||
for f in churn_frames(30):
|
||||
out = c.smooth_radarstate(rs(f))
|
||||
assert c.lead_unstable() # churn detected
|
||||
assert 40.0 < out.leadOne.dRel < 42.0 # EMA settled between the two jittering tracks
|
||||
assert out.leadOne.dRel not in (40.0, 42.0) # not the raw alternating value
|
||||
|
||||
|
||||
def test_churn_smoother_off_when_disabled():
|
||||
c = ctrl(enabled=False)
|
||||
out = None
|
||||
for f in churn_frames(30):
|
||||
r = rs(f)
|
||||
out = c.smooth_radarstate(r)
|
||||
assert out is r # disabled -> raw passthrough, no smoothing
|
||||
|
||||
|
||||
def test_smoother_inactive_without_churn():
|
||||
c = ctrl()
|
||||
one = lead(dRel=40.0, radarTrackId=7)
|
||||
for _ in range(10):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, radarTrackId=7)))
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne is one # steady id -> no churn -> exact passthrough
|
||||
|
||||
|
||||
def test_churn_smoother_closer_accepted_immediately():
|
||||
# A steadily-closing lead that also briefly churns must never be held farther than the current raw value --
|
||||
# otherwise the EMA lags a real closing lead for the whole LEAD_SMOOTH_HOLD window, then snaps (a false
|
||||
# relief followed by a hard catch-up brake -- route 550a71ee4c7a7fbe/00000499, t~1387, real regression).
|
||||
c = ctrl()
|
||||
d = 82.0
|
||||
for i in range(40):
|
||||
tid = 1 if i % 3 else 2 # enough id-churn to keep the smoother engaged
|
||||
out = c.smooth_radarstate(rs(lead(dRel=d, vRel=-6.0, vLead=24.0, radarTrackId=tid)))
|
||||
assert out.leadOne.dRel <= d + 1e-6 # never farther than the latest raw reading
|
||||
d -= 0.4 # steadily closing
|
||||
|
||||
|
||||
def test_churn_smoother_replays_real_route_late_acquisition():
|
||||
# route 550a71ee4c7a7fbe/00000499--7f57e1d000, t~1386.9-1388.4: radard toggles between two real candidate
|
||||
# tracks (id 4611 ~110m, id 4609 ~82m closing) while acquiring, then a couple of vision-fallback frames
|
||||
# (id -1) report ~104-109m mid-acquisition. The real dRel (track 4609) closes smoothly 82.0 -> 73.2m the
|
||||
# whole time. Old symmetric EMA held the reported dRel near ~82m (farther than truth) for ~1s after the
|
||||
# brief churn window, then snapped -- this is the false-relief-then-correction pattern being fixed here.
|
||||
c = ctrl()
|
||||
raw = [
|
||||
(110.84, -1.75, 4611), (82.04, -3.78, 4609), (110.60, -1.85, 4611), (81.68, -3.80, 4609),
|
||||
(82.28, -4.13, 4609), (110.40, -2.05, 4611), (110.16, -1.93, 4611), (110.08, -2.00, 4611),
|
||||
(110.00, -2.00, 4611), (80.12, -4.83, 4609), (79.88, -4.95, 4609), (79.64, -5.08, 4609),
|
||||
(79.32, -5.20, 4609), (79.48, -5.38, 4609), (79.08, -5.55, 4609), (78.64, -5.70, 4609),
|
||||
(78.20, -5.88, 4609), (77.84, -6.00, 4609), (77.60, -6.18, 4609), (77.48, -6.30, 4609),
|
||||
(76.96, -6.50, 4609), (76.48, -6.65, 4609), (103.52, -1.75, -1), (76.08, -6.90, 4609),
|
||||
(75.52, -7.05, 4609), (108.97, -2.02, -1), (104.23, -2.15, -1), (103.64, -2.13, -1),
|
||||
(74.16, -7.43, 4609), (73.72, -7.60, 4609), (73.24, -7.70, 4609),
|
||||
]
|
||||
out = None
|
||||
for dRel, vRel, tid in raw:
|
||||
out = c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=24.0 + vRel, radarTrackId=tid)))
|
||||
assert out.leadOne.dRel == pytest.approx(73.24, abs=0.5) # tracks the true closing value, no lag
|
||||
# at no point does the reported dRel sit meaningfully farther than the most recent real (id>0) reading
|
||||
c = ctrl()
|
||||
worst_overshoot = 0.0
|
||||
last_real = None
|
||||
for dRel, vRel, tid in raw:
|
||||
out = c.smooth_radarstate(rs(lead(dRel=dRel, vRel=vRel, vLead=24.0 + vRel, radarTrackId=tid)))
|
||||
if tid > 0:
|
||||
last_real = dRel
|
||||
if last_real is not None:
|
||||
worst_overshoot = max(worst_overshoot, out.leadOne.dRel - last_real)
|
||||
assert worst_overshoot < 1.0 # old code overshot by ~6-9m for up to ~1s
|
||||
|
||||
|
||||
# --- instability telemetry --------------------------------------------------------------------------------
|
||||
|
||||
def test_stability_quiet_on_clean_lead():
|
||||
c = ctrl()
|
||||
for _ in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0, radarTrackId=5)))
|
||||
assert not c.lead_unstable()
|
||||
|
||||
|
||||
def test_stability_flags_bimodal_lead():
|
||||
c = ctrl()
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0, radarTrackId=5)))
|
||||
assert c.lead_unstable()
|
||||
|
||||
|
||||
def test_stability_flags_trackid_churn():
|
||||
c = ctrl()
|
||||
for f in churn_frames(20):
|
||||
c.smooth_radarstate(rs(f))
|
||||
assert c.lead_unstable()
|
||||
|
||||
|
||||
def test_stability_resets_on_dropout():
|
||||
c = ctrl()
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0)))
|
||||
assert c.lead_unstable()
|
||||
c.smooth_radarstate(rs(lead(status=False, dRel=0.0, modelProb=0.0)))
|
||||
assert not c.lead_unstable()
|
||||
|
||||
|
||||
def test_stability_runs_even_when_disabled():
|
||||
c = ctrl(enabled=False)
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0)))
|
||||
assert c.lead_unstable() # telemetry not gated by the RadarDistance param
|
||||
|
||||
|
||||
# --- same-track noise smoother (bimodal vLead / repeated dRel jump on a CONSTANT radarTrackId) -------------
|
||||
|
||||
def test_smoother_dejitters_bimodal_vlead_on_same_track():
|
||||
# Same physical object (radarTrackId constant) but a bouncing velocity read (Doppler/fusion noise) -- the
|
||||
# id evidence pins this to ONE real lead, so it's safe to EMA (unlike a bimodal read with a changing id).
|
||||
c = ctrl()
|
||||
out = None
|
||||
for i in range(30):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0, vRel=-1.0, radarTrackId=9)))
|
||||
assert c.lead_unstable()
|
||||
assert 10.0 < out.leadOne.vLead < 18.0 # EMA settled between the two bouncing readings
|
||||
assert out.leadOne.vLead not in (10.0, 18.0)
|
||||
|
||||
|
||||
def test_smoother_inactive_on_bimodal_vlead_with_changing_track():
|
||||
# Same bimodal vLead signature, but radarTrackId ALSO changes -- ambiguous (could be two really-different
|
||||
# real objects at different speeds), so this must NOT be smoothed, unlike the same-track case above.
|
||||
c = ctrl()
|
||||
one = lead(dRel=40.0, vLead=18.0, radarTrackId=1)
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0, radarTrackId=1 if i % 2 else 2)))
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne is one # exact passthrough -- not averaged across tracks
|
||||
|
||||
|
||||
def test_smoother_same_track_noise_ignores_drel_jump():
|
||||
# dRel track-jumps are excluded from same_track_noise on purpose: while status stays True, a repeated
|
||||
# farther jump this large is already absorbed by _JumpGuard upstream, so the smoother never even sees the
|
||||
# raw alternation here -- confirms the two mechanisms don't double up on the same signal.
|
||||
c = ctrl()
|
||||
out = None
|
||||
for i in range(30):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0 if i % 2 == 0 else 55.0, vLead=18.0, vRel=-1.0, radarTrackId=4)))
|
||||
assert out.leadOne.dRel < 45.0 # held near the trusted value by the jump-guard, not 55
|
||||
|
||||
|
||||
def test_smoother_does_not_lag_a_stale_ema_after_an_extended_low_speed_gap():
|
||||
# Mirrors the identical bug already fixed in _JumpGuard/_LeadHold: _smoother.update() is only called above
|
||||
# CREEP_PASSTHROUGH_V (see smooth_radarstate), so its EMA state (_d/_vl/_vr) and _hold freeze for the entire
|
||||
# duration of any full standstill. Resuming and EMA-ing a real, opening lead against that frozen state as if
|
||||
# no time had passed lags dRel toward the stale, closer pre-stop value -- confirmed on the same real route as
|
||||
# the _JumpGuard bug (550a71ee4c7a7fbe/000004dc--c8c0867520): pre-fix this reported 12.86m instead of the
|
||||
# real 23.16m on relaunch.
|
||||
c = ctrl(v_ego=LOW_SPEED_PASSTHROUGH_V + 1.0)
|
||||
for i in range(12):
|
||||
c.smooth_radarstate(rs(lead(dRel=11.72, vLead=2.72, vRel=-2.33, radarTrackId=1 if i % 2 else 2))) # arms churn
|
||||
c._v_ego = CREEP_PASSTHROUGH_V - 0.5 # full stop: smoother.update() never called
|
||||
for _ in range(60):
|
||||
c.smooth_radarstate(rs(lead(dRel=3.68, vRel=0.0, vLead=0.0)))
|
||||
c._v_ego = LOW_SPEED_PASSTHROUGH_V + 1.0 # relaunch: real lead now far + opening
|
||||
out = c.smooth_radarstate(rs(lead(dRel=23.16, vRel=4.72, vLead=9.77, radarTrackId=1))).leadOne
|
||||
assert out.dRel == pytest.approx(23.16) # fresh reading passes through, not EMA-lagged stale
|
||||
|
||||
|
||||
def test_single_incidental_gap_during_churn_does_not_leak_a_glitch_into_the_ema():
|
||||
# Regression, deeper than the single-mechanism cases above: a flat ">1 frame gap -> stale" threshold made
|
||||
# _jump_guard treat ANY single skipped call as fully stale and skip the SWITCH_DREL check -- so a same-cycle
|
||||
# fusion-transient glitch right after one incidental low-speed dip passed straight through _jump_guard
|
||||
# unguarded, then got folded into the churn smoother's EMA (which was still live from before the dip),
|
||||
# lagging vLead/dRel toward the glitch's inflated values for ~1s: a farther-and-faster-than-real lead, i.e.
|
||||
# a real violation of this file's own invariant ("NEVER report a farther-or-faster lead than reality").
|
||||
c = ctrl(v_ego=10.0)
|
||||
for i in range(6): # prime churn (real radar trackId-flip signature)
|
||||
tid = 1 if i % 2 == 0 else 2
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-1.0, vLead=15.0, radarTrackId=tid)))
|
||||
c._v_ego = 3.0 # one incidental dip into the creep band
|
||||
c.smooth_radarstate(rs(lead(dRel=30.0, vRel=-1.0, vLead=15.0, radarTrackId=1)))
|
||||
c._v_ego = 10.0 # immediately back above the full-pipeline gate
|
||||
glitch = lead(dRel=30.0 + SWITCH_DREL + 1.0, vRel=6.0, vLead=20.0, radarTrackId=1)
|
||||
out = c.smooth_radarstate(rs(glitch)).leadOne
|
||||
assert out.dRel < 31.0 # held near real ~30, not the glitch's 35.0
|
||||
assert out.vLead < 16.0 # held near real ~15, not the glitch's 20.0
|
||||
@@ -1,106 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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 numpy as np
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.params import Params
|
||||
|
||||
NEARSIDE_PROB = 0.25
|
||||
EDGE_PROB = 0.35
|
||||
EDGE_REACTION_TIME = 1.0
|
||||
EDGE_CLEAR_TIME = 0.3
|
||||
MIN_SPEED = 20 * CV.MPH_TO_MS
|
||||
NEAR_EDGE_DISTANCE = 4.5
|
||||
LEFT_NEARSIDE_LANE_IDX = 1
|
||||
RIGHT_NEARSIDE_LANE_IDX = 2
|
||||
|
||||
|
||||
class RoadEdgeLaneChangeController:
|
||||
def __init__(self, desire_helper):
|
||||
self.DH = desire_helper
|
||||
self.params = Params()
|
||||
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||
self.param_read_counter = 0
|
||||
self.left_edge_detected = False
|
||||
self.right_edge_detected = False
|
||||
self.left_edge_timer = 0.0
|
||||
self.right_edge_timer = 0.0
|
||||
self.left_clear_timer = 0.0
|
||||
self.right_clear_timer = 0.0
|
||||
|
||||
def read_params(self) -> None:
|
||||
self.enabled = self.params.get_bool("RoadEdgeLaneChangeEnabled")
|
||||
|
||||
def update_params(self) -> None:
|
||||
if self.param_read_counter % 50 == 0:
|
||||
self.read_params()
|
||||
self.param_read_counter += 1
|
||||
|
||||
def reset(self) -> None:
|
||||
self.left_edge_detected = False
|
||||
self.right_edge_detected = False
|
||||
self.left_edge_timer = 0.0
|
||||
self.right_edge_timer = 0.0
|
||||
self.left_clear_timer = 0.0
|
||||
self.right_clear_timer = 0.0
|
||||
|
||||
@staticmethod
|
||||
def _road_edge_y(road_edges, idx: int) -> float | None:
|
||||
if road_edges is None or len(road_edges) <= idx or len(road_edges[idx].y) == 0:
|
||||
return None
|
||||
return road_edges[idx].y[0]
|
||||
|
||||
@staticmethod
|
||||
def _edge_is_near(edge_y: float | None, left: bool) -> bool:
|
||||
if edge_y is None:
|
||||
return False
|
||||
if left:
|
||||
return bool(-NEAR_EDGE_DISTANCE < edge_y < 0.0)
|
||||
return bool(0.0 < edge_y < NEAR_EDGE_DISTANCE)
|
||||
|
||||
def update(self, road_edge_stds, lane_line_probs, v_ego: float, road_edges=None) -> None:
|
||||
self.update_params()
|
||||
|
||||
if not self.enabled or v_ego < MIN_SPEED:
|
||||
self.reset()
|
||||
return
|
||||
|
||||
left_edge_prob = np.clip(1.0 - road_edge_stds[0], 0.0, 1.0)
|
||||
right_edge_prob = np.clip(1.0 - road_edge_stds[1], 0.0, 1.0)
|
||||
left_lane_prob = lane_line_probs[LEFT_NEARSIDE_LANE_IDX]
|
||||
right_lane_prob = lane_line_probs[RIGHT_NEARSIDE_LANE_IDX]
|
||||
|
||||
left_edge_y = self._road_edge_y(road_edges, 0)
|
||||
right_edge_y = self._road_edge_y(road_edges, 1)
|
||||
left_edge_near = self._edge_is_near(left_edge_y, True)
|
||||
right_edge_near = self._edge_is_near(right_edge_y, False)
|
||||
|
||||
left_cond = left_edge_prob > EDGE_PROB and (left_edge_near or (left_edge_y is None and left_lane_prob < NEARSIDE_PROB))
|
||||
right_cond = right_edge_prob > EDGE_PROB and (right_edge_near or (right_edge_y is None and right_lane_prob < NEARSIDE_PROB))
|
||||
|
||||
if left_cond:
|
||||
self.left_edge_timer = min(self.left_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
|
||||
self.left_clear_timer = 0.0
|
||||
if self.left_edge_timer > EDGE_REACTION_TIME:
|
||||
self.left_edge_detected = True
|
||||
else:
|
||||
self.left_clear_timer += DT_MDL
|
||||
if self.left_clear_timer > EDGE_CLEAR_TIME:
|
||||
self.left_edge_timer = 0.0
|
||||
self.left_edge_detected = False
|
||||
|
||||
if right_cond:
|
||||
self.right_edge_timer = min(self.right_edge_timer + DT_MDL, EDGE_REACTION_TIME + EDGE_CLEAR_TIME)
|
||||
self.right_clear_timer = 0.0
|
||||
if self.right_edge_timer > EDGE_REACTION_TIME:
|
||||
self.right_edge_detected = True
|
||||
else:
|
||||
self.right_clear_timer += DT_MDL
|
||||
if self.right_clear_timer > EDGE_CLEAR_TIME:
|
||||
self.right_edge_timer = 0.0
|
||||
self.right_edge_detected = False
|
||||
@@ -92,7 +92,8 @@ class SpeedLimitAssist:
|
||||
self._minus_hold = 0.
|
||||
self._last_carstate_ts = 0.
|
||||
|
||||
# Solution functions mapped to respective states (dispatched from get_a_target_from_control)
|
||||
# TODO-SP: SLA's own output_a_target for planner
|
||||
# Solution functions mapped to respective states
|
||||
self.acceleration_solutions = {
|
||||
SpeedLimitAssistState.disabled: self.get_current_acceleration_as_target,
|
||||
SpeedLimitAssistState.inactive: self.get_current_acceleration_as_target,
|
||||
@@ -134,14 +135,9 @@ class SpeedLimitAssist:
|
||||
# Fallback
|
||||
return V_CRUISE_UNSET
|
||||
|
||||
# TODO-SP: SLA's own output_a_target for planner
|
||||
def get_a_target_from_control(self) -> float:
|
||||
# Dispatches to acceleration_solutions[self.state] -- a real per-state decel/accel estimate (kinematic
|
||||
# distance-to-limit formula while adapting, offset-over-horizon while active/holding) instead of echoing
|
||||
# a_ego. Clipped to LIMIT_MIN_ACC/LIMIT_MAX_ACC: this is a comfort bound on the VALUE HANDED TO THE MPC AS
|
||||
# ITS INITIAL STATE (x0), not a brake cap -- the MPC's own solve is free to command more decel than this
|
||||
# from that starting point, so clipping here can't reduce achievable braking.
|
||||
solution = self.acceleration_solutions.get(self.state, self.get_current_acceleration_as_target)
|
||||
return max(LIMIT_MIN_ACC, min(LIMIT_MAX_ACC, solution()))
|
||||
return self.a_ego
|
||||
|
||||
def update_params(self) -> None:
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
|
||||
@@ -16,14 +16,12 @@ from openpilot.common.constants import CV
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, LIMIT_MIN_ACC, LIMIT_MAX_ACC
|
||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
|
||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||
|
||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||
@@ -245,36 +243,7 @@ class TestSpeedLimitAssist:
|
||||
|
||||
self.sla.update(True, False, current_speed, 0, self.pcm_long_max_set_speed, target_speed, target_speed, True, distance, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.adapting
|
||||
assert self.sla.output_v_target == target_speed
|
||||
# kinematic (target^2 - current^2) / (2*distance) is well past LIMIT_MIN_ACC here -> clipped
|
||||
expected_raw = (target_speed ** 2 - current_speed ** 2) / (2. * distance)
|
||||
assert expected_raw < LIMIT_MIN_ACC
|
||||
assert self.sla.output_a_target == pytest.approx(LIMIT_MIN_ACC)
|
||||
|
||||
def test_active_state_uses_offset_over_horizon_formula(self):
|
||||
self.initialize_active_state(self.pcm_long_max_set_speed)
|
||||
target_speed = SPEED_LIMITS['highway']
|
||||
current_speed = target_speed - 1.0 # small offset, stays within the clip band
|
||||
|
||||
self.sla.update(True, False, current_speed, 0, self.pcm_long_max_set_speed, target_speed, target_speed, True, 0, self.events_sp)
|
||||
assert self.sla.state == SpeedLimitAssistState.active
|
||||
expected = (target_speed - current_speed) / float(ModelConstants.T_IDXS[CONTROL_N])
|
||||
assert LIMIT_MIN_ACC < expected < LIMIT_MAX_ACC # sanity: this case is meant to land inside the clip band
|
||||
assert self.sla.output_a_target == pytest.approx(expected)
|
||||
|
||||
def test_a_target_clipped_to_max_acc(self):
|
||||
self.sla.state = SpeedLimitAssistState.adapting
|
||||
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
|
||||
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
|
||||
|
||||
distance = 1.0 # tiny distance -> a large positive kinematic solution -> clipped
|
||||
current_speed = SPEED_LIMITS['city']
|
||||
target_speed = SPEED_LIMITS['highway']
|
||||
|
||||
self.sla.update(True, False, current_speed, 0, self.pcm_long_max_set_speed, target_speed, target_speed, True, distance, self.events_sp)
|
||||
expected_raw = (target_speed ** 2 - current_speed ** 2) / (2. * distance)
|
||||
assert expected_raw > LIMIT_MAX_ACC
|
||||
assert self.sla.output_a_target == pytest.approx(LIMIT_MAX_ACC)
|
||||
assert self.sla.output_v_target == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions
|
||||
|
||||
def test_long_disengaged_to_disabled(self):
|
||||
self.initialize_active_state(self.pcm_long_max_set_speed)
|
||||
|
||||
@@ -1,185 +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.
|
||||
|
||||
Closed-loop regression tests for sunnypilot's longitudinal comfort/smoothness fixes, built on the real
|
||||
LongitudinalPlanner via selfdrive/test/longitudinal_maneuvers/plant.py. Unlike a static log replay (which
|
||||
can only recompute an isolated input factor from recorded data), this drives the REAL MPC solver every
|
||||
cycle and feeds its output back into ego's own simulated speed/distance -- so it can catch regressions in
|
||||
the actual re-solved trajectory, not just in one factor's value. Params are set via the real Params() store;
|
||||
pytest's autouse openpilot_function_fixture (root conftest.py) gives each test function a fresh isolated
|
||||
prefix, so this is safe to run without touching real device state.
|
||||
|
||||
Each test targets a SPECIFIC bug found and fixed this session, and is verified to actually fail if that fix
|
||||
is reverted (see the commit history / memory notes referenced in each test's docstring) -- these aren't
|
||||
just plausible-looking assertions, they have demonstrated teeth.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
|
||||
from openpilot.selfdrive.test.longitudinal_maneuvers.plant import Plant
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import NORMAL
|
||||
|
||||
|
||||
def enable_sunnypilot_longitudinal(params: Params, accel_personality=True, radar_distance=True, dec=False):
|
||||
params.put_bool("AccelPersonalityEnabled", accel_personality)
|
||||
if accel_personality:
|
||||
params.put("AccelPersonality", int(NORMAL))
|
||||
params.put_bool("RadarDistance", radar_distance)
|
||||
params.put_bool("DynamicExperimentalControl", dec)
|
||||
|
||||
|
||||
def count_launch_brake_cycles(accel: np.ndarray, speed: np.ndarray, launch_th=0.3, settle_speed=0.5) -> int:
|
||||
# Count distinct "launch" events (acceleration crosses above launch_th while ego is near-stopped) followed
|
||||
# by a return to near-zero/negative acceleration -- the "creep brake creep brake" signature. Only counts
|
||||
# launches while speed stays low (settle_speed) -- a real, sustained departure isn't a cycle.
|
||||
cycles = 0
|
||||
launched = False
|
||||
for a, v in zip(accel, speed, strict=True):
|
||||
if not launched and a > launch_th and v < settle_speed:
|
||||
launched = True
|
||||
cycles += 1
|
||||
elif launched and a < 0.0:
|
||||
launched = False
|
||||
return cycles
|
||||
|
||||
|
||||
def test_creep_noise_never_causes_repeated_launch_brake_cycling():
|
||||
# Route 550a71ee4c7a7fbe/000004b6, t~678-690s: a genuinely-stopped lead's vLead sensor noise (small blips
|
||||
# above STOP_GAP_CREEP_V=0.03 m/s, never sustained) previously accumulated in radar_distance.py's
|
||||
# stop-gap creep-override counter (monotonic, never decayed) until it falsely latched the bias off,
|
||||
# producing a same-cycle gap-widening the MPC read as "room to launch" -- see
|
||||
# lead_unstable_gate_revert / routes_04b5_04b6_creep_bug memory notes; fixed in commit 73bea3866f
|
||||
# (counter now decays on sub-threshold frames). This reproduces the noise pattern in closed loop: ego
|
||||
# approaches and settles behind a near-stopped lead, then the lead's speed hovers with intermittent
|
||||
# noise (never sustained motion) for 40s. Must never repeatedly launch-then-brake.
|
||||
params = Params()
|
||||
enable_sunnypilot_longitudinal(params)
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
breakpoints = [0.0, 8.0]
|
||||
speed_lead_values = [3.0, 0.0]
|
||||
# 40s of intermittent noise: alternating 0.0 / small blip every 0.5s -- never two consecutive "moving" ticks,
|
||||
# so no real sustained motion, matching the real route's noise signature.
|
||||
noise_t = np.arange(8.0, 48.0, 0.5)
|
||||
noise_v = np.where(rng.random(len(noise_t)) > 0.5, 0.08, 0.0)
|
||||
breakpoints += list(noise_t)
|
||||
speed_lead_values += list(noise_v)
|
||||
|
||||
man = Maneuver(
|
||||
'stopped lead with intermittent vLead noise, no sustained motion',
|
||||
duration=48.0,
|
||||
initial_speed=8.0,
|
||||
lead_relevancy=True,
|
||||
initial_distance_lead=30.0,
|
||||
speed_lead_values=speed_lead_values,
|
||||
breakpoints=breakpoints,
|
||||
)
|
||||
valid, logs = man.evaluate()
|
||||
assert valid
|
||||
# logs columns: time, distance, distance_lead, speed, speed_lead, acceleration, d_rel
|
||||
t, speed, accel = logs[:, 0], logs[:, 3], logs[:, 5]
|
||||
settled = t > 10.0 # after the initial approach, while noise is active
|
||||
cycles = count_launch_brake_cycles(accel[settled], speed[settled])
|
||||
assert cycles <= 1, f'expected at most one settle-launch, got {cycles} launch-brake cycles from pure sensor noise'
|
||||
|
||||
|
||||
def test_drel_glitch_does_not_whipsaw_accel():
|
||||
# Route 550a71ee4c7a7fbe/000004b4, t~976.1s: raw dRel bounced 17.7->12.3->17.15->12.0m across ~0.3s while
|
||||
# vRel stayed -1 to -2 m/s -- physically impossible for one real object, a fusion glitch. SWITCH_DREL was
|
||||
# 8.0 (too coarse to catch the ~5m bounce); lowered to 4.0 in commit f978c923a4. This reproduces the exact
|
||||
# bounce pattern via Plant's lead_dRel_glitch_fn hook (overrides only dRel, leaving the true physics-based
|
||||
# vRel/speed evolution intact for the closed loop) and checks the MPC's actual commanded accel doesn't
|
||||
# whipsaw in response.
|
||||
params = Params()
|
||||
enable_sunnypilot_longitudinal(params)
|
||||
|
||||
glitch_window = (5.0, 5.5) # apply the bounce for a short window mid-maneuver
|
||||
|
||||
def glitch_fn(t, d_rel, v_rel):
|
||||
if glitch_window[0] <= t < glitch_window[1]:
|
||||
# alternate between the true (closer) reading and a ~5m-farther bounce, matching the real route
|
||||
phase = int((t - glitch_window[0]) / 0.05) % 2
|
||||
if phase == 1:
|
||||
return d_rel + 5.0, v_rel
|
||||
return d_rel, v_rel
|
||||
|
||||
plant = Plant(lead_relevancy=True, speed=8.0, distance_lead=20.0, e2e=False,
|
||||
lead_dRel_glitch_fn=glitch_fn)
|
||||
|
||||
accels = []
|
||||
while plant.current_time < 10.0:
|
||||
log = plant.step(v_lead=1.5)
|
||||
accels.append(log['acceleration'])
|
||||
accels = np.array(accels)
|
||||
|
||||
jerk = np.diff(accels) / (1.0 / plant.rate)
|
||||
# focus on the glitch window and its immediate aftermath
|
||||
t_arr = np.arange(len(accels)) / plant.rate
|
||||
during = (t_arr[1:] >= glitch_window[0]) & (t_arr[1:] < glitch_window[1] + 0.5)
|
||||
assert during.any()
|
||||
peak_jerk = np.max(np.abs(jerk[during]))
|
||||
assert peak_jerk < 3.0, f'dRel glitch produced a {peak_jerk:.2f} m/s^3 accel whipsaw -- glitch is leaking into the commanded accel'
|
||||
|
||||
|
||||
def test_fast_closing_lead_onset_is_ramped_not_snapped():
|
||||
# A severe closing-rate lead (matching route 000004b5's flagship regression episode, vRel to -16.5 m/s)
|
||||
# legitimately requires a large final decel -- that's not a bug (see routes_04b5_04b6_creep_bug memory:
|
||||
# "very firm brake... looks legitimate"). What IS a bug is an instantaneous snap rather than a ramped
|
||||
# onset. This checks onset smoothness (peak jerk during the transition) without asserting the final
|
||||
# magnitude must be small.
|
||||
params = Params()
|
||||
enable_sunnypilot_longitudinal(params)
|
||||
|
||||
man = Maneuver(
|
||||
'severe closing-rate lead, onset must ramp not snap',
|
||||
duration=12.0,
|
||||
initial_speed=20.0,
|
||||
lead_relevancy=True,
|
||||
initial_distance_lead=160.0,
|
||||
speed_lead_values=[3.5, 3.5],
|
||||
breakpoints=[0.0, 12.0],
|
||||
)
|
||||
valid, logs = man.evaluate()
|
||||
assert valid
|
||||
t, accel = logs[:, 0], logs[:, 5]
|
||||
dt = np.diff(t)
|
||||
dt[dt <= 0] = np.nan
|
||||
jerk = np.diff(accel) / dt
|
||||
onset = (t[1:] > 0.5) & (t[1:] < 3.0) # after the first solve settles, during the initial hard reaction
|
||||
assert onset.any()
|
||||
peak_onset_jerk = np.nanmax(np.abs(jerk[onset]))
|
||||
assert peak_onset_jerk < 4.0, f'onset jerk {peak_onset_jerk:.2f} m/s^3 -- braking snapped instead of ramping'
|
||||
# sanity: this scenario genuinely needs real braking (not asserting it stays small)
|
||||
assert np.min(accel) < -1.0
|
||||
|
||||
|
||||
def test_dec_on_off_agree_with_lead_present():
|
||||
# is_e2e() previously only enforced "near/closing radar lead -> pure MPC, never blend e2e" inside DEC's
|
||||
# active()-gated branch -- DEC off silently dropped the whole check, letting the e2e model's opinion
|
||||
# blend in via min() regardless of the lead. Fixed in commit 7ff32eafea (dec.has_radar_acc_lead() checked
|
||||
# unconditionally, before dec.active() is even consulted). This closed-loop test uses an adversarial
|
||||
# e2e_accel_fn (independently opinionated, not the harness's default mild self.acceleration+0.1 echo) so
|
||||
# DEC-on vs DEC-off would visibly diverge if the fix regressed.
|
||||
def adversarial_e2e(t, speed, accel):
|
||||
return -2.0 # e2e model insists on a hard brake, independent of the MPC's own view
|
||||
|
||||
def run(dec_enabled):
|
||||
params = Params()
|
||||
enable_sunnypilot_longitudinal(params, dec=dec_enabled)
|
||||
plant = Plant(lead_relevancy=True, speed=15.0, distance_lead=40.0, e2e=True,
|
||||
e2e_accel_fn=adversarial_e2e)
|
||||
accels = []
|
||||
while plant.current_time < 5.0:
|
||||
log = plant.step(v_lead=13.0) # near lead, closing slowly -- within RADAR_LEAD_ACC_MAX_DREL
|
||||
accels.append(log['acceleration'])
|
||||
return np.array(accels)
|
||||
|
||||
accel_dec_on = run(dec_enabled=True)
|
||||
accel_dec_off = run(dec_enabled=False)
|
||||
np.testing.assert_allclose(accel_dec_on, accel_dec_off, atol=0.05,
|
||||
err_msg='DEC on vs off disagree with an identical near/closing lead present')
|
||||
@@ -1,72 +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.
|
||||
|
||||
_E2ETransitionGuard bounds how fast output_a_target may drop right after DEC switches into blended mode for
|
||||
a routine reason, so the e2e model's own (previously-hidden) desiredAcceleration can't produce a same-cycle
|
||||
discontinuous brake. It must never limit a rise, never limit anything while smoothing is inactive, and never
|
||||
apply when the caller signals an urgent/immediate transition.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import _E2ETransitionGuard, TRANSITION_MAX_DROP_PER_CYCLE
|
||||
|
||||
|
||||
def test_inactive_is_identity():
|
||||
g = _E2ETransitionGuard()
|
||||
g.apply(-0.3, False)
|
||||
assert g.apply(-1.1, False) == pytest.approx(-1.1) # smoothing inactive -> raw passthrough always
|
||||
|
||||
|
||||
def test_first_call_seeds_without_limiting():
|
||||
g = _E2ETransitionGuard()
|
||||
assert g.apply(-1.1, True) == pytest.approx(-1.1) # no prior baseline -> nothing to limit against yet
|
||||
|
||||
|
||||
def test_limits_downward_jump_when_active():
|
||||
g = _E2ETransitionGuard()
|
||||
g.apply(-0.3, False) # establish baseline while inactive
|
||||
out = g.apply(-1.1, True) # a farther-in-one-cycle jump, smoothing now active
|
||||
assert out == pytest.approx(-0.3 - TRANSITION_MAX_DROP_PER_CYCLE)
|
||||
assert out > -1.1 # not the raw discontinuous value
|
||||
|
||||
|
||||
def test_never_limits_a_rise():
|
||||
g = _E2ETransitionGuard()
|
||||
g.apply(-1.0, False)
|
||||
out = g.apply(0.5, True) # accel rising -- must never be held back
|
||||
assert out == pytest.approx(0.5)
|
||||
|
||||
|
||||
def test_converges_to_raw_within_a_few_cycles():
|
||||
g = _E2ETransitionGuard()
|
||||
g.apply(-0.3, False)
|
||||
out = -0.3
|
||||
for _ in range(20):
|
||||
out = g.apply(-1.1, True)
|
||||
assert out == pytest.approx(-1.1, abs=1e-6) # eventually tracks the sustained raw value exactly
|
||||
|
||||
|
||||
def test_replays_real_route_e2e_transition():
|
||||
# route 550a71ee4c7a7fbe/0000049f--71203acd12, t~165.3-166.2: DEC switches acc->blended (routine slow-down,
|
||||
# not FCW) and the raw blended output snaps -0.312 -> -1.109 in one 50ms tick, then continues to ~-1.33.
|
||||
g = _E2ETransitionGuard()
|
||||
raw_acc = [-0.312, -0.312] # still acc mode
|
||||
raw_blended = [-1.109, -1.112, -1.121, -1.110, -1.118, -1.137, -1.140, -1.133, -1.145, -1.165,
|
||||
-1.199, -1.227, -1.255, -1.268, -1.287, -1.293, -1.326]
|
||||
for v in raw_acc:
|
||||
g.apply(v, False)
|
||||
guarded = [g.apply(v, i < 10) for i, v in enumerate(raw_blended)] # smoothing active for the first 10 frames
|
||||
assert guarded[0] == pytest.approx(-0.312 - TRANSITION_MAX_DROP_PER_CYCLE) # graded, not the -1.109 snap
|
||||
assert min(guarded[:3]) > -1.0 # nowhere near the raw value in the first few frames
|
||||
assert guarded[-1] == pytest.approx(raw_blended[-1]) # long since converged and tracking raw exactly
|
||||
|
||||
|
||||
def test_reset_drops_stale_baseline():
|
||||
g = _E2ETransitionGuard()
|
||||
g.apply(-0.3, False)
|
||||
g.reset()
|
||||
assert g.apply(-1.1, True) == pytest.approx(-1.1) # no stale baseline to limit against after reset
|
||||
@@ -1,62 +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.
|
||||
|
||||
LongitudinalPlannerSP.is_e2e() decides whether the e2e model's raw action.desiredAcceleration blends into
|
||||
the MPC's solution via min(). A near/fast-closing radar lead must always route to pure MPC regardless of
|
||||
whether DEC itself is on -- that baseline previously lived entirely inside DEC's active()-gated branch, so
|
||||
turning DEC off silently dropped it (identical lead input, different is_e2e() answer). These tests pin the
|
||||
fix: the lead check now runs unconditionally, before DEC's own toggle is even consulted.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
||||
|
||||
|
||||
class FakeDec:
|
||||
def __init__(self, active=False, mode="acc", has_radar_acc_lead=False):
|
||||
self._active = active
|
||||
self._mode = mode
|
||||
self._has_radar_acc_lead = has_radar_acc_lead
|
||||
|
||||
def active(self):
|
||||
return self._active
|
||||
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
def has_radar_acc_lead(self):
|
||||
return self._has_radar_acc_lead
|
||||
|
||||
|
||||
def make_sm(experimental_mode=True):
|
||||
return {'selfdriveState': SimpleNamespace(experimentalMode=experimental_mode)}
|
||||
|
||||
|
||||
def is_e2e(dec):
|
||||
# is_e2e only reads self.dec -- no need to construct the full LongitudinalPlannerSP.
|
||||
return LongitudinalPlannerSP.is_e2e(SimpleNamespace(dec=dec), make_sm())
|
||||
|
||||
|
||||
def test_experimental_mode_off_never_e2e():
|
||||
sm_off = make_sm(experimental_mode=False)
|
||||
assert not LongitudinalPlannerSP.is_e2e(SimpleNamespace(dec=FakeDec(active=True, mode="blended")), sm_off)
|
||||
assert not LongitudinalPlannerSP.is_e2e(SimpleNamespace(dec=FakeDec(has_radar_acc_lead=True)), sm_off)
|
||||
|
||||
|
||||
def test_lead_present_blocks_e2e_regardless_of_dec_active():
|
||||
# the bug this fixes: identical lead, DEC on vs off must agree.
|
||||
assert not is_e2e(FakeDec(active=True, mode="acc", has_radar_acc_lead=True))
|
||||
assert not is_e2e(FakeDec(active=False, mode="acc", has_radar_acc_lead=True))
|
||||
|
||||
|
||||
def test_no_lead_dec_off_falls_back_to_experimental_mode():
|
||||
assert is_e2e(FakeDec(active=False, has_radar_acc_lead=False))
|
||||
|
||||
|
||||
def test_no_lead_dec_on_follows_dec_mode():
|
||||
assert is_e2e(FakeDec(active=True, mode="blended", has_radar_acc_lead=False))
|
||||
assert not is_e2e(FakeDec(active=True, mode="acc", has_radar_acc_lead=False))
|
||||
@@ -5,8 +5,6 @@ from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController
|
||||
|
||||
|
||||
TurnDirection = custom.ModelDataV2SP.TurnDirection
|
||||
|
||||
@@ -109,11 +107,7 @@ def set_lane_turn_params():
|
||||
])
|
||||
def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params):
|
||||
dh = DesireHelper()
|
||||
relc = RoadEdgeLaneChangeController(dh)
|
||||
relc.enabled = True
|
||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||
for _ in range(10):
|
||||
dh.update(carstate, lateral_active, lane_change_prob,
|
||||
left_edge_detected=relc.left_edge_detected, right_edge_detected=relc.right_edge_detected)
|
||||
dh.update(carstate, lateral_active, lane_change_prob)
|
||||
assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, rav4kumar, 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.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.relc import (
|
||||
RoadEdgeLaneChangeController, EDGE_REACTION_TIME, EDGE_CLEAR_TIME, MIN_SPEED,
|
||||
)
|
||||
|
||||
V_HIGH = MIN_SPEED + 2.0
|
||||
V_LOW = MIN_SPEED - 1.0
|
||||
|
||||
|
||||
class DummyRoadEdge:
|
||||
def __init__(self, y):
|
||||
self.y = [y]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def relc(mocker):
|
||||
mock_params = mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.relc.Params")
|
||||
mock_params.return_value.get_bool.return_value = True
|
||||
controller = RoadEdgeLaneChangeController(DesireHelper())
|
||||
controller.enabled = True
|
||||
return controller
|
||||
|
||||
|
||||
def make_road_edges(left_y=-3.0, right_y=3.0):
|
||||
return [DummyRoadEdge(left_y), DummyRoadEdge(right_y)]
|
||||
|
||||
|
||||
def drive(controller, road_edge_stds, lane_line_probs, seconds, v_ego=V_HIGH, road_edges=None):
|
||||
for _ in range(int(seconds / DT_MDL) + 1):
|
||||
controller.update(road_edge_stds, lane_line_probs, v_ego, road_edges)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("road_edge_stds,lane_line_probs,attr", [
|
||||
([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], "left_edge_detected"),
|
||||
([0.9, 0.0], [0.8, 0.8, 0.0, 0.8], "right_edge_detected"),
|
||||
])
|
||||
def test_edge_detection(relc, road_edge_stds, lane_line_probs, attr):
|
||||
drive(relc, road_edge_stds, lane_line_probs, EDGE_REACTION_TIME + 0.1)
|
||||
assert getattr(relc, attr)
|
||||
|
||||
|
||||
def test_edge_detection_requires_time(relc):
|
||||
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME - 0.05)
|
||||
assert not relc.left_edge_detected
|
||||
|
||||
|
||||
def test_both_edges_detected(relc):
|
||||
drive(relc, [0.0, 0.0], [0.8, 0.0, 0.0, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||
assert relc.left_edge_detected
|
||||
assert relc.right_edge_detected
|
||||
|
||||
|
||||
def test_noise_doesnt_clear(relc):
|
||||
edge = ([0.0, 0.9], [0.8, 0.0, 0.8, 0.8])
|
||||
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
|
||||
|
||||
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
relc.update(*clear, V_HIGH)
|
||||
relc.update(*edge, V_HIGH)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
|
||||
def test_clears_after_window(relc):
|
||||
edge = ([0.0, 0.9], [0.8, 0.0, 0.8, 0.8])
|
||||
clear = ([0.9, 0.9], [0.8, 0.8, 0.8, 0.8])
|
||||
|
||||
drive(relc, *edge, EDGE_REACTION_TIME + 0.1)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
drive(relc, *clear, EDGE_CLEAR_TIME + 0.05)
|
||||
assert not relc.left_edge_detected
|
||||
assert relc.left_edge_timer == 0.0
|
||||
|
||||
|
||||
def test_low_speed_skips(relc):
|
||||
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, v_ego=V_LOW)
|
||||
assert not relc.left_edge_detected
|
||||
assert relc.left_edge_timer == 0.0
|
||||
|
||||
|
||||
def test_speed_drop_resets(relc):
|
||||
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
relc.update([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], V_LOW)
|
||||
assert not relc.left_edge_detected
|
||||
|
||||
|
||||
def test_param_off_resets(relc):
|
||||
drive(relc, [0.0, 0.9], [0.8, 0.0, 0.8, 0.8], EDGE_REACTION_TIME + 0.1)
|
||||
assert relc.left_edge_detected
|
||||
|
||||
relc.params.get_bool.return_value = False
|
||||
relc.read_params()
|
||||
relc.update([0.0, 0.9], [0.8, 0.0, 0.8, 0.8], V_HIGH)
|
||||
assert not relc.left_edge_detected
|
||||
assert not relc.right_edge_detected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lane_line_probs", [
|
||||
[0.0, 0.8, 0.8, 0.8],
|
||||
[0.8, 0.8, 0.8, 0.0],
|
||||
])
|
||||
def test_outer_lane_lines_do_not_drive_edge_detection(relc, lane_line_probs):
|
||||
drive(relc, [0.0, 0.0], lane_line_probs, EDGE_REACTION_TIME + 0.1)
|
||||
assert not relc.left_edge_detected
|
||||
assert not relc.right_edge_detected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("road_edge_stds,road_edges,attr", [
|
||||
([0.0, 0.9], make_road_edges(left_y=-3.0, right_y=8.0), "left_edge_detected"),
|
||||
([0.9, 0.0], make_road_edges(left_y=-8.0, right_y=3.0), "right_edge_detected"),
|
||||
])
|
||||
def test_near_road_edge_geometry_blocks_with_visible_lane_lines(relc, road_edge_stds, road_edges, attr):
|
||||
drive(relc, road_edge_stds, [0.8, 0.8, 0.8, 0.8], EDGE_REACTION_TIME + 0.1, road_edges=road_edges)
|
||||
assert getattr(relc, attr)
|
||||
|
||||
|
||||
def test_far_road_edge_geometry_does_not_block(relc):
|
||||
drive(relc, [0.0, 0.0], [0.8, 0.0, 0.0, 0.8], EDGE_REACTION_TIME + 0.1, road_edges=make_road_edges(left_y=-8.0, right_y=8.0))
|
||||
assert not relc.left_edge_detected
|
||||
assert not relc.right_edge_detected
|
||||
@@ -243,12 +243,4 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
AlertStatus.normal, AlertSize.none,
|
||||
Priority.MID, VisualAlert.none, AudibleAlert.prompt, 3.),
|
||||
},
|
||||
|
||||
EventNameSP.laneChangeRoadEdge: {
|
||||
ET.WARNING: Alert(
|
||||
"Lane Change Unavailable: Road Edge",
|
||||
"",
|
||||
AlertStatus.userPrompt, AlertSize.small,
|
||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ LOCAL_PORT_WHITELIST = {8022}
|
||||
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
||||
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
|
||||
DISALLOW_LOG_UPLOAD = threading.Event()
|
||||
METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json")
|
||||
|
||||
params = Params()
|
||||
|
||||
@@ -170,39 +169,6 @@ def getParamsAllKeys() -> list[str]:
|
||||
return keys
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsAllKeysV1() -> dict[str, str]:
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.metadata.exception")
|
||||
metadata = {}
|
||||
|
||||
try:
|
||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||
|
||||
params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []}
|
||||
for key in available_keys:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
|
||||
param_entry = {
|
||||
"key": key,
|
||||
"type": int(params.get_type(key).value),
|
||||
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
|
||||
}
|
||||
|
||||
if key in metadata:
|
||||
meta_copy = metadata[key].copy()
|
||||
param_entry["_extra"] = meta_copy
|
||||
|
||||
params_dict["params"].append(param_entry)
|
||||
return {"keys": json.dumps(params_dict.get("params", []))}
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception")
|
||||
raise
|
||||
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsMetadata() -> str:
|
||||
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
|
||||
|
||||
@@ -97,12 +97,11 @@ The compiler splices a list-context `$ref` into its parent list. Macros may refe
|
||||
|
||||
```
|
||||
1. common/params_keys.h — add/remove the C++ param key
|
||||
2. params_metadata.json — automated via update_params_metadata.py
|
||||
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||
7. commit
|
||||
2. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||
3. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
4. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||
5. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||
6. commit
|
||||
```
|
||||
|
||||
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -537,12 +537,6 @@
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "RoadEdgeLaneChangeEnabled",
|
||||
"widget": "toggle",
|
||||
"title": "Block Lane Change: Road Edge Detection",
|
||||
"description": "Blocks the lane change if the model sees a road edge on your signaled side."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -593,26 +587,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "RadarDistance",
|
||||
"widget": "toggle",
|
||||
"title": "Radar Distance",
|
||||
"description": "De-noises the lead sunnypilot follows -- holds it through brief radar flicker/dropout and smooths a jittery (track-flipping) lead -- so it does not lose and re-grab the lead or hunt the gap. Braking is never reduced below stock.",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "DisengageOnAccelerator",
|
||||
"widget": "toggle",
|
||||
@@ -646,58 +620,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AccelPersonalityEnabled",
|
||||
"widget": "toggle",
|
||||
"title": "Enable Acceleration Profiles",
|
||||
"description": "Enables Eco/Normal/Sport profiles -- a tier-scaled take-off from a stop and a slightly wider, smoother following gap. Braking is never reduced below stock.",
|
||||
"visibility": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "AccelPersonality",
|
||||
"widget": "multiple_button",
|
||||
"title": "Acceleration Profile",
|
||||
"description": "Eco takes off gently and keeps the roomiest, smoothest following gap; Sport takes off briskly and follows a touch tighter. Hard-braking authority is always preserved.",
|
||||
"options": [
|
||||
{
|
||||
"value": 0,
|
||||
"label": "Eco"
|
||||
},
|
||||
{
|
||||
"value": 1,
|
||||
"label": "Normal"
|
||||
},
|
||||
{
|
||||
"value": 2,
|
||||
"label": "Sport"
|
||||
}
|
||||
],
|
||||
"enablement": [
|
||||
{
|
||||
"type": "capability",
|
||||
"field": "has_longitudinal_control",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"type": "param",
|
||||
"key": "AccelPersonalityEnabled",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "IntelligentCruiseButtonManagement",
|
||||
"widget": "toggle",
|
||||
@@ -2079,22 +2001,6 @@
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "PlanplusControl",
|
||||
"widget": "option",
|
||||
"title": "Plan Plus Controls",
|
||||
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lane center; too high and it will ping-pong.",
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1,
|
||||
"enablement": [
|
||||
{
|
||||
"type": "param",
|
||||
"key": "ShowAdvancedControls",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2262,50 +2168,6 @@
|
||||
"title": "Toyota / Lexus Settings",
|
||||
"description": "",
|
||||
"items": [
|
||||
{
|
||||
"key": "ToyotaAutoHold",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnhancedBsm",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: Prius TSS2 BSM and some tssp",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaTSS2Long",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Toyota: custom longitudinal for TSS2",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaDriveMode",
|
||||
"widget": "toggle",
|
||||
"needs_onroad_cycle": true,
|
||||
"title": "Enable drive mode btn link",
|
||||
"enablement": [
|
||||
{
|
||||
"type": "not_engaged"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ToyotaEnforceStockLongitudinal",
|
||||
"widget": "toggle",
|
||||
|
||||
@@ -24,16 +24,6 @@ sections:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: RadarDistance
|
||||
widget: toggle
|
||||
title: Radar Distance
|
||||
description: De-noises the lead sunnypilot follows -- holds it through brief radar flicker/dropout and
|
||||
smooths a jittery (track-flipping) lead -- so it does not lose and re-grab the lead or hunt the gap.
|
||||
Braking is never reduced below stock.
|
||||
visibility:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: DisengageOnAccelerator
|
||||
widget: toggle
|
||||
title: Disengage Cruise on Accelerator Pedal
|
||||
@@ -53,32 +43,6 @@ sections:
|
||||
label: Relaxed
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: AccelPersonalityEnabled
|
||||
widget: toggle
|
||||
title: Enable Acceleration Profiles
|
||||
description: Enables Eco/Normal/Sport profiles -- a tier-scaled take-off from a stop and a slightly wider,
|
||||
smoother following gap. Braking is never reduced below stock.
|
||||
visibility:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- key: AccelPersonality
|
||||
widget: multiple_button
|
||||
title: Acceleration Profile
|
||||
description: Eco takes off gently and keeps the roomiest, smoothest following gap; Sport takes off briskly
|
||||
and follows a touch tighter. Hard-braking authority is always preserved.
|
||||
options:
|
||||
- value: 0
|
||||
label: Eco
|
||||
- value: 1
|
||||
label: Normal
|
||||
- value: 2
|
||||
label: Sport
|
||||
enablement:
|
||||
- $ref: '#/macros/longitudinal'
|
||||
- type: param
|
||||
key: AccelPersonalityEnabled
|
||||
equals: true
|
||||
- key: IntelligentCruiseButtonManagement
|
||||
widget: toggle
|
||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||
|
||||
@@ -51,16 +51,6 @@ sections:
|
||||
key: LagdToggle
|
||||
equals: true
|
||||
- $ref: '#/macros/advanced_only'
|
||||
- key: PlanplusControl
|
||||
widget: option
|
||||
title: Plan Plus Controls
|
||||
description: Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover
|
||||
to lane center; too high and it will ping-pong.
|
||||
min: 0.0
|
||||
max: 2.0
|
||||
step: 0.1
|
||||
enablement:
|
||||
- $ref: '#/macros/advanced_only'
|
||||
- id: lateral_control
|
||||
title: Lateral Control
|
||||
description: Neural network lateral control for supported models
|
||||
|
||||
@@ -255,7 +255,3 @@ sections:
|
||||
key: AutoLaneChangeTimer
|
||||
op: '>'
|
||||
value: 0
|
||||
- key: RoadEdgeLaneChangeEnabled
|
||||
widget: toggle
|
||||
title: 'Block Lane Change: Road Edge Detection'
|
||||
description: Blocks the lane change if the model sees a road edge on your signaled side.
|
||||
|
||||
@@ -60,30 +60,6 @@ sections:
|
||||
title: Toyota / Lexus Settings
|
||||
description: ''
|
||||
items:
|
||||
- key: ToyotaAutoHold
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: 'Toyota: Auto Brake Hold FOR TSS2 HYBRID CARS'
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- key: ToyotaEnhancedBsm
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: 'Toyota: Prius TSS2 BSM and some tssp'
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- key: ToyotaTSS2Long
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: 'Toyota: custom longitudinal for TSS2'
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- key: ToyotaDriveMode
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
title: Enable drive mode btn link
|
||||
enablement:
|
||||
- $ref: '#/macros/not_engaged'
|
||||
- key: ToyotaEnforceStockLongitudinal
|
||||
widget: toggle
|
||||
needs_onroad_cycle: true
|
||||
|
||||
@@ -1,86 +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 json
|
||||
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH
|
||||
|
||||
|
||||
def test_get_params_all_keys_v1():
|
||||
"""
|
||||
Test the getParamsAllKeysV1 API endpoint.
|
||||
|
||||
Why:
|
||||
This endpoint is used by the UI (and potentially external tools) to fetch the list of
|
||||
available parameters along with their metadata (titles, descriptions, options, constraints).
|
||||
We need to ensure it returns the correct structure and that the metadata from
|
||||
params_metadata.json is correctly merged into the response.
|
||||
|
||||
Expected:
|
||||
- The response should contain a "keys" field which is a JSON string of a list of parameters.
|
||||
- Each parameter object should have "key", "type", "default_value", and optionally "_extra".
|
||||
- The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching
|
||||
the source of truth (params_metadata.json).
|
||||
"""
|
||||
response = getParamsAllKeysV1()
|
||||
assert "keys" in response
|
||||
|
||||
keys_json = response["keys"]
|
||||
params_list = json.loads(keys_json)
|
||||
|
||||
assert isinstance(params_list, list)
|
||||
assert len(params_list) > 0
|
||||
|
||||
# Check structure of first item
|
||||
first_param = params_list[0]
|
||||
assert "key" in first_param
|
||||
assert "type" in first_param
|
||||
assert "default_value" in first_param
|
||||
|
||||
if "_extra" in first_param:
|
||||
assert isinstance(first_param["_extra"], dict)
|
||||
assert "default" not in first_param["_extra"]
|
||||
assert "type" not in first_param["_extra"]
|
||||
|
||||
# Load the source of truth
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Verify that the API response matches the metadata file for a few sample keys
|
||||
# This ensures the plumbing is working without being brittle to content changes
|
||||
|
||||
# 1. Check a key that should have metadata
|
||||
keys_with_metadata = [k for k in params_list if k["key"] in metadata]
|
||||
assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys"
|
||||
|
||||
for param in keys_with_metadata[:5]: # Check first 5 matches
|
||||
key = param["key"]
|
||||
expected_meta = metadata[key]
|
||||
|
||||
assert "_extra" in param, f"Parameter {key} should have _extra field"
|
||||
actual_meta = param["_extra"]
|
||||
|
||||
# Verify all fields in JSON are present in the API response
|
||||
for meta_key, meta_val in expected_meta.items():
|
||||
assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}"
|
||||
assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}"
|
||||
|
||||
# 2. Check that we are correctly serving options if they exist
|
||||
params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})]
|
||||
if params_with_options:
|
||||
param = params_with_options[0]
|
||||
key = param["key"]
|
||||
assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list"
|
||||
assert param["_extra"]["options"] == metadata[key]["options"]
|
||||
|
||||
# 3. Check that we are correctly serving numeric constraints if they exist
|
||||
params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})]
|
||||
if params_with_constraints:
|
||||
param = params_with_constraints[0]
|
||||
key = param["key"]
|
||||
assert param["_extra"]["min"] == metadata[key]["min"]
|
||||
assert param["_extra"]["max"] == metadata[key]["max"]
|
||||
assert param["_extra"]["step"] == metadata[key]["step"]
|
||||
@@ -1,284 +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 json
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH
|
||||
|
||||
|
||||
def test_metadata_json_exists():
|
||||
"""
|
||||
Test that the params_metadata.json file exists at the expected path.
|
||||
|
||||
Why:
|
||||
The metadata file is the source of truth for parameter descriptions, options, and constraints.
|
||||
If it's missing, the UI will not be able to display rich information for parameters.
|
||||
|
||||
Expected:
|
||||
The file should exist at sunnypilot/sunnylink/params_metadata.json.
|
||||
"""
|
||||
assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}"
|
||||
|
||||
|
||||
def test_metadata_json_valid():
|
||||
"""
|
||||
Test that the params_metadata.json file contains valid JSON.
|
||||
|
||||
Why:
|
||||
Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or
|
||||
resulting in missing metadata.
|
||||
|
||||
Expected:
|
||||
The file content should be parseable as a JSON object (dictionary).
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail("Metadata file is not valid JSON")
|
||||
|
||||
assert isinstance(data, dict), "Metadata root must be a dictionary"
|
||||
|
||||
|
||||
def test_all_params_have_metadata():
|
||||
"""
|
||||
Test that every parameter in the codebase has a corresponding entry in params_metadata.json.
|
||||
|
||||
Why:
|
||||
We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase
|
||||
should also be documented in the metadata file.
|
||||
|
||||
Expected:
|
||||
There should be no parameters in Params() that are missing from the metadata file.
|
||||
If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = [k.decode('utf-8') for k in params.all_keys()]
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
missing_keys = [key for key in all_keys if key not in metadata]
|
||||
|
||||
if missing_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters are missing from metadata: {missing_keys}. "
|
||||
+ "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update."
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_keys_exist_in_params():
|
||||
"""
|
||||
Test that all keys in params_metadata.json actually exist in the codebase.
|
||||
|
||||
Why:
|
||||
We want to avoid stale metadata for parameters that have been removed or renamed.
|
||||
This keeps the metadata file clean and relevant.
|
||||
|
||||
Expected:
|
||||
There should be no keys in the metadata file that are not present in Params().
|
||||
This prints a warning rather than failing, as it's less critical than missing metadata.
|
||||
"""
|
||||
params = Params()
|
||||
all_keys = {k.decode('utf-8') for k in params.all_keys()}
|
||||
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
extra_keys = [key for key in metadata.keys() if key not in all_keys]
|
||||
|
||||
if extra_keys:
|
||||
print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}")
|
||||
|
||||
|
||||
def test_no_default_titles():
|
||||
"""
|
||||
Test that no parameter has a title that is identical to its key.
|
||||
|
||||
Why:
|
||||
The default behavior of the update script is to set the title equal to the key.
|
||||
We want to force developers to provide human-readable, descriptive titles for all parameters.
|
||||
|
||||
Expected:
|
||||
No parameter metadata should have 'title' == 'key'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key]
|
||||
|
||||
if default_title_keys:
|
||||
pytest.fail(
|
||||
f"The following parameters have default titles (title == key): {default_title_keys}. "
|
||||
+ "Please update 'params_metadata.json' with descriptive titles."
|
||||
)
|
||||
|
||||
|
||||
def test_options_structure():
|
||||
"""
|
||||
Test that the 'options' field in metadata follows the correct structure.
|
||||
|
||||
Why:
|
||||
The UI expects 'options' to be a list of objects with 'value' and 'label' keys.
|
||||
Incorrect structure will break the UI rendering for dropdowns/toggles.
|
||||
|
||||
Expected:
|
||||
If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "options" in meta:
|
||||
options = meta["options"]
|
||||
assert isinstance(options, list), f"Options for {key} must be a list"
|
||||
for option in options:
|
||||
assert isinstance(option, dict), f"Option in {key} must be a dictionary"
|
||||
assert "value" in option, f"Option in {key} must have a 'value' key"
|
||||
assert "label" in option, f"Option in {key} must have a 'label' key"
|
||||
|
||||
|
||||
def test_numeric_constraints():
|
||||
"""
|
||||
Test that numeric parameters have valid 'min', 'max', and 'step' constraints.
|
||||
|
||||
Why:
|
||||
The UI uses these constraints to validate user input and render sliders/steppers.
|
||||
Missing or invalid constraints can lead to UI bugs or invalid parameter values.
|
||||
|
||||
Expected:
|
||||
If any of min/max/step is present, ALL of them must be present.
|
||||
They must be numbers (int/float), and min must be less than max.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
for key, meta in metadata.items():
|
||||
if "min" in meta or "max" in meta or "step" in meta:
|
||||
assert "min" in meta, f"Numeric param {key} must have 'min'"
|
||||
assert "max" in meta, f"Numeric param {key} must have 'max'"
|
||||
assert "step" in meta, f"Numeric param {key} must have 'step'"
|
||||
|
||||
assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number"
|
||||
assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number"
|
||||
assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number"
|
||||
assert meta["min"] < meta["max"], f"Min must be less than max for {key}"
|
||||
|
||||
|
||||
def test_known_params_metadata():
|
||||
"""
|
||||
Test specific known parameters to ensure they have the expected rich metadata.
|
||||
|
||||
Why:
|
||||
This acts as a spot check to ensure that our rich metadata population logic is working correctly
|
||||
and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved.
|
||||
|
||||
Expected:
|
||||
'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed).
|
||||
'CustomAccLongPressIncrement' should have min=1, max=10, step=1.
|
||||
"""
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
# Check an enum-like param
|
||||
lp = metadata.get("LongitudinalPersonality")
|
||||
assert lp is not None
|
||||
assert "options" in lp
|
||||
assert len(lp["options"]) == 3
|
||||
assert lp["options"][0]["label"] == "Aggressive"
|
||||
assert lp["options"][0]["value"] == 0
|
||||
|
||||
# Check a numeric param
|
||||
acc_long = metadata.get("CustomAccLongPressIncrement")
|
||||
assert acc_long is not None
|
||||
assert acc_long["min"] == 1
|
||||
assert acc_long["max"] == 10
|
||||
assert acc_long["step"] == 1
|
||||
|
||||
|
||||
def test_torque_control_tune_versions_in_sync():
|
||||
"""
|
||||
Test that TorqueControlTune options in params_metadata.json match versions in latcontrol_torque_versions.json.
|
||||
|
||||
Why:
|
||||
The TorqueControlTune dropdown in the UI should always reflect the available torque tune versions.
|
||||
If versions are added/removed from latcontrol_torque_versions.json, the metadata must be updated accordingly.
|
||||
|
||||
Expected:
|
||||
- TorqueControlTune should have a 'Default' option with empty string value
|
||||
- All versions from latcontrol_torque_versions.json should be present in the options
|
||||
- The version values and labels should match between both files
|
||||
"""
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
|
||||
versions_json_path = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
|
||||
sync_script_path = "python3 sunnypilot/sunnylink/tools/sync_torque_versions.py"
|
||||
|
||||
# Load both files
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
with open(versions_json_path) as f:
|
||||
versions = json.load(f)
|
||||
|
||||
# Get TorqueControlTune metadata
|
||||
torque_tune = metadata.get("TorqueControlTune")
|
||||
if torque_tune is None:
|
||||
pytest.fail(f"TorqueControlTune not found in params_metadata.json. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if "options" not in torque_tune:
|
||||
pytest.fail(f"TorqueControlTune must have options. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
options = torque_tune["options"]
|
||||
if not isinstance(options, list):
|
||||
pytest.fail(f"TorqueControlTune options must be a list. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if len(options) == 0:
|
||||
pytest.fail(f"TorqueControlTune must have at least one option. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Check that Default option exists
|
||||
default_option = next((opt for opt in options if opt.get("value") == ""), None)
|
||||
if default_option is None:
|
||||
pytest.fail(f"TorqueControlTune must have a 'Default' option with empty string value. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
if default_option.get("label") != "Default":
|
||||
pytest.fail(f"Default option must have label 'Default'. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Build expected options from versions.json
|
||||
expected_version_keys = set(versions.keys())
|
||||
actual_version_keys = set()
|
||||
|
||||
for option in options:
|
||||
if option.get("value") == "":
|
||||
continue # Skip the default option
|
||||
|
||||
label = option.get("label")
|
||||
value = option.get("value")
|
||||
|
||||
# Check that this option corresponds to a version
|
||||
if label not in versions:
|
||||
pytest.fail(f"Option label '{label}' not found in latcontrol_torque_versions.json. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
# Check that the value matches the version number
|
||||
expected_value = float(versions[label]["version"])
|
||||
if value != expected_value:
|
||||
pytest.fail(f"Option '{label}' has value {value}, expected {expected_value}. Please run '{sync_script_path}' to sync.")
|
||||
|
||||
actual_version_keys.add(label)
|
||||
|
||||
# Check that all versions are represented
|
||||
missing_versions = expected_version_keys - actual_version_keys
|
||||
if missing_versions:
|
||||
pytest.fail(f"The following versions are missing from TorqueControlTune options: {missing_versions}. " +
|
||||
f"Please run '{sync_script_path}' to sync.")
|
||||
|
||||
extra_versions = actual_version_keys - expected_version_keys
|
||||
if extra_versions:
|
||||
pytest.fail("The following versions in TorqueControlTune options are not in latcontrol_torque_versions.json: " +
|
||||
f"{extra_versions}. Please run '{sync_script_path}' to sync.")
|
||||
@@ -1,133 +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 json
|
||||
import os
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
|
||||
TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
|
||||
|
||||
|
||||
def main():
|
||||
params = Params()
|
||||
all_keys = params.all_keys()
|
||||
|
||||
if os.path.exists(METADATA_PATH):
|
||||
with open(METADATA_PATH) as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
|
||||
# Add new keys
|
||||
for key in all_keys:
|
||||
key_str = key.decode("utf-8")
|
||||
if key_str not in data:
|
||||
print(f"Adding new key: {key_str}")
|
||||
data[key_str] = {
|
||||
"title": key_str,
|
||||
"description": "",
|
||||
}
|
||||
|
||||
# Remove deleted keys
|
||||
# keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys]
|
||||
# for k in keys_to_remove:
|
||||
# print(f"Removing deleted key: {k}")
|
||||
# del data[k]
|
||||
|
||||
# Sort keys
|
||||
sorted_data = dict(sorted(data.items()))
|
||||
|
||||
with open(METADATA_PATH, "w") as f:
|
||||
json.dump(sorted_data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Updated {METADATA_PATH}")
|
||||
|
||||
# update onroad screen brightness params
|
||||
update_onroad_brightness_param()
|
||||
|
||||
# update onroad screen brightness timer params
|
||||
update_onroad_brightness_timer_param()
|
||||
|
||||
# update torque versions param
|
||||
update_torque_versions_param()
|
||||
|
||||
|
||||
def update_onroad_brightness_param():
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
if "OnroadScreenOffBrightness" in params_metadata:
|
||||
options = [
|
||||
{"value": 0, "label": "Auto (Default)"},
|
||||
{"value": 1, "label": "Auto (Dark)"},
|
||||
{"value": 2, "label": "Screen Off"},
|
||||
]
|
||||
for i in range(3, 23):
|
||||
options.append({"value": i, "label": f"{(i - 2) * 5} %"})
|
||||
params_metadata["OnroadScreenOffBrightness"]["options"] = options
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
print(f"Updated OnroadScreenOffBrightness options in params_metadata.json with {len(options)} options.")
|
||||
except Exception as e:
|
||||
print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}")
|
||||
|
||||
|
||||
def update_onroad_brightness_timer_param():
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
if "OnroadScreenOffTimer" in params_metadata:
|
||||
options = []
|
||||
for _index, seconds in sorted(ONROAD_BRIGHTNESS_TIMER_VALUES.items()):
|
||||
label = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m"
|
||||
options.append({"value": seconds, "label": label})
|
||||
params_metadata["OnroadScreenOffTimer"]["options"] = options
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
print(f"Updated OnroadScreenOffTimer options in params_metadata.json with {len(options)} options.")
|
||||
except Exception as e:
|
||||
print(f"Failed to update OnroadScreenOffTimer options in params_metadata.json: {e}")
|
||||
|
||||
|
||||
def update_torque_versions_param():
|
||||
with open(TORQUE_VERSIONS_JSON) as f:
|
||||
current_versions = json.load(f)
|
||||
|
||||
try:
|
||||
with open(METADATA_PATH) as f:
|
||||
params_metadata = json.load(f)
|
||||
|
||||
options = [{"value": "", "label": "Default"}]
|
||||
for version_key, version_data in current_versions.items():
|
||||
version_value = float(version_data["version"])
|
||||
options.append({"value": version_value, "label": str(version_key)})
|
||||
|
||||
if "TorqueControlTune" in params_metadata:
|
||||
params_metadata["TorqueControlTune"]["options"] = options
|
||||
|
||||
with open(METADATA_PATH, 'w') as f:
|
||||
json.dump(params_metadata, f, indent=2)
|
||||
f.write('\n')
|
||||
|
||||
print(f"Updated TorqueControlTune options in params_metadata.json with {len(options)} options: \n{options}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -45,9 +45,8 @@ class ScrollState(Enum):
|
||||
|
||||
|
||||
class GuiScrollPanel2:
|
||||
def __init__(self, horizontal: bool = True, handle_out_of_bounds: bool = True) -> None:
|
||||
def __init__(self, horizontal: bool = True) -> None:
|
||||
self._horizontal = horizontal
|
||||
self._handle_out_of_bounds = handle_out_of_bounds
|
||||
self._state = ScrollState.STEADY
|
||||
self._offset: rl.Vector2 = rl.Vector2(0, 0)
|
||||
self._initial_click_event: MouseEvent | None = None
|
||||
@@ -99,7 +98,7 @@ class GuiScrollPanel2:
|
||||
# simple exponential return if out of bounds
|
||||
# out of bounds is handled by snapping, so skip if set
|
||||
out_of_bounds = self.get_offset() > max_offset or self.get_offset() < min_offset
|
||||
if out_of_bounds and snap_target is None and self._handle_out_of_bounds:
|
||||
if out_of_bounds and snap_target is None:
|
||||
target = max_offset if self.get_offset() > max_offset else min_offset
|
||||
|
||||
dt = rl.get_frame_time() or 1e-6
|
||||
|
||||
@@ -75,6 +75,7 @@ class _Scroller(Widget):
|
||||
self._items: list[Widget] = []
|
||||
self._horizontal = horizontal
|
||||
self._snap_items = snap_items
|
||||
assert not self._snap_items or self._horizontal, "Snapping is only supported for horizontal scrolling"
|
||||
self._spacing = spacing
|
||||
self._pad = pad
|
||||
|
||||
@@ -190,12 +191,8 @@ class _Scroller(Widget):
|
||||
snap_target: float | None = None
|
||||
if self._snap_items and visible_items and self._scrolling_to[0] is None:
|
||||
# TODO: this doesn't handle two small buttons at the edges well
|
||||
if self._horizontal:
|
||||
center_pos = self._rect.x + self._rect.width / 2
|
||||
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||
else:
|
||||
center_pos = self._rect.y + self._rect.height / 2
|
||||
closest_delta_pos = min((((item.rect.y + item.rect.height / 2) - center_pos) for item in visible_items), key=abs)
|
||||
center_pos = self._rect.x + self._rect.width / 2
|
||||
closest_delta_pos = min((((item.rect.x + item.rect.width / 2) - center_pos) for item in visible_items), key=abs)
|
||||
snap_target = self.scroll_panel.get_offset() - closest_delta_pos
|
||||
|
||||
return self.scroll_panel.update(self._rect, content_size, snap_target=snap_target)
|
||||
|
||||
@@ -5,23 +5,23 @@ requires-python = ">=3.12.3, <3.13"
|
||||
[[package]]
|
||||
name = "acados"
|
||||
version = "0.2.2"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=acados&rev=release-acados#25933c81c7db5573afe38f012f52befc15cdb804" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=acados&rev=release-acados#a38a7abfa04eb2f545b1e973ce81597b241d1ae9" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.2"
|
||||
version = "2.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.5"
|
||||
version = "3.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -30,27 +30,29 @@ dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -124,90 +126,87 @@ wheels = [
|
||||
[[package]]
|
||||
name = "bootstrap-icons"
|
||||
version = "1.10.5.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bootstrap-icons&rev=release-bootstrap-icons#d7e41838af9320328dd7102f9288a94f0cf0fa9f" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bootstrap-icons&rev=release-bootstrap-icons#c4393237b34dffdb325b5e72b24712c5ca074e03" }
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "1.0.8"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2#12916f9f84848239e5a69a8c5a282f3dcb0d86a3" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2#9341fe191031e3a635c2b8a2c5151cd4443d977f" }
|
||||
|
||||
[[package]]
|
||||
name = "capnproto"
|
||||
version = "1.0.1"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto#a75eb83bd98440e890261f82aa8dcfbda0b0a206" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto#c2b8852cf28c5e9e8443420703d92c6bdbd48e53" }
|
||||
|
||||
[[package]]
|
||||
name = "catch2"
|
||||
version = "2.13.10"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2#7287b4c99d23fdd87e66f9941b0e35ece6b4d91b" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=catch2&rev=release-catch2#762e049ae7392778a72024929930f52a20f86139" }
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.5.20"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
version = "3.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.4.1"
|
||||
version = "8.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -252,26 +251,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.14.1"
|
||||
version = "7.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/83/6051c2a2feab48ae5bd27c84ef047191d2d4a3172f689e38eaa48ed17db1/coverage-7.15.1.tar.gz", hash = "sha256:165e9949eaf222ef1f018635d0d7f368a23bfe0212af558534c40d8c04686d67", size = 927640, upload-time = "2026-07-12T20:58:19.908Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/76/32c1826309beaf4604c54accef108fdd611e5e5e93f2f5192f050cd5f6bd/coverage-7.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9476292594309db922cc841dd13b303b3c388f4c25d279884f7e2341c681f80", size = 221497, upload-time = "2026-07-12T20:56:37.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/5c/b88ce0d68fa550c7f3b58617fbf363bce64df5bf8295a01b627e4696e022/coverage-7.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c579056b0de461b3a62318b63d0b6ce90aed7f8158d3f00da094df82f29d189", size = 221854, upload-time = "2026-07-12T20:56:39.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/fe/8509fd2a66fc4e0a829f76a0f0b1dc3cc163368352435b5f243168658077/coverage-7.15.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:23214bdbe226f2b0e9c66a7d6a1d59d4a88045dcf86e702cf0fe0d0935e3d615", size = 253359, upload-time = "2026-07-12T20:56:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/81/c7009ed7ea9765adb2b9d095054d748266fae5f07ac6c5f925f33715fcde/coverage-7.15.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df164be93b46b4825cc39339440a05edc54c4d1d865ba4a60fd43d151a2a1cd3", size = 256096, upload-time = "2026-07-12T20:56:42.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/52/dc8ee03968a5ba86e2da5aa48ddc9e3747bd65d63825fdce2d96acb9c5ff/coverage-7.15.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a524fca1a6f08927d9dc2d4c873cfb7bd7202c247f08b14bdc02424071b8b304", size = 257211, upload-time = "2026-07-12T20:56:43.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/27/95d7623908da8937deb53d48efcdbf423907a47540e63c62fa21372c652b/coverage-7.15.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d70f3542cd38de85a9e257dcb1ac4c1ab4b6d7d2c2a645809207556628755d1c", size = 259473, upload-time = "2026-07-12T20:56:44.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3b/730d761928de97d585465680b568ae69622fb40716babadeabffe75cb51b/coverage-7.15.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d78aa537237212c4313aabe5e964b66acc86350ed19ebc56a3e202df33b6077b", size = 253759, upload-time = "2026-07-12T20:56:46.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/fc/6b9277acff1f9484b6c12857af5774689d1a6a95e13265f7405329d2f5da/coverage-7.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a318112bb4f79d9d04766196d5a3388caa825908a6a9b052aa87de3d9aea7c61", size = 255131, upload-time = "2026-07-12T20:56:48.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f2/c704f86129594ba34e25a64695d2068c71d51c2b98907184d716c94f4aec/coverage-7.15.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e55d24cada901963eed5bc89fa562aa033f0d84b9d3de4ecf363737c13aed11e", size = 253275, upload-time = "2026-07-12T20:56:49.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/29/80fee8af47de4a6dce71ccf2938491f444687a756af258a56d8469b8f1b0/coverage-7.15.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3c78f0cea7275342cf2adc2ad5fdd0aafa106ad91e66d573568f2fcf62c41df5", size = 257345, upload-time = "2026-07-12T20:56:51.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/21/a1e7d7ed1b48a8adf8fd5154d9e83fcc5ad8e6ff20ae00e44865057dce8d/coverage-7.15.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:86bd37eabe39977216f630a7fc1b698e7f5e81a191c7186013245c6c3d313f9d", size = 252844, upload-time = "2026-07-12T20:56:52.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/8c/a4bc26e6ee207d412f3678f04d74be1550e83140563ca0e4997510579712/coverage-7.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6db15c217693bdc3ca0b84de1ba9afafe1c14c26a8a29d77f4ed0de2b6132e2", size = 254716, upload-time = "2026-07-12T20:56:53.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/9d/8ad0266ecfada6353cf6627a1a02294cf55a907521b6ee0bd7b770cfd659/coverage-7.15.1-cp312-cp312-win32.whl", hash = "sha256:359f3fbe09a51500c51966596ee4ee4070b356552c70b3b2420eb200d68e0f76", size = 223554, upload-time = "2026-07-12T20:56:55.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/6d/24224929e06c6e05a93f738bc5f9e8e6ab658f8f1d9b823e7b85430e28b8/coverage-7.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:fa75dc099c126e941a9c0baa8ebd2cbc78bd778687534fe410baf754f6d9e374", size = 224087, upload-time = "2026-07-12T20:56:57.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/23/f81441dd01de88e53c97842e706907b307d9078918c3f4998b11e9ac7250/coverage-7.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:26f89cf6d0634375f454fa71057945ad18edb0f1607a90fecf22c57dc3dc289a", size = 223472, upload-time = "2026-07-12T20:56:58.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/98/07a67cf1a26e795d617ed5c540c042b0ac87b72f810c30c07f076cf334f3/coverage-7.15.1-py3-none-any.whl", hash = "sha256:717d01e6e00bed56ad13306f19e0dd2f4f645ee8159d2c72c72301d6cfc7090c", size = 213284, upload-time = "2026-07-12T20:58:18.079Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -292,41 +291,39 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "48.0.0"
|
||||
version = "49.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -340,33 +337,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cython"
|
||||
version = "3.2.5"
|
||||
version = "3.2.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/3b/ebd94c8b85f8e41b5015a9ed94ee3df866024d480d05cd08b774684fb81d/cython-3.2.5.tar.gz", hash = "sha256:3dd42e4cf36ad15f265bdfec2337cc00c688c8eb6d374ffd13bb19437c27bba1", size = 3286381, upload-time = "2026-05-23T19:34:08.439Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/6b/80101e02ebacaf9232ecf32bf6a788d36b27d820ee02434746252569ef98/cython-3.2.8.tar.gz", hash = "sha256:f4f23a56b25221a06f91817fe8f3114ab8b48a4fac73187dbb64bc2c4a87961f", size = 3290300, upload-time = "2026-06-30T07:41:57.874Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a6/efc97000fdb2f34e2431eb09a6ab4de9fbd3bcdb73a8f9d224afa4a9abd3/cython-3.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eb38b89e5a8eb2508a1a0832063826b0703dfb02be84e4aa34b8818ce0ca50fe", size = 2979670, upload-time = "2026-05-23T19:34:41.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b7/951206add609c11f3bb9e82329a653c39a8bc9039c13bce57362caf84bb6/cython-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80e1e5cba5b4b9890364e9360939fc298c474f25754bb4bb861270d24bda6d6", size = 3232779, upload-time = "2026-05-23T19:34:43.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/aa/8a1d02eabe8bc1e5066fde920010a4a4a4c5f0bac3625d8e7c946f72ef98/cython-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e2c976ee96da4deff50506c7882ccebb4a932fc178ef27eb42bfde959839", size = 3400054, upload-time = "2026-05-23T19:34:45.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/30/67a1b6192c828456f096d4bf4d840b9a749904b9030d9f857549fc1f9b53/cython-3.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:29243859d6824e2d33bae92fc83d591c3671b6d9ac1b757fa264b894ae906c2b", size = 2759539, upload-time = "2026-05-23T19:34:47.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/de/e3e0cf5704fe569d54b8cd5dc316c9fbf08b1b74728732f86e90168b7a3f/cython-3.2.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:224149d18d980e6ea5001b70fc7ce096c1891d59035dfa9cc5ede50f55804913", size = 2879054, upload-time = "2026-05-23T19:35:18.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d1/0a6a8caa35c4c57a1f1866b1141c2d00c6af67f73edbe34b2baec6919ccf/cython-3.2.5-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:992a50e90d01813333752f374a4405863113059ec67102ab8d6a431a171ee328", size = 3210422, upload-time = "2026-05-23T19:35:20.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b8/2523398ec96bb0c9bf69ada625a2256a581940b09fe11fcd0029f26ef4ad/cython-3.2.5-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8d7b81e6a52a84a02993f01aa5873786ba1dd593c892d93d5fe9866da0bad297", size = 2863809, upload-time = "2026-05-23T19:35:22.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/3d/6b2f316d97bdb02283d79934e50da5cedfec65a536cdd3d69cc3a93486f9/cython-3.2.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34d21aeb08477c9173e8be7a566b19e880a7c8109ec6bb47a4b20cb680141114", size = 2992518, upload-time = "2026-05-23T19:35:24.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/2c/c9238db1eba208e226d363c00c8b74bf531a6b40c75df2334baa85e142bf/cython-3.2.5-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c4c79e697db55f082a2d3ba97702e71881d5bb1f56f0a80fa338e69101e4c59b", size = 2886221, upload-time = "2026-05-23T19:35:26.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/15/229cc5c2ed92bb8b43c73a3d31c2b4eaf498409300c34a06d93147f7a42b/cython-3.2.5-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:39acb30eba78ba6d995d5cf3d97d57d450663d93aac6f8b93753d2b89d768c60", size = 3226990, upload-time = "2026-05-23T19:35:28.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/31/9c0024f2c772fc303f8cae2a204bcad2fedfaf921ba71cf13a878639432d/cython-3.2.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:382122de8d6b6024fc374fabc3a2b14ba5860ed981c25055ed14fe44278b9dc7", size = 3111004, upload-time = "2026-05-23T19:35:30.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/71/8b528247e42ee63cbe1c1d53805d30b28663fa782c88da4a9b69a1a412dd/cython-3.2.5-cp39-abi3-win32.whl", hash = "sha256:0bc29c7f870b09efdb1f583fbec9592b33af81a7ce273b89c8f5163d7572d5c1", size = 2440395, upload-time = "2026-05-23T19:35:33.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/4d/81c91d3279d156ee2c9ead7ed9eaa862e498066d759e92fb83d0d842c5a7/cython-3.2.5-cp39-abi3-win_arm64.whl", hash = "sha256:85b2944c3eddfc230f9082720195a2e9f869908e5a8b3185be1be832755ee7fc", size = 2446963, upload-time = "2026-05-23T19:35:35.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/5c/9cd909e6a8bb178e4e0f9a2a9227c8201a2be38abe45ada4a4c3e9154277/cython-3.2.5-py3-none-any.whl", hash = "sha256:dc1c8cebb7df5bce37f5f8dc1e5bf04313272a5973d50a55c0ec76c83812911b", size = 1257622, upload-time = "2026-05-23T19:34:05.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/c4/47e0bcfc15b36b1c5cbde5235c60bf88df552ab216ddb836d7f816386ae6/cython-3.2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2547a31fbd3b1610a8859a16edee2a141f7781691cb98a2c6fd54870c5f7541", size = 2995546, upload-time = "2026-06-30T07:42:23.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/f4/bc5830abeb57a7c7498cd9a0f2df953fd9fc7f33e3f5352c9824802b83bb/cython-3.2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab1fe11ebd61e497a848622cdd157a4324ac06e7935b1219715408844e15dd13", size = 3179546, upload-time = "2026-06-30T07:42:25.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/38/a70e879ea52debac11d2810e066a5a2cb16e71229edae303f024506bc142/cython-3.2.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bce1f079734753649f8a3d3a95832297207feb120d0ef4fd5db4c813cc6c04", size = 3351714, upload-time = "2026-06-30T07:42:27.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/f1/f071c5e7050a7924ffad9822558c74d489afe4764f7486cb68555a509219/cython-3.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:8297efe129e6421c34ddbeb09ed5627ef6c0fc4868bf7f9bdf6c147f595ccaed", size = 2774196, upload-time = "2026-06-30T07:42:29.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/a2/0f2eaa5076bcaef52567471a54ce02ffd70007bf8688cd054f7aab9bc3b8/cython-3.2.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:127bc4039be48c6eebe7f1d68c33d23eb3a0c5ae95e1d730bdd048837751438b", size = 2892527, upload-time = "2026-06-30T07:42:55.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/08/b5488aef44662e48ac09b42d4cb398207f591c770797036fb1d6fbeb7a52/cython-3.2.8-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e480ae9f195cd29e5ce334e3d434c83dbac0783c0cc88f2407e31ba997724192", size = 3220335, upload-time = "2026-06-30T07:42:57.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/96/d04a3621045e9fe9c7c5e406a688ee3d6e04a65f545ea7c622ead4b4afd8/cython-3.2.8-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3142407b9d63f233c766e17000e2aac782411bf0409b9adc97cb7c320aecd199", size = 2876481, upload-time = "2026-06-30T07:42:59.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/9a/daa259b638c5eabb8a8c36f203b85b01b5362101ff8fc4ec6ad592d34bb3/cython-3.2.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41c118fd91d320cd72af26e29232ff3f1a0a170c47d477c9a176d766067a4718", size = 2999974, upload-time = "2026-06-30T07:43:01.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/de/71cc5b4be5ee4d34c3302b6e7272189106a4072e9890d284d05239d2b645/cython-3.2.8-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6335c6e8737a39734e20d25f89f425bf3274c104fc7efc05aacc7ed1a4858c9d", size = 2897932, upload-time = "2026-06-30T07:43:03.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/0c/da68b9d3056e90b2060970b50d575cd7fcd1c778e8f23cc467f346e1d471/cython-3.2.8-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1034facc082cb882e1e5beb61e136ae8e282df2eafa11e9771e9b0a15c860801", size = 3235980, upload-time = "2026-06-30T07:43:05.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0b/d88bc50e66fd1f1160dd2677d9af18273a2fb2f102c086d21f64a5a9c78b/cython-3.2.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8896ff6b133f346ebcf22aa23706c4031e8d9d5ae184433dfc67dc1053318b69", size = 3118504, upload-time = "2026-06-30T07:43:07.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/de/511c4364808b3b4036d051a0301b0b142a3ddf8a319bfdbbd474fcfdc879/cython-3.2.8-cp39-abi3-win32.whl", hash = "sha256:3fd6464433d925cba66ae31bf5780c8a469a06da1d109180cffb39ee3c88ae20", size = 2435866, upload-time = "2026-06-30T07:43:09.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/4f/911b2b2a0a02be15829ccbf0c906029a318efbf53d9f8e021e438261c206/cython-3.2.8-cp39-abi3-win_arm64.whl", hash = "sha256:4e9447d9b652396a285cdfbd4f9f0721842c63c6df281720e87dcc4b9ea65af5", size = 2457829, upload-time = "2026-06-30T07:43:11.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/19/31aa63ab719b2e1eea5f200a8a54e2591dc1966a6b75e3de30ef8be9bc2c/cython-3.2.8-py3-none-any.whl", hash = "sha256:f635e113677666de13a2ec2979e9b1d5b90617cdfd1a691d3559be81e2dd6cb9", size = 1258688, upload-time = "2026-06-30T07:41:55.624Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deepmerge"
|
||||
version = "2.0"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -381,7 +378,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "eigen"
|
||||
version = "3.4.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen#f855b17636a376d8399f3df03629ff3e535f6b22" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen#bc4ff17caca5789acc9f15cee1fa3e79cbc523c6" }
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
@@ -395,7 +392,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "ffmpeg"
|
||||
version = "7.1.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg#f5153626027621f8ac0cdec056d254d3a1ef6acd" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg#c2b28a06b8bc6ae009a2cbc80556b5e3e0ba5846" }
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
@@ -442,12 +439,12 @@ wheels = [
|
||||
[[package]]
|
||||
name = "gcc-arm-none-eabi"
|
||||
version = "13.2.1"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#15791161bd7a752ef821ad6d999a364b13d6e9ca" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#63be8b7c4657f751046bff290d811f3dd7eeef9e" }
|
||||
|
||||
[[package]]
|
||||
name = "git-lfs"
|
||||
version = "3.6.1"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs#035fb711541a6d8609bb57b26c2f31813c743eb3" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs#1c0a3b36a988b1dad460397968e70d2034d55be0" }
|
||||
|
||||
[[package]]
|
||||
name = "google-crc32c"
|
||||
@@ -477,11 +474,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.17"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -496,7 +493,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "imgui"
|
||||
version = "1.92.7"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui#af4a30c3930c5e19cf8cea128e342686ba283624" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui#7acc08c04b4d681da414a822e6840e6c2166076d" }
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -549,7 +546,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "json11"
|
||||
version = "20170411.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=json11&rev=release-json11#0443a506c32eadfec2071a7c163c38c6c7b81d66" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=json11&rev=release-json11#1074970fdcc351f94cf49adbb7fba6cdbd3d6e41" }
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
@@ -581,12 +578,12 @@ wheels = [
|
||||
[[package]]
|
||||
name = "libjpeg"
|
||||
version = "3.1.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg#a615ec26d971cc4cca9a1a426e2c996760fe3c7f" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg#9f7bfba1702811f320f76c17040decff9677f361" }
|
||||
|
||||
[[package]]
|
||||
name = "libusb"
|
||||
version = "1.0.29"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb#23132cff0d629bf69590e23b6a682950433aaec8" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb#87c09087af4affd0bcf894cfb4e1f265ea520a46" }
|
||||
|
||||
[[package]]
|
||||
name = "libusb1"
|
||||
@@ -602,7 +599,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "libyuv"
|
||||
version = "1922.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv#70295e7a22d89f9c5bdb61dd93c30f0cbb55d815" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv#23d87efdeaed283a10007be31a3edff2ab3aa5d2" }
|
||||
|
||||
[[package]]
|
||||
name = "markdown"
|
||||
@@ -634,7 +631,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.9"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "contourpy" },
|
||||
@@ -647,15 +644,15 @@ dependencies = [
|
||||
{ name = "pyparsing" },
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -707,43 +704,44 @@ wheels = [
|
||||
[[package]]
|
||||
name = "ncurses"
|
||||
version = "6.5"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses#eae89aae0266b3fbae7b808a3c0c19a97a82a81c" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses#ede98fb00d9587450eb16e7f11b2c06c3085e90c" }
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
version = "2.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.13.0.92"
|
||||
version = "5.0.0.93"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/99/76b7c80252aa83c1af16393454aafd125a0287101afe8deb0a6821af0e30/opencv_python_headless-5.0.0.93.tar.gz", hash = "sha256:b82f9831daab90b725c7c1ee1b36cb5732c367096ac76d119e64e14eb70d5f3c", size = 81817738, upload-time = "2026-07-02T07:01:06.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/7c/8c8097891c509d98cd128493835c95631c80be6a8f37ed9d25716c2e16f1/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:030ca5e0837a2963ab36ef896baa9767eb8d2b83353fb28af5a521e40dd8756f", size = 48322581, upload-time = "2026-07-02T05:50:34.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/8c/eab2ad388c3cbab2a350c10c2ef19ce6bd099240afc31789032c996bab52/opencv_python_headless-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1e55af3abfb462eeeabe5c775f12bdb36216d8a93a3583d69e6bd6e1d6ba7d00", size = 34782894, upload-time = "2026-07-02T05:51:39.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/78/afca939f40ffe2b2380bfa86f812b2f7d4acc5a27b27dc41b49cad7ce7b4/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10818d91510e05c04568ae12b5cd120779c70c01bf897b001a6221fe430df80f", size = 36521085, upload-time = "2026-07-02T06:55:24.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/97/8170e9819764c47e436c130d3ff6cfb73b58f923eae9d3a03d8982b04aec/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09a872a157c1376ab922a69bbf22f9a95bcc7b658a9d8b436a60212b02b2eeb4", size = 56563598, upload-time = "2026-07-02T06:55:47.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/98/1a28a7101e31801042b3098871a74b76c61581d328ef40774ff4edb53a56/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:840bd717c21e5c11cadadc022a823315ea417f961213d06b4df010e019eb16f4", size = 39648433, upload-time = "2026-07-02T06:56:04.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/21/f6ef335f6e65724aa78b8d792b48d40a48c381715f1e62f5a5049e09d07e/opencv_python_headless-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ed709fdf9aa0bd1f2ed8549e71d19449b03a675bb581eb292285f6861953be37", size = 61204038, upload-time = "2026-07-02T06:56:41.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/8f/b8756467ea991449a293797f6b3fa80fcfdd29598a0a60d1cd5715b96e61/opencv_python_headless-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:c6bcd96b185975ea240d22cfdb15a1f6d080cc95264cfbe2621f21bb144d89b9", size = 35411237, upload-time = "2026-07-02T05:50:12.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/88/763b967f7efd7226b82c9fae16d560cba049b1f0c036647e65c610fd636e/opencv_python_headless-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:829717b6a95554f273e49e357cee3b3a2a26b6f4842fbc1bed2b45bdd8f87e0e", size = 43825962, upload-time = "2026-07-02T05:50:09.627Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -950,21 +948,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
version = "12.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1132,28 +1128,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "10.21.3"
|
||||
version = "11.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyopenssl"
|
||||
version = "26.2.0"
|
||||
version = "26.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1176,7 +1172,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -1185,9 +1181,9 @@ dependencies = [
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1301,8 +1297,8 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "raylib"
|
||||
version = "6.0.0.0"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=raylib&rev=release-raylib#dbf05cd774c2f74a70a0ee9ebb80015ed70ce718" }
|
||||
version = "6.0.0.1"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=raylib&rev=release-raylib#85d64783fb2c3553122db1568640b734c64ad190" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
@@ -1333,27 +1329,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.15"
|
||||
version = "0.15.21"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1367,15 +1363,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.61.1"
|
||||
version = "2.65.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/1f/ed17a390348156ca99fe622b97cd7d2f1969b5f49df89084b0f28e7953e9/sentry_sdk-2.65.0.tar.gz", hash = "sha256:c94dc945d54bad49d4f20448b1e6b217ca2f92f46d05c3e83d41764af685c3d1", size = 932133, upload-time = "2026-07-13T11:33:19.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/3b/326ad4c03b5da89b5124c8890af66e8119c4d2e10abc0619e0d67d9f7c7f/sentry_sdk-2.65.0-py3-none-any.whl", hash = "sha256:3595169677a808e4d0e1ea6ffb89443459549c7a98392ed71c77c847182ab6bf", size = 503869, upload-time = "2026-07-13T11:33:17.71Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1398,11 +1394,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "82.0.1"
|
||||
version = "83.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1477,48 +1473,48 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.3"
|
||||
version = "4.68.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ae/5f/57ff8b434839e70dab45601284ea413e947a63799891b7553e5960a793a8/tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520", size = 792418, upload-time = "2026-07-07T09:58:18.369Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.41"
|
||||
version = "0.0.59"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/8b/a64ba465cbc5d1b83c561a498ee5e7729b810606220277cafa93983e6ce1/ty-0.0.41.tar.gz", hash = "sha256:1e8b55bf4729634b2db64a7d9541cd880087cd681e87efc36e6a056cf05fb648", size = 5765398, upload-time = "2026-06-01T11:49:36.815Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/b0/84ae7b3bf6e3e9f57eb9635eeff5a80b36e57aa089f40be0fb5c384fa176/ty-0.0.59.tar.gz", hash = "sha256:53e53ffeed78ad59cd237fa8ea1316d2b94e13efdea9a945698acab549e005aa", size = 6145435, upload-time = "2026-07-12T20:22:02.781Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/22/b032e4d0f35a436f60eabaa19c70dbed6f5095eb7e30c53ddff918aafcff/ty-0.0.41-py3-none-linux_armv6l.whl", hash = "sha256:5f61c5c06616129ce31dd74141e18be69f5e4204a9a0f4505688213353475b30", size = 11541803, upload-time = "2026-06-01T11:49:33.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/f8/78f073febd728f19f0fcbe2e320855ed337b871ed775e0abe5c7b25bbc6a/ty-0.0.41-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25d6b4fbf198ae7523b8e4845ec3c7e1b1cd5efa1712febcad3856e70e66632b", size = 11275600, upload-time = "2026-06-01T11:49:59.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c6/b1b46684514e7372960348c723657f56db56799c36542571b49c77c18c87/ty-0.0.41-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3509788ed3753669cdd28d289f281b834ceee91b4de648fcf0ef60677a75b030", size = 10695726, upload-time = "2026-06-01T11:50:05.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/73/badf085590648a14bfe06571799becefaa2b409c5838c39b198b790c8f58/ty-0.0.41-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c04f1424c872f1d8d28efbd69fb52728fcc6c37aa2d11675bf16f76d33ae14", size = 11196461, upload-time = "2026-06-01T11:50:07.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/8f/399ee615282f80fd912b5ce4b3fe31206a6d40283e0700046aebbbb893f2/ty-0.0.41-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e52bfce12f3723c376d690625f5c462fe318c46dbe53b16d36c266388ee04c", size = 11310318, upload-time = "2026-06-01T11:49:50.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/43/204e8fe9f4f8932dd8e42a23e3aa8a365a93ecc601f67172da1c4cea50c0/ty-0.0.41-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4f1d606602dc0eb895a0943af737d638311c7afd48b414897fd42a05e876a76", size = 11785266, upload-time = "2026-06-01T11:49:48.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/85/9098e96e40324ebd3797c3cad20ea0d855b56219806605e10b6455cf71b3/ty-0.0.41-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8f3c41f0b6b0525bcc8d76a9a5921cccda11886ae11f73e2d51e20e185bafb3", size = 12328093, upload-time = "2026-06-01T11:49:52.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/41/b6a4d29591e86a27ab1364ddc70d6cee9a78cff1ede5466f6862f0384a68/ty-0.0.41-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff8c4468933961e656664c3ef190800e3d50582c248aed41bb8a1578d64864a", size = 11979937, upload-time = "2026-06-01T11:49:29.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/b2/6772c2c24fe412ebd0d57166d36afc176948857f7e90f368d321c561a12e/ty-0.0.41-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83077a5c56770b1dab5b086a908a4e77740fb8f29d862ed05bd4c854549603d", size = 11859185, upload-time = "2026-06-01T11:49:46.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/59/e4a1e0feef9d74308621c3a8b1558db97fa1c022948341d0eed1bdfc59a1/ty-0.0.41-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8067fae5532ad3c48295c7ddbd1657ea3ee7392810c57810c9c98fe83d43d57f", size = 12036836, upload-time = "2026-06-01T11:49:39.182Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/67/38f5312c74d51a510fb46c55b49880af6ca186cb87618938c67e808a45a5/ty-0.0.41-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:73a245242380bac3fbf3005d085c153923a45de8918001fad475af54c6fd50e4", size = 11179559, upload-time = "2026-06-01T11:50:01.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/55/f8cb36874dc1b8b26c6ae066f8ebec392197caa22becdc55bc7e53d251a5/ty-0.0.41-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4eb25f5dc398aa0a5fbd90de62d6a0fd3713354e566ba93cc53670fe84067ee1", size = 11347231, upload-time = "2026-06-01T11:49:55.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/2d4369d7e8240b2dea1d7a7985709c38c6ccc57bbc8dbbdae3db5ee8d71a/ty-0.0.41-py3-none-musllinux_1_2_i686.whl", hash = "sha256:092c3a1ef9e3a189f71428092e736080ef42bf5c14412aa61bee8d644261797d", size = 11446240, upload-time = "2026-06-01T11:49:31.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/b0/36f09bfb568bc14f8496f2ace503c63c9152b4b65da4518cd3be0aa85312/ty-0.0.41-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53edeb6e2ac430fe789454471f0e22a2a71581100818271150bd8f89fa468cc7", size = 11947795, upload-time = "2026-06-01T11:49:41.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/33/ffd38f5e3ecb25e1b7aac314d902ceaa93be16f4874dc236f9da79509468/ty-0.0.41-py3-none-win32.whl", hash = "sha256:f66bdf3afb71f11d412a4c704fa9691e0209cbc8268a19489c8960c7b74eac8b", size = 10776684, upload-time = "2026-06-01T11:50:03.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/35/4c3e821906cfb05a88c232f5c1202be7ed336e05f4cc796213c809ab6177/ty-0.0.41-py3-none-win_amd64.whl", hash = "sha256:c18386e5c3a0c3d118ce58ee0d8a35f8d18d6b1020ca8ec690f6064e6230a79b", size = 11860965, upload-time = "2026-06-01T11:49:57.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/20/7c587e4c3c6da1757555831cac308c4add7a958f8b1c7f76cb1dd55ceabf/ty-0.0.41-py3-none-win_arm64.whl", hash = "sha256:289eb66ee5c3554d59b0ea69885ddc5fc8051f3228dd9fee2501799db4a07117", size = 11206414, upload-time = "2026-06-01T11:49:44.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e8/650b42fbef4d48e6ca682b0b6e9b68fa8fcf55cbb0a6892ab89990018b6f/ty-0.0.59-py3-none-linux_armv6l.whl", hash = "sha256:f8fb08a767ef8f11ea3c537b9d77860726cc2bc39e6f77ad13c02d5b289f20a7", size = 11700328, upload-time = "2026-07-12T20:21:26.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ac/0ca3a89d5f59ae5f308e5e83428cac5f9143200767743e052fba90b4b81e/ty-0.0.59-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c7f4d5630836c8a0ba13dd4ac7bdae080a7d6ebe965b817ff642dc961bcf2a53", size = 11494310, upload-time = "2026-07-12T20:21:28.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/f8/5076de6001cefbccd8e6dc8472262697e43308ff66b0e87c72abba136357/ty-0.0.59-py3-none-macosx_11_0_arm64.whl", hash = "sha256:872f6fb02c6db5553c4d5fb283b3d50f0985fb9a29a910e4fda4793a775c1926", size = 11026797, upload-time = "2026-07-12T20:21:30.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/0f/fca28481b6a138e2b798ad9fdc98a095475f9104948ba242fce4b477782b/ty-0.0.59-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af8eefbfe806337770eec12c0c819c5f1b8f5b85f8369cb1cc9fa25234a2208", size = 11475304, upload-time = "2026-07-12T20:21:33.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/4b/1fed8b81b389ef4bbc0400f19e05fc16496b162577779dc0e5fc65ac216c/ty-0.0.59-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0acf8b76a1c9a7ddef460b42475f6c76193164426ab080783af1c3175b4b999b", size = 11533131, upload-time = "2026-07-12T20:21:35.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/fc/04eec35e05a10e0fea1c6503a290ccc3935efda9c845aff64e83282c1af7/ty-0.0.59-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:043c2e00eb1d7475f928af7dedd71f69b64e69bfca55e36f4c968479e1373fc4", size = 12205932, upload-time = "2026-07-12T20:21:37.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/dd/a61de859659fa11b55917ad38340a8f2c61f5ae17d1874929f29084c6990/ty-0.0.59-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0d688d857441df57f48fca66c029d85cf737c510e7be1d01144cdad1e58d968", size = 12758406, upload-time = "2026-07-12T20:21:39.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/e8/fa66f05997eab8ca75fc4f17320140e25467849e0cc75597f898cc22099c/ty-0.0.59-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a96c9f88394a3b42c737e2125b2330543f0d90a43b49761f377d96f8c3ee0d62", size = 12288176, upload-time = "2026-07-12T20:21:41.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/68/0fca59963bd5123f42d5f7da50667e7a52e8e9615e3a16d8c2c0d3b2d143/ty-0.0.59-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08dbcb268edcafcb152e59475b5b495ce28d0b340a395c09943557678f4d5a6", size = 12028471, upload-time = "2026-07-12T20:21:43.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5b/cd7dabbbab392578f11179919da5c25d8c3322e5388a688f539ea0539603/ty-0.0.59-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8812764b9a40fdc98df1272826e73a298ef56b06681135e643bcf90aad1896f7", size = 12297646, upload-time = "2026-07-12T20:21:45.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/37/2e9c94f0b383d8cbe1a35517ab470b7810bc9d7501603ab532bcd5be5e90/ty-0.0.59-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fd53b8581641d8dad7bfac6d5ea589e91a883d6837e0b9a286fdae30722b7c69", size = 11432519, upload-time = "2026-07-12T20:21:47.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/0a/af93e9785200f11ac416cc20235fc2464c9bd978e791190684ea0e458795/ty-0.0.59-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:86da5872124a41877d95058bc17d33ddcff034b587eb5f1e2917ab88ba227dac", size = 11554993, upload-time = "2026-07-12T20:21:49.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/dd/651bf87e20d00376c81b19124756491cffaf20eb8bec05a8794e5a8cf641/ty-0.0.59-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a233eef5f2fd4d894881e4a0aec83c9f172bfae1d787d6596ee1939fcc7723e", size = 11818230, upload-time = "2026-07-12T20:21:51.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/50/c947c4155fea751d135b19affdf734bbce72a94e446b866cf0c62f8bed69/ty-0.0.59-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ff678c18b5f1e3128b75a35e50dee7908dea55155baa31cd790619d5014cbf5", size = 12135194, upload-time = "2026-07-12T20:21:53.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/13/e5feb138888de1e95037c843571bbbd4ac21bf0a190507468098599a321f/ty-0.0.59-py3-none-win32.whl", hash = "sha256:cf8abb4b8095c5fe39102b8127f5886db308c8d4600909ddbc905512ce9c8163", size = 11179249, upload-time = "2026-07-12T20:21:55.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/dd/52914dcbeeba92c207de40ef7109a58dcb5527aeb21c8f8feb7402aa9e29/ty-0.0.59-py3-none-win_amd64.whl", hash = "sha256:1dde20a82243d24407869e5a608c2f15efddd5cefc662aef461a5af84bfb3f8b", size = 12251079, upload-time = "2026-07-12T20:21:58.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/8f/ac36fde77e223297454c1e0aeb8888c169eaacf3163bb609e3af942c88cb/ty-0.0.59-py3-none-win_arm64.whl", hash = "sha256:987043ee9e021f49493d9135891ac69c1affeee0d4ad4480c5fa4d9c975fc91b", size = 11650921, upload-time = "2026-07-12T20:22:00.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1560,7 +1556,7 @@ wheels = [
|
||||
[[package]]
|
||||
name = "xvfb"
|
||||
version = "1.20.11.post1"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=xvfb&rev=release-xvfb#f0bb0b0d9154ea7858f3c0333b607ba7d356b571" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=xvfb&rev=release-xvfb#5575cd83f2f369bf02a878d682c24bdb69feffc9" }
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
@@ -1595,7 +1591,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "zensical"
|
||||
version = "0.0.43"
|
||||
version = "0.0.50"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -1607,26 +1603,26 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tomli" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/85/ec45162e7824a8f879d887ef0774ee65926bf7d1064e2eebccc7eaee3378/zensical-0.0.43.tar.gz", hash = "sha256:dc2d3804ff562795c1024130e0c3ce79736467930729dda314f096d0e35b98c8", size = 3932396, upload-time = "2026-05-19T09:44:07.418Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/79/f7959b13c766a831f1248779bb718555f70f40c5b8e68db7de1de9936662/zensical-0.0.50.tar.gz", hash = "sha256:7040e52ebe5e6a275e4edeb351bf2bc314d007f3fb5750f178a38d840723e69c", size = 3979246, upload-time = "2026-07-09T13:52:11.908Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c2/55e0709607ae41c266987c3b91a1a9702b37fbbef0d07eddfe5e25c2d823/zensical-0.0.43-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:17c335362b6bac3a50178181694a964f6d9f0c516fc532129ba5a0a5c4103fb6", size = 12706531, upload-time = "2026-05-19T09:43:32.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/64/ce8627bc5ea30556162b29b041fe97d6a6aef2a87b51f12def628e4fa608/zensical-0.0.43-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8fe97f185194215f6193af45a17d2b30ebd72c8113e3650f2d7d6767b9c2206", size = 12563012, upload-time = "2026-05-19T09:43:35.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d1/533bc9454f0e06b3d9d8bd2e7ac405308c3d4dee6572acab98f0ed6d1c07/zensical-0.0.43-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c4c85978c765b3e7f347e8102dfe1373d4bbe4229d7008b6bdbf352f1fbcd7f", size = 12947599, upload-time = "2026-05-19T09:43:38.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a0/94f47d6fb592997be7ab9526938c929f0199adf2637c3c2b2b9b2101b28e/zensical-0.0.43-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90d7c06ffd07b2bdf78bef041d541baba8a3ea51fd2dd84dbdbc5b0229076524", size = 12904911, upload-time = "2026-05-19T09:43:42.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/fb/1db3ad9a86ff772f74a8bc60ad5b447aa02a158e70f94adacf50bdd5c40f/zensical-0.0.43-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60022f4a6b95e46ec0023f51052fcd491743b3ebd08c0066b22a5cf1e741fecd", size = 13269386, upload-time = "2026-05-19T09:43:45.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/ee/b24fd0f94885519d851c35615b086d069a1077b0198021a56755395a4633/zensical-0.0.43-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e278eb948a0b7545d50609d713c7c27e366dade4523ff73a311a5d5f136518a", size = 12999364, upload-time = "2026-05-19T09:43:48.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/78/401ccd7afd9d2690f81b5319b7f1eed05108154ce20e4207053914518c1c/zensical-0.0.43-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b85e5ab99fbda13823e67c43a4be6e5ebda6600602969c6575e143f20ac203fd", size = 13124392, upload-time = "2026-05-19T09:43:50.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/b3/9af6eba5826b0ef143fc8308bd1e219e221441e307a958e39f824ba9ab53/zensical-0.0.43-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:751385accc92cccfd4560dabed7c423870686ef6ede244a67e5c96286af25e8f", size = 13177538, upload-time = "2026-05-19T09:43:53.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/6b/cd090bd6659d32692487206469988ee84d41aa6de4cdf9e380f847da90e2/zensical-0.0.43-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:dd3ff5bfa6e65cf3d2550dc639c3da2a3bfa11087b83d57e06623c4c1607d583", size = 13327086, upload-time = "2026-05-19T09:43:56.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/5b/ac2555354b5a53cb9c2c942811905c47be0b9f5603d3c1328ee8564333eb/zensical-0.0.43-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85055a115b12f49c6ab194dcf04f966fc06b690ed6a8ddddd819929fc5f340e6", size = 13284645, upload-time = "2026-05-19T09:43:59.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c6/1688ec6e5be15e3ab367d7804753291bfbdff3109b06e20c19ce30a7129c/zensical-0.0.43-cp310-abi3-win32.whl", hash = "sha256:8a75ddd4bb3cd3c4a8e71d2ebae44c5611fd636c1d355c6124dd96e2f9c52838", size = 12256740, upload-time = "2026-05-19T09:44:02.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/a8/d967e70eac810a7e9eb8c5150d6d02848a1f42260f42977c71debed3cb02/zensical-0.0.43-cp310-abi3-win_amd64.whl", hash = "sha256:03a9d1744a6394ad66c355d6f1de04cfd92efa525b0b94bf6dbf6971c5cd2c6b", size = 12496166, upload-time = "2026-05-19T09:44:04.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d2/e126d56642a5fd437eeb5a067b7c0d1f766c08e22d6a708bb923986f1d44/zensical-0.0.50-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:26dda9dbacab84db2743726f83202798e1893fbfddecb6a845812d7a331043ad", size = 12817706, upload-time = "2026-07-09T13:51:29.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9b/e1a22fb4ea8fc3e8377d7b394ee06cf0349e7f9a64ecb4c9873dee79b102/zensical-0.0.50-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:376887def6470c57509b48519870f74e2b987e30509cf5c819fcf372771f9403", size = 12691061, upload-time = "2026-07-09T13:51:33.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/f5/16c087635680efb39e1d7b7aa3c2cca548305aa29ad51dee04f37b32b65f/zensical-0.0.50-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bbf3034a2cb1a9d2a72a5ee7047688717c93da0d8a90d25dd871db8d5f33a6d", size = 13129505, upload-time = "2026-07-09T13:51:36.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/50d02402e53aa01a5c5c116a3522c7bb18d6ae4da964d8b236173b6fa086/zensical-0.0.50-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26fe4167663d544ca385ef420a49d3b738c7606a79d7a62a6bedcbf74cd383e7", size = 13055300, upload-time = "2026-07-09T13:51:39.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/0f/45f4f3e56a6483ea9f88478b20d6b434718e7c46579239589202e5135ec1/zensical-0.0.50-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8813d1f8895b19520d4a982d6bfcfd06372d73d96e6a63f0d2cf8f3b6036d5a1", size = 13445447, upload-time = "2026-07-09T13:51:43.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9b/1f5065c664afde6a63e0ada8c14eae75f9e6960df597a5ca28c07340f938/zensical-0.0.50-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a64f957f94f7d8e8847a7f1e8205509ba4b188c93c157c5e7be49bcb8556a127", size = 13098775, upload-time = "2026-07-09T13:51:46.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/65/1e42e18d8f9a0cb9fcc8b871852810c7fe196237b79aaa831e863325091e/zensical-0.0.50-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:781d333bcb42c6a6b92360e05eadc944e2674794e1a95537971de1bf64cb9f89", size = 13305922, upload-time = "2026-07-09T13:51:51.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/36/9cace194ba13aef3bdde8ca2d327f3a05cdf5c456e94f6d896852e2cbd38/zensical-0.0.50-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5d0f86eec76c23efb04566cd444bf025845ac8a02e75c8a6b4af13a39a2a9955", size = 13325488, upload-time = "2026-07-09T13:51:54.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/80/2891a9316e486794dae43151561b49f3313499cfb2c95c7886abad7bcf2d/zensical-0.0.50-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:392a7299a3c4d1ac7ff288bbed181d77a9fd23e6614180476e01630aaee0d67e", size = 13496866, upload-time = "2026-07-09T13:51:58.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/09/fbd3af58081a0d399fa913ef1f2fd6a007f2f06f7026c8d6a6096e34b996/zensical-0.0.50-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cac3448d8c8a76dfb43e55908b4ca9ad17596aee89a9287a184c1a0ba7dce2a2", size = 13437565, upload-time = "2026-07-09T13:52:01.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/79/709311ff202326260ff223fb6e9ddefa3100474c428e3c70a04036cefe0e/zensical-0.0.50-cp310-abi3-win32.whl", hash = "sha256:32b244f96a930f68e049b2e03d50790c120ef701b6277ddd854905a33040c75f", size = 12371966, upload-time = "2026-07-09T13:52:05.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e1/4babb30544d7465c95a6a93abc0680b1c51752411a225c2b7f2cb44e80c7/zensical-0.0.50-cp310-abi3-win_amd64.whl", hash = "sha256:2f054769bf96ae46353de3eca2463ad4db6df1da9fde515c6d7fc54c3608e615", size = 12604832, upload-time = "2026-07-09T13:52:09.013Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeromq"
|
||||
version = "4.3.5"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq#40b29e8e88a69802e8afc4f71f5c955bc2d5c74c" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq#20854ca20ac9888ceb7e3a76601ea5e3fcb9dee4" }
|
||||
|
||||
[[package]]
|
||||
name = "zstandard"
|
||||
@@ -1656,4 +1652,4 @@ wheels = [
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "1.5.6"
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd#13015596466eb68825f109ad0d5aefa6330cc495" }
|
||||
source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd#6a67fb99ec24a41d11680c21fae3c1f9dbd3f8fd" }
|
||||
|
||||
Reference in New Issue
Block a user