mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-29 12:13:47 +08:00
Compare commits
120 Commits
Keyboard-Test
...
cookie
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c8999c9ec | |||
| 44c90310b4 | |||
| b242b61f8a | |||
| 18907aa122 | |||
| 4d7d73379c | |||
| e0c8e95c9d | |||
| a31e43c771 | |||
| f0759a3271 | |||
| 97329e46ae | |||
| d76f756f42 | |||
| 71a418d166 | |||
| de024fd4a7 | |||
| 7c90c0669a | |||
| 560ed80123 | |||
| 12220ec82d | |||
| 3715fe85aa | |||
| ba6e5f125d | |||
| 1459d3519d | |||
| c9cfe2c727 | |||
| bc979ea6aa | |||
| 79472cdf83 | |||
| adf6f28ebf | |||
| a7dfd36c00 | |||
| 6c7f3751e7 | |||
| c179a3ccb7 | |||
| 13efc421c4 | |||
| 10db1edc7f | |||
| 039b85f355 | |||
| 0b41b42f7b | |||
| a46ff01cab | |||
| 1f9efd9311 | |||
| 7f8dbf24e7 | |||
| 5e4b88201e | |||
| 1252188b4b | |||
| 15d3a166f7 | |||
| f51c2aeced | |||
| 3edb3243f6 | |||
| c693bc1247 | |||
| 5d3ab260e1 | |||
| ea64c4c0ae | |||
| 84bce8ae02 | |||
| 3c5974930a | |||
| be854df32d | |||
| adbf68f771 | |||
| f62177a827 | |||
| bb40d161e8 | |||
| 84b1f363e4 | |||
| a5348b8679 | |||
| 63c9a85c6a | |||
| adf9ec5360 | |||
| 883d1232d3 | |||
| b58fddb83e | |||
| ce1491df9c | |||
| ea01a53711 | |||
| 85cdb2ed9a | |||
| 368947c88c | |||
| 3f1f7ad89c | |||
| a8cfa2e2fe | |||
| c8eed43538 | |||
| 29a2f576f5 | |||
| 31ec0096e4 | |||
| 8728c7dde3 | |||
| e9a37d99c3 | |||
| 67742699cc | |||
| de975d5af9 | |||
| c3143f3833 | |||
| 1c2f9e6190 | |||
| 654338f9c7 | |||
| dfd7a8c8d7 | |||
| bb8a5bd476 | |||
| e4359e9acb | |||
| 13b8a67ae2 | |||
| 89d9fdca82 | |||
| a478b64ff3 | |||
| b3c2daf9e5 | |||
| 792a9b715c | |||
| 631d6d9ef4 | |||
| 6249211745 | |||
| 2213f8f8a4 | |||
| 4e85568370 | |||
| 88a4f2baf1 | |||
| 4bfc28dec0 | |||
| be2818a131 | |||
| 1646fd94b8 | |||
| 3fbd928b98 | |||
| d2125aafd4 | |||
| b9c3b1219a | |||
| 99983d39c3 | |||
| 31e46f929d | |||
| cecce82015 | |||
| a112e6e882 | |||
| c69c076acb | |||
| 6069c87b07 | |||
| 90ed6d739c | |||
| 95350ad854 | |||
| 9768109ec1 | |||
| 4fa4237e3f | |||
| 4624d8f936 | |||
| bcdeec3133 | |||
| 545f7c6f2a | |||
| 507f420927 | |||
| 752ef8696a | |||
| 9e4c2bcacf | |||
| 9d9e5aa02d | |||
| e2fd6f34e9 | |||
| a3c638697f | |||
| f287d487e5 | |||
| 7cabab69a1 | |||
| 716ad288bb | |||
| 1c135f7ff2 | |||
| 1504e10380 | |||
| 65008d281f | |||
| 2e8586fab5 | |||
| 6c5be6ddab | |||
| 7a324fc377 | |||
| f4dea7977b | |||
| e9255d1e9c | |||
| 350dc6a50f | |||
| 3206784dd8 | |||
| 0dd59d0404 |
Vendored
+1
-1
@@ -22,7 +22,7 @@ shopt -s huponexit # kill all child processes when the shell exits
|
|||||||
|
|
||||||
export CI=1
|
export CI=1
|
||||||
export PYTHONWARNINGS=error
|
export PYTHONWARNINGS=error
|
||||||
export LOGPRINT=debug
|
#export LOGPRINT=debug # this has gotten too spammy...
|
||||||
export TEST_DIR=${env.TEST_DIR}
|
export TEST_DIR=${env.TEST_DIR}
|
||||||
export SOURCE_DIR=${env.SOURCE_DIR}
|
export SOURCE_DIR=${env.SOURCE_DIR}
|
||||||
export GIT_BRANCH=${env.GIT_BRANCH}
|
export GIT_BRANCH=${env.GIT_BRANCH}
|
||||||
|
|||||||
+11
-1
@@ -1,5 +1,15 @@
|
|||||||
Version 0.10.3 (2025-12-10)
|
Version 0.10.4 (2026-02-17)
|
||||||
========================
|
========================
|
||||||
|
* Lexus LS 2018 support thanks to Hacheoy!
|
||||||
|
|
||||||
|
Version 0.10.3 (2025-12-17)
|
||||||
|
========================
|
||||||
|
* New driving model #36249
|
||||||
|
* New temporal policy architecture
|
||||||
|
* New on-policy training physics noise model
|
||||||
|
* New driver monitoring model #36409
|
||||||
|
* Trained on a new dataset, including comma four data
|
||||||
|
* Improved inter-process communication memory efficiency
|
||||||
|
|
||||||
Version 0.10.2 (2025-11-19)
|
Version 0.10.2 (2025-11-19)
|
||||||
========================
|
========================
|
||||||
|
|||||||
+6
-5
@@ -87,6 +87,7 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
|||||||
laneChange @50;
|
laneChange @50;
|
||||||
lowMemory @51;
|
lowMemory @51;
|
||||||
stockAeb @52;
|
stockAeb @52;
|
||||||
|
stockLkas @98;
|
||||||
ldw @53;
|
ldw @53;
|
||||||
carUnrecognized @54;
|
carUnrecognized @54;
|
||||||
invalidLkasSetting @55;
|
invalidLkasSetting @55;
|
||||||
@@ -2226,9 +2227,9 @@ struct DriverMonitoringState @0xb83cda094a1da284 {
|
|||||||
isActiveMode @16 :Bool;
|
isActiveMode @16 :Bool;
|
||||||
isRHD @4 :Bool;
|
isRHD @4 :Bool;
|
||||||
uncertainCount @19 :UInt32;
|
uncertainCount @19 :UInt32;
|
||||||
phoneProbOffset @20 :Float32;
|
|
||||||
phoneProbValidCount @21 :UInt32;
|
|
||||||
|
|
||||||
|
phoneProbOffsetDEPRECATED @20 :Float32;
|
||||||
|
phoneProbValidCountDEPRECATED @21 :UInt32;
|
||||||
isPreviewDEPRECATED @15 :Bool;
|
isPreviewDEPRECATED @15 :Bool;
|
||||||
rhdCheckedDEPRECATED @5 :Bool;
|
rhdCheckedDEPRECATED @5 :Bool;
|
||||||
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);
|
eventsDEPRECATED @0 :List(Car.OnroadEventDEPRECATED);
|
||||||
@@ -2524,13 +2525,10 @@ struct Event {
|
|||||||
controlsState @7 :ControlsState;
|
controlsState @7 :ControlsState;
|
||||||
selfdriveState @130 :SelfdriveState;
|
selfdriveState @130 :SelfdriveState;
|
||||||
gyroscope @99 :SensorEventData;
|
gyroscope @99 :SensorEventData;
|
||||||
gyroscope2 @100 :SensorEventData;
|
|
||||||
accelerometer @98 :SensorEventData;
|
accelerometer @98 :SensorEventData;
|
||||||
accelerometer2 @101 :SensorEventData;
|
|
||||||
magnetometer @95 :SensorEventData;
|
magnetometer @95 :SensorEventData;
|
||||||
lightSensor @96 :SensorEventData;
|
lightSensor @96 :SensorEventData;
|
||||||
temperatureSensor @97 :SensorEventData;
|
temperatureSensor @97 :SensorEventData;
|
||||||
temperatureSensor2 @123 :SensorEventData;
|
|
||||||
pandaStates @81 :List(PandaState);
|
pandaStates @81 :List(PandaState);
|
||||||
peripheralState @80 :PeripheralState;
|
peripheralState @80 :PeripheralState;
|
||||||
radarState @13 :RadarState;
|
radarState @13 :RadarState;
|
||||||
@@ -2693,5 +2691,8 @@ struct Event {
|
|||||||
liveLocationKalmanDEPRECATED @72 :LiveLocationKalman;
|
liveLocationKalmanDEPRECATED @72 :LiveLocationKalman;
|
||||||
liveTracksDEPRECATED @16 :List(LiveTracksDEPRECATED);
|
liveTracksDEPRECATED @16 :List(LiveTracksDEPRECATED);
|
||||||
onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED);
|
onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED);
|
||||||
|
gyroscope2DEPRECATED @100 :SensorEventData;
|
||||||
|
accelerometer2DEPRECATED @101 :SensorEventData;
|
||||||
|
temperatureSensor2DEPRECATED @123 :SensorEventData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# must be built with scons
|
# must be built with scons
|
||||||
from msgq.ipc_pyx import Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
|
from msgq import fake_event_handle, drain_sock_raw, MultiplePublishersError, IpcError, \
|
||||||
set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event
|
Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \
|
||||||
from msgq.ipc_pyx import MultiplePublishersError, IpcError
|
set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event
|
||||||
from msgq import fake_event_handle, pub_sock, sub_sock, drain_sock_raw
|
|
||||||
import msgq
|
import msgq
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import capnp
|
import capnp
|
||||||
import time
|
import time
|
||||||
@@ -13,11 +11,25 @@ from typing import Optional, List, Union, Dict
|
|||||||
|
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from cereal.services import SERVICE_LIST
|
from cereal.services import SERVICE_LIST
|
||||||
from openpilot.common.util import MovingAverage
|
from openpilot.common.utils import MovingAverage
|
||||||
|
|
||||||
NO_TRAVERSAL_LIMIT = 2**64-1
|
NO_TRAVERSAL_LIMIT = 2**64-1
|
||||||
|
|
||||||
|
|
||||||
|
def pub_sock(endpoint: str) -> PubSocket:
|
||||||
|
service = SERVICE_LIST.get(endpoint)
|
||||||
|
segment_size = service.queue_size if service else 0
|
||||||
|
return msgq.pub_sock(endpoint, segment_size)
|
||||||
|
|
||||||
|
|
||||||
|
def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1",
|
||||||
|
conflate: bool = False, timeout: Optional[int] = None) -> SubSocket:
|
||||||
|
service = SERVICE_LIST.get(endpoint)
|
||||||
|
segment_size = service.queue_size if service else 0
|
||||||
|
return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate,
|
||||||
|
timeout=timeout, segment_size=segment_size)
|
||||||
|
|
||||||
|
|
||||||
def reset_context():
|
def reset_context():
|
||||||
msgq.context = Context()
|
msgq.context = Context()
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ void zmq_to_msgq(const std::vector<std::string> &endpoints, const std::string &i
|
|||||||
for (auto endpoint : endpoints) {
|
for (auto endpoint : endpoints) {
|
||||||
auto pub_sock = new MSGQPubSocket();
|
auto pub_sock = new MSGQPubSocket();
|
||||||
auto sub_sock = new ZMQSubSocket();
|
auto sub_sock = new ZMQSubSocket();
|
||||||
pub_sock->connect(pub_context.get(), endpoint);
|
size_t queue_size = services.at(endpoint).queue_size;
|
||||||
|
pub_sock->connect(pub_context.get(), endpoint, true, queue_size);
|
||||||
sub_sock->connect(sub_context.get(), endpoint, ip, false);
|
sub_sock->connect(sub_context.get(), endpoint, ip, false);
|
||||||
|
|
||||||
poller->registerSocket(sub_sock);
|
poller->registerSocket(sub_sock);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "cereal/services.h"
|
||||||
#include "common/util.h"
|
#include "common/util.h"
|
||||||
|
|
||||||
extern ExitHandler do_exit;
|
extern ExitHandler do_exit;
|
||||||
@@ -108,7 +109,8 @@ void MsgqToZmq::zmqMonitorThread() {
|
|||||||
if (++pair.connected_clients == 1) {
|
if (++pair.connected_clients == 1) {
|
||||||
// Create new MSGQ subscriber socket and map to ZMQ publisher
|
// Create new MSGQ subscriber socket and map to ZMQ publisher
|
||||||
pair.sub_sock = std::make_unique<MSGQSubSocket>();
|
pair.sub_sock = std::make_unique<MSGQSubSocket>();
|
||||||
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1");
|
size_t queue_size = services.at(pair.endpoint).queue_size;
|
||||||
|
pair.sub_sock->connect(msgq_context.get(), pair.endpoint, "127.0.0.1", false, true, queue_size);
|
||||||
sub2pub[pair.sub_sock.get()] = pair.pub_sock.get();
|
sub2pub[pair.sub_sock.get()] = pair.pub_sock.get();
|
||||||
registerSockets();
|
registerSockets();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ SubMaster::SubMaster(const std::vector<const char *> &service_list, const std::v
|
|||||||
assert(services.count(std::string(name)) > 0);
|
assert(services.count(std::string(name)) > 0);
|
||||||
|
|
||||||
service serv = services.at(std::string(name));
|
service serv = services.at(std::string(name));
|
||||||
SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true);
|
SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true, true, serv.queue_size);
|
||||||
assert(socket != 0);
|
assert(socket != 0);
|
||||||
bool is_polled = inList(poll, name) || poll.empty();
|
bool is_polled = inList(poll, name) || poll.empty();
|
||||||
if (is_polled) poller_->registerSocket(socket);
|
if (is_polled) poller_->registerSocket(socket);
|
||||||
@@ -187,7 +187,8 @@ SubMaster::~SubMaster() {
|
|||||||
PubMaster::PubMaster(const std::vector<const char *> &service_list) {
|
PubMaster::PubMaster(const std::vector<const char *> &service_list) {
|
||||||
for (auto name : service_list) {
|
for (auto name : service_list) {
|
||||||
assert(services.count(name) > 0);
|
assert(services.count(name) > 0);
|
||||||
PubSocket *socket = PubSocket::create(message_context.context(), name);
|
service serv = services.at(std::string(name));
|
||||||
|
PubSocket *socket = PubSocket::create(message_context.context(), name, true, serv.queue_size);
|
||||||
assert(socket);
|
assert(socket);
|
||||||
sockets_[name] = socket;
|
sockets_[name] = socket;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-19
@@ -1,37 +1,44 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from enum import IntEnum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: this should be automatically determined using the capnp schema
|
||||||
|
class QueueSize(IntEnum):
|
||||||
|
BIG = 10 * 1024 * 1024 # 10MB - video frames, large AI outputs
|
||||||
|
MEDIUM = 2 * 1024 * 1024 # 2MB - high freq (CAN), livestream
|
||||||
|
SMALL = 250 * 1024 # 250KB - most services
|
||||||
|
|
||||||
|
|
||||||
class Service:
|
class Service:
|
||||||
def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None):
|
def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None,
|
||||||
|
queue_size: QueueSize = QueueSize.SMALL):
|
||||||
self.should_log = should_log
|
self.should_log = should_log
|
||||||
self.frequency = frequency
|
self.frequency = frequency
|
||||||
self.decimation = decimation
|
self.decimation = decimation
|
||||||
|
self.queue_size = queue_size
|
||||||
|
|
||||||
|
|
||||||
_services: dict[str, tuple] = {
|
_services: dict[str, tuple] = {
|
||||||
# service: (should_log, frequency, qlog decimation (optional))
|
# service: (should_log, frequency, qlog decimation (optional))
|
||||||
# note: the "EncodeIdx" packets will still be in the log
|
# note: the "EncodeIdx" packets will still be in the log
|
||||||
"gyroscope": (True, 104., 104),
|
"gyroscope": (True, 104., 104),
|
||||||
"gyroscope2": (True, 100., 100),
|
|
||||||
"accelerometer": (True, 104., 104),
|
"accelerometer": (True, 104., 104),
|
||||||
"accelerometer2": (True, 100., 100),
|
|
||||||
"magnetometer": (True, 25.),
|
"magnetometer": (True, 25.),
|
||||||
"lightSensor": (True, 100., 100),
|
"lightSensor": (True, 100., 100),
|
||||||
"temperatureSensor": (True, 2., 200),
|
"temperatureSensor": (True, 2., 200),
|
||||||
"temperatureSensor2": (True, 2., 200),
|
|
||||||
"gpsNMEA": (True, 9.),
|
"gpsNMEA": (True, 9.),
|
||||||
"deviceState": (True, 2., 1),
|
"deviceState": (True, 2., 1),
|
||||||
"touch": (True, 20., 1),
|
"touch": (True, 20., 1),
|
||||||
"can": (True, 100., 2053), # decimation gives ~3 msgs in a full segment
|
"can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment
|
||||||
"controlsState": (True, 100., 10),
|
"controlsState": (True, 100., 10, QueueSize.MEDIUM),
|
||||||
"selfdriveState": (True, 100., 10),
|
"selfdriveState": (True, 100., 10),
|
||||||
"pandaStates": (True, 10., 1),
|
"pandaStates": (True, 10., 1),
|
||||||
"peripheralState": (True, 2., 1),
|
"peripheralState": (True, 2., 1),
|
||||||
"radarState": (True, 20., 5),
|
"radarState": (True, 20., 5),
|
||||||
"roadEncodeIdx": (False, 20., 1),
|
"roadEncodeIdx": (False, 20., 1),
|
||||||
"liveTracks": (True, 20.),
|
"liveTracks": (True, 20.),
|
||||||
"sendcan": (True, 100., 139),
|
"sendcan": (True, 100., 139, QueueSize.MEDIUM),
|
||||||
"logMessage": (True, 0.),
|
"logMessage": (True, 0.),
|
||||||
"errorLogMessage": (True, 0., 1),
|
"errorLogMessage": (True, 0., 1),
|
||||||
"liveCalibration": (True, 4., 4),
|
"liveCalibration": (True, 4., 4),
|
||||||
@@ -43,7 +50,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carOutput": (True, 100., 10),
|
"carOutput": (True, 100., 10),
|
||||||
"longitudinalPlan": (True, 20., 10),
|
"longitudinalPlan": (True, 20., 10),
|
||||||
"driverAssistance": (True, 20., 20),
|
"driverAssistance": (True, 20., 20),
|
||||||
"procLog": (True, 0.5, 15),
|
"procLog": (True, 0.5, 15, QueueSize.BIG),
|
||||||
"gpsLocationExternal": (True, 10., 10),
|
"gpsLocationExternal": (True, 10., 10),
|
||||||
"gpsLocation": (True, 1., 1),
|
"gpsLocation": (True, 1., 1),
|
||||||
"ubloxGnss": (True, 10.),
|
"ubloxGnss": (True, 10.),
|
||||||
@@ -65,7 +72,7 @@ _services: dict[str, tuple] = {
|
|||||||
"wideRoadEncodeIdx": (False, 20., 1),
|
"wideRoadEncodeIdx": (False, 20., 1),
|
||||||
"wideRoadCameraState": (True, 20., 20),
|
"wideRoadCameraState": (True, 20., 20),
|
||||||
"drivingModelData": (True, 20., 10),
|
"drivingModelData": (True, 20., 10),
|
||||||
"modelV2": (True, 20.),
|
"modelV2": (True, 20., None, QueueSize.BIG),
|
||||||
"managerState": (True, 2., 1),
|
"managerState": (True, 2., 1),
|
||||||
"uploaderState": (True, 0., 1),
|
"uploaderState": (True, 0., 1),
|
||||||
"navInstruction": (True, 1., 10),
|
"navInstruction": (True, 1., 10),
|
||||||
@@ -77,21 +84,21 @@ _services: dict[str, tuple] = {
|
|||||||
"rawAudioData": (False, 20.),
|
"rawAudioData": (False, 20.),
|
||||||
"bookmarkButton": (True, 0., 1),
|
"bookmarkButton": (True, 0., 1),
|
||||||
"audioFeedback": (True, 0., 1),
|
"audioFeedback": (True, 0., 1),
|
||||||
|
"roadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||||
|
"driverEncodeData": (False, 20., None, QueueSize.BIG),
|
||||||
|
"wideRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||||
|
"qRoadEncodeData": (False, 20., None, QueueSize.BIG),
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
"uiDebug": (True, 0., 1),
|
"uiDebug": (True, 0., 1),
|
||||||
"testJoystick": (True, 0.),
|
"testJoystick": (True, 0.),
|
||||||
"alertDebug": (True, 20., 5),
|
"alertDebug": (True, 20., 5),
|
||||||
"roadEncodeData": (False, 20.),
|
|
||||||
"driverEncodeData": (False, 20.),
|
|
||||||
"wideRoadEncodeData": (False, 20.),
|
|
||||||
"qRoadEncodeData": (False, 20.),
|
|
||||||
"livestreamWideRoadEncodeIdx": (False, 20.),
|
"livestreamWideRoadEncodeIdx": (False, 20.),
|
||||||
"livestreamRoadEncodeIdx": (False, 20.),
|
"livestreamRoadEncodeIdx": (False, 20.),
|
||||||
"livestreamDriverEncodeIdx": (False, 20.),
|
"livestreamDriverEncodeIdx": (False, 20.),
|
||||||
"livestreamWideRoadEncodeData": (False, 20.),
|
"livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||||
"livestreamRoadEncodeData": (False, 20.),
|
"livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||||
"livestreamDriverEncodeData": (False, 20.),
|
"livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM),
|
||||||
"customReservedRawData0": (True, 0.),
|
"customReservedRawData0": (True, 0.),
|
||||||
"customReservedRawData1": (True, 0.),
|
"customReservedRawData1": (True, 0.),
|
||||||
"customReservedRawData2": (True, 0.),
|
"customReservedRawData2": (True, 0.),
|
||||||
@@ -109,13 +116,13 @@ def build_header():
|
|||||||
h += "#include <map>\n"
|
h += "#include <map>\n"
|
||||||
h += "#include <string>\n"
|
h += "#include <string>\n"
|
||||||
|
|
||||||
h += "struct service { std::string name; bool should_log; float frequency; int decimation; };\n"
|
h += "struct service { std::string name; bool should_log; float frequency; int decimation; size_t queue_size; };\n"
|
||||||
h += "static std::map<std::string, service> services = {\n"
|
h += "static std::map<std::string, service> services = {\n"
|
||||||
for k, v in SERVICE_LIST.items():
|
for k, v in SERVICE_LIST.items():
|
||||||
should_log = "true" if v.should_log else "false"
|
should_log = "true" if v.should_log else "false"
|
||||||
decimation = -1 if v.decimation is None else v.decimation
|
decimation = -1 if v.decimation is None else v.decimation
|
||||||
h += ' { "%s", {"%s", %s, %f, %d}},\n' % \
|
h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \
|
||||||
(k, k, should_log, v.frequency, decimation)
|
(k, k, should_log, v.frequency, decimation, v.queue_size)
|
||||||
h += "};\n"
|
h += "};\n"
|
||||||
|
|
||||||
h += "#endif\n"
|
h += "#endif\n"
|
||||||
|
|||||||
+1
-6
@@ -19,11 +19,6 @@ if GetOption('extras'):
|
|||||||
# Cython bindings
|
# Cython bindings
|
||||||
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
|
params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11'])
|
||||||
|
|
||||||
SConscript([
|
common_python = [params_python]
|
||||||
'transformations/SConscript',
|
|
||||||
])
|
|
||||||
|
|
||||||
Import('transformations_python')
|
|
||||||
common_python = [params_python, transformations_python]
|
|
||||||
|
|
||||||
Export('common_python')
|
Export('common_python')
|
||||||
|
|||||||
+4
-2
@@ -42,14 +42,16 @@ class Api:
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def api_get(endpoint, method='GET', timeout=None, access_token=None, **params):
|
def api_get(endpoint, method='GET', timeout=None, access_token=None, session=None, **params):
|
||||||
headers = {}
|
headers = {}
|
||||||
if access_token is not None:
|
if access_token is not None:
|
||||||
headers['Authorization'] = "JWT " + access_token
|
headers['Authorization'] = "JWT " + access_token
|
||||||
|
|
||||||
headers['User-Agent'] = "openpilot-" + get_version()
|
headers['User-Agent'] = "openpilot-" + get_version()
|
||||||
|
|
||||||
return requests.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params)
|
# TODO: add session to Api
|
||||||
|
req = requests if session is None else session
|
||||||
|
return req.request(method, API_HOST + "/" + endpoint, timeout=timeout, headers=headers, params=params)
|
||||||
|
|
||||||
|
|
||||||
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
|
def get_key_pair() -> tuple[str, str, str] | tuple[None, None, None]:
|
||||||
|
|||||||
+6
-6
@@ -4,27 +4,27 @@ from openpilot.common.utils import run_cmd, run_cmd_default
|
|||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_commit(cwd: str = None, branch: str = "HEAD") -> str:
|
def get_commit(cwd: str | None = None, branch: str = "HEAD") -> str:
|
||||||
return run_cmd_default(["git", "rev-parse", branch], cwd=cwd)
|
return run_cmd_default(["git", "rev-parse", branch], cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_commit_date(cwd: str = None, commit: str = "HEAD") -> str:
|
def get_commit_date(cwd: str | None = None, commit: str = "HEAD") -> str:
|
||||||
return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit], cwd=cwd)
|
return run_cmd_default(["git", "show", "--no-patch", "--format='%ct %ci'", commit], cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_short_branch(cwd: str = None) -> str:
|
def get_short_branch(cwd: str | None = None) -> str:
|
||||||
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
|
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_branch(cwd: str = None) -> str:
|
def get_branch(cwd: str | None = None) -> str:
|
||||||
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd)
|
return run_cmd_default(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], cwd=cwd)
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_origin(cwd: str = None) -> str:
|
def get_origin(cwd: str | None = None) -> str:
|
||||||
try:
|
try:
|
||||||
local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], cwd=cwd)
|
local_branch = run_cmd(["git", "name-rev", "--name-only", "HEAD"], cwd=cwd)
|
||||||
tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"], cwd=cwd)
|
tracking_remote = run_cmd(["git", "config", "branch." + local_branch + ".remote"], cwd=cwd)
|
||||||
@@ -34,7 +34,7 @@ def get_origin(cwd: str = None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_normalized_origin(cwd: str = None) -> str:
|
def get_normalized_origin(cwd: str | None = None) -> str:
|
||||||
return get_origin(cwd) \
|
return get_origin(cwd) \
|
||||||
.replace("git@", "", 1) \
|
.replace("git@", "", 1) \
|
||||||
.replace(".git", "", 1) \
|
.replace(".git", "", 1) \
|
||||||
|
|||||||
+3
-9
@@ -3,15 +3,9 @@ from numbers import Number
|
|||||||
|
|
||||||
class PIDController:
|
class PIDController:
|
||||||
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
|
||||||
self._k_p = k_p
|
self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p
|
||||||
self._k_i = k_i
|
self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i
|
||||||
self._k_d = k_d
|
self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d
|
||||||
if isinstance(self._k_p, Number):
|
|
||||||
self._k_p = [[0], [self._k_p]]
|
|
||||||
if isinstance(self._k_i, Number):
|
|
||||||
self._k_i = [[0], [self._k_i]]
|
|
||||||
if isinstance(self._k_d, Number):
|
|
||||||
self._k_d = [[0], [self._k_d]]
|
|
||||||
|
|
||||||
self.set_limits(pos_limit, neg_limit)
|
self.set_limits(pos_limit, neg_limit)
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -13,7 +13,11 @@ public:
|
|||||||
if (prefix.empty()) {
|
if (prefix.empty()) {
|
||||||
prefix = util::random_string(15);
|
prefix = util::random_string(15);
|
||||||
}
|
}
|
||||||
msgq_path = Path::shm_path() + "/" + prefix;
|
#ifdef __APPLE__
|
||||||
|
msgq_path = "/tmp/msgq_" + prefix;
|
||||||
|
#else
|
||||||
|
msgq_path = "/dev/shm/msgq_" + prefix;
|
||||||
|
#endif
|
||||||
bool ret = util::create_directories(msgq_path, 0777);
|
bool ret = util::create_directories(msgq_path, 0777);
|
||||||
assert(ret);
|
assert(ret);
|
||||||
setenv("OPENPILOT_PREFIX", prefix.c_str(), 1);
|
setenv("OPENPILOT_PREFIX", prefix.c_str(), 1);
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -9,9 +10,10 @@ from openpilot.system.hardware.hw import Paths
|
|||||||
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
|
from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
|
||||||
|
|
||||||
class OpenpilotPrefix:
|
class OpenpilotPrefix:
|
||||||
def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
|
def __init__(self, prefix: str | None = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False):
|
||||||
self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15])
|
self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15])
|
||||||
self.msgq_path = os.path.join(Paths.shm_path(), self.prefix)
|
shm_path = "/tmp" if platform.system() == "Darwin" else "/dev/shm"
|
||||||
|
self.msgq_path = os.path.join(shm_path, "msgq_" + self.prefix)
|
||||||
self.create_dirs_on_enter = create_dirs_on_enter
|
self.create_dirs_on_enter = create_dirs_on_enter
|
||||||
self.clean_dirs_on_exit = clean_dirs_on_exit
|
self.clean_dirs_on_exit = clean_dirs_on_exit
|
||||||
self.shared_download_cache = shared_download_cache
|
self.shared_download_cache = shared_download_cache
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import time
|
|||||||
|
|
||||||
from setproctitle import getproctitle
|
from setproctitle import getproctitle
|
||||||
|
|
||||||
from openpilot.common.util import MovingAverage
|
from openpilot.common.utils import MovingAverage
|
||||||
from openpilot.system.hardware import PC
|
from openpilot.system.hardware import PC
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Import('env', 'envCython')
|
|
||||||
|
|
||||||
transformations = env.Library('transformations', ['orientation.cc', 'coordinates.cc'])
|
|
||||||
transformations_python = envCython.Program('transformations.so', 'transformations.pyx')
|
|
||||||
Export('transformations', 'transformations_python')
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#define _USE_MATH_DEFINES
|
|
||||||
|
|
||||||
#include "common/transformations/coordinates.hpp"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <cmath>
|
|
||||||
#include <eigen3/Eigen/Dense>
|
|
||||||
|
|
||||||
double a = 6378137; // lgtm [cpp/short-global-name]
|
|
||||||
double b = 6356752.3142; // lgtm [cpp/short-global-name]
|
|
||||||
double esq = 6.69437999014 * 0.001; // lgtm [cpp/short-global-name]
|
|
||||||
double e1sq = 6.73949674228 * 0.001;
|
|
||||||
|
|
||||||
|
|
||||||
static Geodetic to_degrees(Geodetic geodetic){
|
|
||||||
geodetic.lat = RAD2DEG(geodetic.lat);
|
|
||||||
geodetic.lon = RAD2DEG(geodetic.lon);
|
|
||||||
return geodetic;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Geodetic to_radians(Geodetic geodetic){
|
|
||||||
geodetic.lat = DEG2RAD(geodetic.lat);
|
|
||||||
geodetic.lon = DEG2RAD(geodetic.lon);
|
|
||||||
return geodetic;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ECEF geodetic2ecef(const Geodetic &geodetic) {
|
|
||||||
auto g = to_radians(geodetic);
|
|
||||||
double xi = sqrt(1.0 - esq * pow(sin(g.lat), 2));
|
|
||||||
double x = (a / xi + g.alt) * cos(g.lat) * cos(g.lon);
|
|
||||||
double y = (a / xi + g.alt) * cos(g.lat) * sin(g.lon);
|
|
||||||
double z = (a / xi * (1.0 - esq) + g.alt) * sin(g.lat);
|
|
||||||
return {x, y, z};
|
|
||||||
}
|
|
||||||
|
|
||||||
Geodetic ecef2geodetic(const ECEF &e) {
|
|
||||||
// Convert from ECEF to geodetic using Ferrari's methods
|
|
||||||
// https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#Ferrari.27s_solution
|
|
||||||
double x = e.x;
|
|
||||||
double y = e.y;
|
|
||||||
double z = e.z;
|
|
||||||
|
|
||||||
double r = sqrt(x * x + y * y);
|
|
||||||
double Esq = a * a - b * b;
|
|
||||||
double F = 54 * b * b * z * z;
|
|
||||||
double G = r * r + (1 - esq) * z * z - esq * Esq;
|
|
||||||
double C = (esq * esq * F * r * r) / (pow(G, 3));
|
|
||||||
double S = cbrt(1 + C + sqrt(C * C + 2 * C));
|
|
||||||
double P = F / (3 * pow((S + 1 / S + 1), 2) * G * G);
|
|
||||||
double Q = sqrt(1 + 2 * esq * esq * P);
|
|
||||||
double r_0 = -(P * esq * r) / (1 + Q) + sqrt(0.5 * a * a*(1 + 1.0 / Q) - P * (1 - esq) * z * z / (Q * (1 + Q)) - 0.5 * P * r * r);
|
|
||||||
double U = sqrt(pow((r - esq * r_0), 2) + z * z);
|
|
||||||
double V = sqrt(pow((r - esq * r_0), 2) + (1 - esq) * z * z);
|
|
||||||
double Z_0 = b * b * z / (a * V);
|
|
||||||
double h = U * (1 - b * b / (a * V));
|
|
||||||
|
|
||||||
double lat = atan((z + e1sq * Z_0) / r);
|
|
||||||
double lon = atan2(y, x);
|
|
||||||
|
|
||||||
return to_degrees({lat, lon, h});
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalCoord::LocalCoord(const Geodetic &geodetic, const ECEF &e) {
|
|
||||||
init_ecef << e.x, e.y, e.z;
|
|
||||||
|
|
||||||
auto g = to_radians(geodetic);
|
|
||||||
|
|
||||||
ned2ecef_matrix <<
|
|
||||||
-sin(g.lat)*cos(g.lon), -sin(g.lon), -cos(g.lat)*cos(g.lon),
|
|
||||||
-sin(g.lat)*sin(g.lon), cos(g.lon), -cos(g.lat)*sin(g.lon),
|
|
||||||
cos(g.lat), 0, -sin(g.lat);
|
|
||||||
ecef2ned_matrix = ned2ecef_matrix.transpose();
|
|
||||||
}
|
|
||||||
|
|
||||||
NED LocalCoord::ecef2ned(const ECEF &e) {
|
|
||||||
Eigen::Vector3d ecef;
|
|
||||||
ecef << e.x, e.y, e.z;
|
|
||||||
|
|
||||||
Eigen::Vector3d ned = (ecef2ned_matrix * (ecef - init_ecef));
|
|
||||||
return {ned[0], ned[1], ned[2]};
|
|
||||||
}
|
|
||||||
|
|
||||||
ECEF LocalCoord::ned2ecef(const NED &n) {
|
|
||||||
Eigen::Vector3d ned;
|
|
||||||
ned << n.n, n.e, n.d;
|
|
||||||
|
|
||||||
Eigen::Vector3d ecef = (ned2ecef_matrix * ned) + init_ecef;
|
|
||||||
return {ecef[0], ecef[1], ecef[2]};
|
|
||||||
}
|
|
||||||
|
|
||||||
NED LocalCoord::geodetic2ned(const Geodetic &g) {
|
|
||||||
ECEF e = ::geodetic2ecef(g);
|
|
||||||
return ecef2ned(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Geodetic LocalCoord::ned2geodetic(const NED &n) {
|
|
||||||
ECEF e = ned2ecef(n);
|
|
||||||
return ::ecef2geodetic(e);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <eigen3/Eigen/Dense>
|
|
||||||
|
|
||||||
#define DEG2RAD(x) ((x) * M_PI / 180.0)
|
|
||||||
#define RAD2DEG(x) ((x) * 180.0 / M_PI)
|
|
||||||
|
|
||||||
struct ECEF {
|
|
||||||
double x, y, z;
|
|
||||||
Eigen::Vector3d to_vector() const {
|
|
||||||
return Eigen::Vector3d(x, y, z);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NED {
|
|
||||||
double n, e, d;
|
|
||||||
Eigen::Vector3d to_vector() const {
|
|
||||||
return Eigen::Vector3d(n, e, d);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Geodetic {
|
|
||||||
double lat, lon, alt;
|
|
||||||
bool radians=false;
|
|
||||||
};
|
|
||||||
|
|
||||||
ECEF geodetic2ecef(const Geodetic &g);
|
|
||||||
Geodetic ecef2geodetic(const ECEF &e);
|
|
||||||
|
|
||||||
class LocalCoord {
|
|
||||||
public:
|
|
||||||
Eigen::Matrix3d ned2ecef_matrix;
|
|
||||||
Eigen::Matrix3d ecef2ned_matrix;
|
|
||||||
Eigen::Vector3d init_ecef;
|
|
||||||
LocalCoord(const Geodetic &g, const ECEF &e);
|
|
||||||
LocalCoord(const Geodetic &g) : LocalCoord(g, ::geodetic2ecef(g)) {}
|
|
||||||
LocalCoord(const ECEF &e) : LocalCoord(::ecef2geodetic(e), e) {}
|
|
||||||
|
|
||||||
NED ecef2ned(const ECEF &e);
|
|
||||||
ECEF ned2ecef(const NED &n);
|
|
||||||
NED geodetic2ned(const Geodetic &g);
|
|
||||||
Geodetic ned2geodetic(const NED &n);
|
|
||||||
};
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
#define _USE_MATH_DEFINES
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <cmath>
|
|
||||||
#include <eigen3/Eigen/Dense>
|
|
||||||
|
|
||||||
#include "common/transformations/orientation.hpp"
|
|
||||||
#include "common/transformations/coordinates.hpp"
|
|
||||||
|
|
||||||
Eigen::Quaterniond ensure_unique(const Eigen::Quaterniond &quat) {
|
|
||||||
if (quat.w() > 0){
|
|
||||||
return quat;
|
|
||||||
} else {
|
|
||||||
return Eigen::Quaterniond(-quat.w(), -quat.x(), -quat.y(), -quat.z());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Quaterniond euler2quat(const Eigen::Vector3d &euler) {
|
|
||||||
Eigen::Quaterniond q;
|
|
||||||
|
|
||||||
q = Eigen::AngleAxisd(euler(2), Eigen::Vector3d::UnitZ())
|
|
||||||
* Eigen::AngleAxisd(euler(1), Eigen::Vector3d::UnitY())
|
|
||||||
* Eigen::AngleAxisd(euler(0), Eigen::Vector3d::UnitX());
|
|
||||||
return ensure_unique(q);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Eigen::Vector3d quat2euler(const Eigen::Quaterniond &quat) {
|
|
||||||
// TODO: switch to eigen implementation if the range of the Euler angles doesn't matter anymore
|
|
||||||
// Eigen::Vector3d euler = quat.toRotationMatrix().eulerAngles(2, 1, 0);
|
|
||||||
// return {euler(2), euler(1), euler(0)};
|
|
||||||
double gamma = atan2(2 * (quat.w() * quat.x() + quat.y() * quat.z()), 1 - 2 * (quat.x()*quat.x() + quat.y()*quat.y()));
|
|
||||||
double asin_arg_clipped = std::clamp(2 * (quat.w() * quat.y() - quat.z() * quat.x()), -1.0, 1.0);
|
|
||||||
double theta = asin(asin_arg_clipped);
|
|
||||||
double psi = atan2(2 * (quat.w() * quat.z() + quat.x() * quat.y()), 1 - 2 * (quat.y()*quat.y() + quat.z()*quat.z()));
|
|
||||||
return {gamma, theta, psi};
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Matrix3d quat2rot(const Eigen::Quaterniond &quat) {
|
|
||||||
return quat.toRotationMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Quaterniond rot2quat(const Eigen::Matrix3d &rot) {
|
|
||||||
return ensure_unique(Eigen::Quaterniond(rot));
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Matrix3d euler2rot(const Eigen::Vector3d &euler) {
|
|
||||||
return quat2rot(euler2quat(euler));
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Vector3d rot2euler(const Eigen::Matrix3d &rot) {
|
|
||||||
return quat2euler(rot2quat(rot));
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Matrix3d rot_matrix(double roll, double pitch, double yaw) {
|
|
||||||
return euler2rot({roll, pitch, yaw});
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Matrix3d rot(const Eigen::Vector3d &axis, double angle) {
|
|
||||||
Eigen::Quaterniond q;
|
|
||||||
q = Eigen::AngleAxisd(angle, axis);
|
|
||||||
return q.toRotationMatrix();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Eigen::Vector3d ecef_euler_from_ned(const ECEF &ecef_init, const Eigen::Vector3d &ned_pose) {
|
|
||||||
/*
|
|
||||||
Using Rotations to Build Aerospace Coordinate Systems
|
|
||||||
Don Koks
|
|
||||||
https://apps.dtic.mil/dtic/tr/fulltext/u2/a484864.pdf
|
|
||||||
*/
|
|
||||||
LocalCoord converter = LocalCoord(ecef_init);
|
|
||||||
Eigen::Vector3d zero = ecef_init.to_vector();
|
|
||||||
|
|
||||||
Eigen::Vector3d x0 = converter.ned2ecef({1, 0, 0}).to_vector() - zero;
|
|
||||||
Eigen::Vector3d y0 = converter.ned2ecef({0, 1, 0}).to_vector() - zero;
|
|
||||||
Eigen::Vector3d z0 = converter.ned2ecef({0, 0, 1}).to_vector() - zero;
|
|
||||||
|
|
||||||
Eigen::Vector3d x1 = rot(z0, ned_pose(2)) * x0;
|
|
||||||
Eigen::Vector3d y1 = rot(z0, ned_pose(2)) * y0;
|
|
||||||
Eigen::Vector3d z1 = rot(z0, ned_pose(2)) * z0;
|
|
||||||
|
|
||||||
Eigen::Vector3d x2 = rot(y1, ned_pose(1)) * x1;
|
|
||||||
Eigen::Vector3d y2 = rot(y1, ned_pose(1)) * y1;
|
|
||||||
Eigen::Vector3d z2 = rot(y1, ned_pose(1)) * z1;
|
|
||||||
|
|
||||||
Eigen::Vector3d x3 = rot(x2, ned_pose(0)) * x2;
|
|
||||||
Eigen::Vector3d y3 = rot(x2, ned_pose(0)) * y2;
|
|
||||||
|
|
||||||
|
|
||||||
x0 = Eigen::Vector3d(1, 0, 0);
|
|
||||||
y0 = Eigen::Vector3d(0, 1, 0);
|
|
||||||
z0 = Eigen::Vector3d(0, 0, 1);
|
|
||||||
|
|
||||||
double psi = atan2(x3.dot(y0), x3.dot(x0));
|
|
||||||
double theta = atan2(-x3.dot(z0), sqrt(pow(x3.dot(x0), 2) + pow(x3.dot(y0), 2)));
|
|
||||||
|
|
||||||
y2 = rot(z0, psi) * y0;
|
|
||||||
z2 = rot(y2, theta) * z0;
|
|
||||||
|
|
||||||
double phi = atan2(y3.dot(z2), y3.dot(y2));
|
|
||||||
|
|
||||||
return {phi, theta, psi};
|
|
||||||
}
|
|
||||||
|
|
||||||
Eigen::Vector3d ned_euler_from_ecef(const ECEF &ecef_init, const Eigen::Vector3d &ecef_pose) {
|
|
||||||
/*
|
|
||||||
Using Rotations to Build Aerospace Coordinate Systems
|
|
||||||
Don Koks
|
|
||||||
https://apps.dtic.mil/dtic/tr/fulltext/u2/a484864.pdf
|
|
||||||
*/
|
|
||||||
LocalCoord converter = LocalCoord(ecef_init);
|
|
||||||
|
|
||||||
Eigen::Vector3d x0 = Eigen::Vector3d(1, 0, 0);
|
|
||||||
Eigen::Vector3d y0 = Eigen::Vector3d(0, 1, 0);
|
|
||||||
Eigen::Vector3d z0 = Eigen::Vector3d(0, 0, 1);
|
|
||||||
|
|
||||||
Eigen::Vector3d x1 = rot(z0, ecef_pose(2)) * x0;
|
|
||||||
Eigen::Vector3d y1 = rot(z0, ecef_pose(2)) * y0;
|
|
||||||
Eigen::Vector3d z1 = rot(z0, ecef_pose(2)) * z0;
|
|
||||||
|
|
||||||
Eigen::Vector3d x2 = rot(y1, ecef_pose(1)) * x1;
|
|
||||||
Eigen::Vector3d y2 = rot(y1, ecef_pose(1)) * y1;
|
|
||||||
Eigen::Vector3d z2 = rot(y1, ecef_pose(1)) * z1;
|
|
||||||
|
|
||||||
Eigen::Vector3d x3 = rot(x2, ecef_pose(0)) * x2;
|
|
||||||
Eigen::Vector3d y3 = rot(x2, ecef_pose(0)) * y2;
|
|
||||||
|
|
||||||
Eigen::Vector3d zero = ecef_init.to_vector();
|
|
||||||
x0 = converter.ned2ecef({1, 0, 0}).to_vector() - zero;
|
|
||||||
y0 = converter.ned2ecef({0, 1, 0}).to_vector() - zero;
|
|
||||||
z0 = converter.ned2ecef({0, 0, 1}).to_vector() - zero;
|
|
||||||
|
|
||||||
double psi = atan2(x3.dot(y0), x3.dot(x0));
|
|
||||||
double theta = atan2(-x3.dot(z0), sqrt(pow(x3.dot(x0), 2) + pow(x3.dot(y0), 2)));
|
|
||||||
|
|
||||||
y2 = rot(z0, psi) * y0;
|
|
||||||
z2 = rot(y2, theta) * z0;
|
|
||||||
|
|
||||||
double phi = atan2(y3.dot(z2), y3.dot(y2));
|
|
||||||
|
|
||||||
return {phi, theta, psi};
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <eigen3/Eigen/Dense>
|
|
||||||
#include "common/transformations/coordinates.hpp"
|
|
||||||
|
|
||||||
|
|
||||||
Eigen::Quaterniond ensure_unique(const Eigen::Quaterniond &quat);
|
|
||||||
|
|
||||||
Eigen::Quaterniond euler2quat(const Eigen::Vector3d &euler);
|
|
||||||
Eigen::Vector3d quat2euler(const Eigen::Quaterniond &quat);
|
|
||||||
Eigen::Matrix3d quat2rot(const Eigen::Quaterniond &quat);
|
|
||||||
Eigen::Quaterniond rot2quat(const Eigen::Matrix3d &rot);
|
|
||||||
Eigen::Matrix3d euler2rot(const Eigen::Vector3d &euler);
|
|
||||||
Eigen::Vector3d rot2euler(const Eigen::Matrix3d &rot);
|
|
||||||
Eigen::Matrix3d rot_matrix(double roll, double pitch, double yaw);
|
|
||||||
Eigen::Matrix3d rot(const Eigen::Vector3d &axis, double angle);
|
|
||||||
Eigen::Vector3d ecef_euler_from_ned(const ECEF &ecef_init, const Eigen::Vector3d &ned_pose);
|
|
||||||
Eigen::Vector3d ned_euler_from_ecef(const ECEF &ecef_init, const Eigen::Vector3d &ecef_pose);
|
|
||||||
@@ -102,3 +102,36 @@ class TestNED:
|
|||||||
np.testing.assert_allclose(converter.ned2ecef(ned_offsets_batch),
|
np.testing.assert_allclose(converter.ned2ecef(ned_offsets_batch),
|
||||||
ecef_positions_offset_batch,
|
ecef_positions_offset_batch,
|
||||||
rtol=1e-9, atol=1e-7)
|
rtol=1e-9, atol=1e-7)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
# Test wrong shape/type for geodetic2ecef
|
||||||
|
# numpy_wrap raises IndexError for scalar input
|
||||||
|
with np.testing.assert_raises(IndexError):
|
||||||
|
coord.geodetic2ecef(1.0)
|
||||||
|
|
||||||
|
with np.testing.assert_raises_regex(ValueError, "Geodetic must be size 3"):
|
||||||
|
coord.geodetic2ecef([0, 0])
|
||||||
|
|
||||||
|
with np.testing.assert_raises_regex(ValueError, "Geodetic must be size 3"):
|
||||||
|
coord.geodetic2ecef([0, 0, 0, 0])
|
||||||
|
|
||||||
|
with np.testing.assert_raises(TypeError):
|
||||||
|
coord.geodetic2ecef(['a', 'b', 'c'])
|
||||||
|
|
||||||
|
# Test LocalCoord constructor errors
|
||||||
|
with np.testing.assert_raises(ValueError):
|
||||||
|
coord.LocalCoord.from_geodetic([0, 0])
|
||||||
|
|
||||||
|
with np.testing.assert_raises(ValueError):
|
||||||
|
coord.LocalCoord.from_geodetic(1)
|
||||||
|
|
||||||
|
with np.testing.assert_raises(TypeError):
|
||||||
|
coord.LocalCoord.from_geodetic(['a', 'b', 'c'])
|
||||||
|
|
||||||
|
# Test wrong shape/type for ecef2geodetic
|
||||||
|
with np.testing.assert_raises(ValueError):
|
||||||
|
coord.ecef2geodetic([1, 2])
|
||||||
|
with np.testing.assert_raises(ValueError):
|
||||||
|
coord.ecef2geodetic([1, 2, 3, 4])
|
||||||
|
with np.testing.assert_raises(IndexError):
|
||||||
|
coord.ecef2geodetic(1.0)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
|
from openpilot.common.transformations.orientation import euler2quat, quat2euler, euler2rot, rot2euler, \
|
||||||
rot2quat, quat2rot, \
|
rot2quat, quat2rot, \
|
||||||
@@ -59,3 +60,32 @@ class TestOrientation:
|
|||||||
np.testing.assert_allclose(ned_eulers[i], ned_euler_from_ecef(ecef_positions[i], eulers[i]), rtol=1e-7)
|
np.testing.assert_allclose(ned_eulers[i], ned_euler_from_ecef(ecef_positions[i], eulers[i]), rtol=1e-7)
|
||||||
#np.testing.assert_allclose(eulers[i], ecef_euler_from_ned(ecef_positions[i], ned_eulers[i]), rtol=1e-7)
|
#np.testing.assert_allclose(eulers[i], ecef_euler_from_ned(ecef_positions[i], ned_eulers[i]), rtol=1e-7)
|
||||||
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
|
# np.testing.assert_allclose(ned_eulers, ned_euler_from_ecef(ecef_positions, eulers), rtol=1e-7)
|
||||||
|
|
||||||
|
def test_inputs(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
euler2quat([1, 2])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
quat2rot([1, 2, 3])
|
||||||
|
|
||||||
|
with pytest.raises(IndexError):
|
||||||
|
rot2quat(np.zeros((2, 2)))
|
||||||
|
|
||||||
|
def test_euler_rot_consistency(self):
|
||||||
|
rpy = [0.1, 0.2, 0.3]
|
||||||
|
R = euler2rot(rpy)
|
||||||
|
|
||||||
|
# R -> q -> R
|
||||||
|
q = rot2quat(R)
|
||||||
|
R_new = quat2rot(q)
|
||||||
|
np.testing.assert_allclose(R, R_new, atol=1e-15)
|
||||||
|
|
||||||
|
# q -> R -> Euler (quat2euler) -> R
|
||||||
|
rpy_new = quat2euler(q)
|
||||||
|
R_new2 = euler2rot(rpy_new)
|
||||||
|
np.testing.assert_allclose(R, R_new2, atol=1e-15)
|
||||||
|
|
||||||
|
# R -> Euler (rot2euler) -> R
|
||||||
|
rpy_from_rot = rot2euler(R)
|
||||||
|
R_new3 = euler2rot(rpy_from_rot)
|
||||||
|
np.testing.assert_allclose(R, R_new3, atol=1e-15)
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
# cython: language_level=3
|
|
||||||
from libcpp cimport bool
|
|
||||||
|
|
||||||
cdef extern from "orientation.cc":
|
|
||||||
pass
|
|
||||||
|
|
||||||
cdef extern from "orientation.hpp":
|
|
||||||
cdef cppclass Quaternion "Eigen::Quaterniond":
|
|
||||||
Quaternion()
|
|
||||||
Quaternion(double, double, double, double)
|
|
||||||
double w()
|
|
||||||
double x()
|
|
||||||
double y()
|
|
||||||
double z()
|
|
||||||
|
|
||||||
cdef cppclass Vector3 "Eigen::Vector3d":
|
|
||||||
Vector3()
|
|
||||||
Vector3(double, double, double)
|
|
||||||
double operator()(int)
|
|
||||||
|
|
||||||
cdef cppclass Matrix3 "Eigen::Matrix3d":
|
|
||||||
Matrix3()
|
|
||||||
Matrix3(double*)
|
|
||||||
|
|
||||||
double operator()(int, int)
|
|
||||||
|
|
||||||
Quaternion euler2quat(const Vector3 &)
|
|
||||||
Vector3 quat2euler(const Quaternion &)
|
|
||||||
Matrix3 quat2rot(const Quaternion &)
|
|
||||||
Quaternion rot2quat(const Matrix3 &)
|
|
||||||
Vector3 rot2euler(const Matrix3 &)
|
|
||||||
Matrix3 euler2rot(const Vector3 &)
|
|
||||||
Matrix3 rot_matrix(double, double, double)
|
|
||||||
Vector3 ecef_euler_from_ned(const ECEF &, const Vector3 &)
|
|
||||||
Vector3 ned_euler_from_ecef(const ECEF &, const Vector3 &)
|
|
||||||
|
|
||||||
|
|
||||||
cdef extern from "coordinates.cc":
|
|
||||||
cdef struct ECEF:
|
|
||||||
double x
|
|
||||||
double y
|
|
||||||
double z
|
|
||||||
|
|
||||||
cdef struct NED:
|
|
||||||
double n
|
|
||||||
double e
|
|
||||||
double d
|
|
||||||
|
|
||||||
cdef struct Geodetic:
|
|
||||||
double lat
|
|
||||||
double lon
|
|
||||||
double alt
|
|
||||||
bool radians
|
|
||||||
|
|
||||||
ECEF geodetic2ecef(const Geodetic &)
|
|
||||||
Geodetic ecef2geodetic(const ECEF &)
|
|
||||||
|
|
||||||
cdef cppclass LocalCoord_c "LocalCoord":
|
|
||||||
Matrix3 ned2ecef_matrix
|
|
||||||
Matrix3 ecef2ned_matrix
|
|
||||||
|
|
||||||
LocalCoord_c(const Geodetic &, const ECEF &)
|
|
||||||
LocalCoord_c(const Geodetic &)
|
|
||||||
LocalCoord_c(const ECEF &)
|
|
||||||
|
|
||||||
NED ecef2ned(const ECEF &)
|
|
||||||
ECEF ned2ecef(const NED &)
|
|
||||||
NED geodetic2ned(const Geodetic &)
|
|
||||||
Geodetic ned2geodetic(const NED &)
|
|
||||||
|
|
||||||
cdef extern from "coordinates.hpp":
|
|
||||||
pass
|
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
a = 6378137.0
|
||||||
|
b = 6356752.3142
|
||||||
|
esq = 6.69437999014e-3
|
||||||
|
e1sq = 6.73949674228e-3
|
||||||
|
|
||||||
|
|
||||||
|
def geodetic2ecef_single(g):
|
||||||
|
"""
|
||||||
|
Convert geodetic coordinates (latitude, longitude, altitude) to ECEF.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if len(g) != 3:
|
||||||
|
raise ValueError("Geodetic must be size 3")
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Geodetic must be a sequence of length 3") from None
|
||||||
|
|
||||||
|
lat, lon, alt = g
|
||||||
|
lat = np.radians(lat)
|
||||||
|
lon = np.radians(lon)
|
||||||
|
xi = np.sqrt(1.0 - esq * np.sin(lat)**2)
|
||||||
|
x = (a / xi + alt) * np.cos(lat) * np.cos(lon)
|
||||||
|
y = (a / xi + alt) * np.cos(lat) * np.sin(lon)
|
||||||
|
z = (a / xi * (1.0 - esq) + alt) * np.sin(lat)
|
||||||
|
return np.array([x, y, z])
|
||||||
|
|
||||||
|
|
||||||
|
def ecef2geodetic_single(e):
|
||||||
|
"""
|
||||||
|
Convert ECEF to geodetic coordinates using Ferrari's solution.
|
||||||
|
"""
|
||||||
|
x, y, z = e
|
||||||
|
r = np.sqrt(x**2 + y**2)
|
||||||
|
Esq = a**2 - b**2
|
||||||
|
F = 54 * b**2 * z**2
|
||||||
|
G = r**2 + (1 - esq) * z**2 - esq * Esq
|
||||||
|
C = (esq**2 * F * r**2) / (G**3)
|
||||||
|
S = np.cbrt(1 + C + np.sqrt(C**2 + 2 * C))
|
||||||
|
P = F / (3 * (S + 1 / S + 1)**2 * G**2)
|
||||||
|
Q = np.sqrt(1 + 2 * esq**2 * P)
|
||||||
|
r_0 = -(P * esq * r) / (1 + Q) + np.sqrt(0.5 * a**2 * (1 + 1.0 / Q) - P * (1 - esq) * z**2 / (Q * (1 + Q)) - 0.5 * P * r**2)
|
||||||
|
U = np.sqrt((r - esq * r_0)**2 + z**2)
|
||||||
|
V = np.sqrt((r - esq * r_0)**2 + (1 - esq) * z**2)
|
||||||
|
Z_0 = b**2 * z / (a * V)
|
||||||
|
h = U * (1 - b**2 / (a * V))
|
||||||
|
lat = np.arctan((z + e1sq * Z_0) / r)
|
||||||
|
lon = np.arctan2(y, x)
|
||||||
|
return np.array([np.degrees(lat), np.degrees(lon), h])
|
||||||
|
|
||||||
|
|
||||||
|
def euler2quat_single(euler):
|
||||||
|
"""
|
||||||
|
Convert Euler angles (roll, pitch, yaw) to a quaternion.
|
||||||
|
Rotation order: Z-Y-X (yaw, pitch, roll).
|
||||||
|
"""
|
||||||
|
phi, theta, psi = euler
|
||||||
|
|
||||||
|
c_phi, s_phi = np.cos(phi / 2), np.sin(phi / 2)
|
||||||
|
c_theta, s_theta = np.cos(theta / 2), np.sin(theta / 2)
|
||||||
|
c_psi, s_psi = np.cos(psi / 2), np.sin(psi / 2)
|
||||||
|
|
||||||
|
w = c_phi * c_theta * c_psi + s_phi * s_theta * s_psi
|
||||||
|
x = s_phi * c_theta * c_psi - c_phi * s_theta * s_psi
|
||||||
|
y = c_phi * s_theta * c_psi + s_phi * c_theta * s_psi
|
||||||
|
z = c_phi * c_theta * s_psi - s_phi * s_theta * c_psi
|
||||||
|
|
||||||
|
if w < 0:
|
||||||
|
return np.array([-w, -x, -y, -z])
|
||||||
|
return np.array([w, x, y, z])
|
||||||
|
|
||||||
|
|
||||||
|
def quat2euler_single(q):
|
||||||
|
"""
|
||||||
|
Convert a quaternion to Euler angles (roll, pitch, yaw).
|
||||||
|
"""
|
||||||
|
w, x, y, z = q
|
||||||
|
gamma = np.arctan2(2 * (w * x + y * z), 1 - 2 * (x**2 + y**2))
|
||||||
|
sin_arg = 2 * (w * y - z * x)
|
||||||
|
sin_arg = np.clip(sin_arg, -1.0, 1.0)
|
||||||
|
theta = np.arcsin(sin_arg)
|
||||||
|
psi = np.arctan2(2 * (w * z + x * y), 1 - 2 * (y**2 + z**2))
|
||||||
|
return np.array([gamma, theta, psi])
|
||||||
|
|
||||||
|
|
||||||
|
def quat2rot_single(q):
|
||||||
|
"""
|
||||||
|
Convert a quaternion to a 3x3 rotation matrix.
|
||||||
|
"""
|
||||||
|
w, x, y, z = q
|
||||||
|
xx, yy, zz = x * x, y * y, z * z
|
||||||
|
xy, xz, yz = x * y, x * z, y * z
|
||||||
|
wx, wy, wz = w * x, w * y, w * z
|
||||||
|
|
||||||
|
mat = np.array([
|
||||||
|
[1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)],
|
||||||
|
[2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)],
|
||||||
|
[2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)]
|
||||||
|
])
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def rot2quat_single(rot):
|
||||||
|
"""
|
||||||
|
Convert a 3x3 rotation matrix to a quaternion.
|
||||||
|
"""
|
||||||
|
trace = np.trace(rot)
|
||||||
|
if trace > 0:
|
||||||
|
s = 0.5 / np.sqrt(trace + 1.0)
|
||||||
|
w = 0.25 / s
|
||||||
|
x = (rot[2, 1] - rot[1, 2]) * s
|
||||||
|
y = (rot[0, 2] - rot[2, 0]) * s
|
||||||
|
z = (rot[1, 0] - rot[0, 1]) * s
|
||||||
|
else:
|
||||||
|
if rot[0, 0] > rot[1, 1] and rot[0, 0] > rot[2, 2]:
|
||||||
|
s = 2.0 * np.sqrt(1.0 + rot[0, 0] - rot[1, 1] - rot[2, 2])
|
||||||
|
w = (rot[2, 1] - rot[1, 2]) / s
|
||||||
|
x = 0.25 * s
|
||||||
|
y = (rot[0, 1] + rot[1, 0]) / s
|
||||||
|
z = (rot[0, 2] + rot[2, 0]) / s
|
||||||
|
elif rot[1, 1] > rot[2, 2]:
|
||||||
|
s = 2.0 * np.sqrt(1.0 + rot[1, 1] - rot[0, 0] - rot[2, 2])
|
||||||
|
w = (rot[0, 2] - rot[2, 0]) / s
|
||||||
|
x = (rot[0, 1] + rot[1, 0]) / s
|
||||||
|
y = 0.25 * s
|
||||||
|
z = (rot[1, 2] + rot[2, 1]) / s
|
||||||
|
else:
|
||||||
|
s = 2.0 * np.sqrt(1.0 + rot[2, 2] - rot[0, 0] - rot[1, 1])
|
||||||
|
w = (rot[1, 0] - rot[0, 1]) / s
|
||||||
|
x = (rot[0, 2] + rot[2, 0]) / s
|
||||||
|
y = (rot[1, 2] + rot[2, 1]) / s
|
||||||
|
z = 0.25 * s
|
||||||
|
|
||||||
|
if w < 0:
|
||||||
|
return np.array([-w, -x, -y, -z])
|
||||||
|
return np.array([w, x, y, z])
|
||||||
|
|
||||||
|
|
||||||
|
def euler2rot_single(euler):
|
||||||
|
"""
|
||||||
|
Convert Euler angles (roll, pitch, yaw) to a 3x3 rotation matrix.
|
||||||
|
Rotation order: Z-Y-X (yaw, pitch, roll).
|
||||||
|
"""
|
||||||
|
phi, theta, psi = euler
|
||||||
|
|
||||||
|
cx, sx = np.cos(phi), np.sin(phi)
|
||||||
|
cy, sy = np.cos(theta), np.sin(theta)
|
||||||
|
cz, sz = np.cos(psi), np.sin(psi)
|
||||||
|
|
||||||
|
Rx = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]])
|
||||||
|
Ry = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]])
|
||||||
|
Rz = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]])
|
||||||
|
|
||||||
|
return Rz @ Ry @ Rx
|
||||||
|
|
||||||
|
|
||||||
|
def rot2euler_single(rot):
|
||||||
|
"""
|
||||||
|
Convert a 3x3 rotation matrix to Euler angles (roll, pitch, yaw).
|
||||||
|
"""
|
||||||
|
return quat2euler_single(rot2quat_single(rot))
|
||||||
|
|
||||||
|
|
||||||
|
def rot_matrix(roll, pitch, yaw):
|
||||||
|
"""
|
||||||
|
Create a 3x3 rotation matrix from roll, pitch, and yaw angles.
|
||||||
|
"""
|
||||||
|
return euler2rot_single([roll, pitch, yaw])
|
||||||
|
|
||||||
|
|
||||||
|
def axis_angle_to_rot(axis, angle):
|
||||||
|
"""
|
||||||
|
Convert an axis-angle representation to a 3x3 rotation matrix.
|
||||||
|
"""
|
||||||
|
c = np.cos(angle / 2)
|
||||||
|
s = np.sin(angle / 2)
|
||||||
|
q = np.array([c, s*axis[0], s*axis[1], s*axis[2]])
|
||||||
|
return quat2rot_single(q)
|
||||||
|
|
||||||
|
|
||||||
|
class LocalCoord:
|
||||||
|
"""
|
||||||
|
A class to handle conversions between ECEF and local NED coordinates.
|
||||||
|
"""
|
||||||
|
def __init__(self, geodetic=None, ecef=None):
|
||||||
|
"""
|
||||||
|
Initialize LocalCoord with either geodetic or ECEF coordinates.
|
||||||
|
"""
|
||||||
|
if geodetic is not None:
|
||||||
|
self.init_ecef = geodetic2ecef_single(geodetic)
|
||||||
|
lat, lon, _ = geodetic
|
||||||
|
elif ecef is not None:
|
||||||
|
self.init_ecef = np.array(ecef)
|
||||||
|
lat, lon, _ = ecef2geodetic_single(ecef)
|
||||||
|
else:
|
||||||
|
raise ValueError("Must provide geodetic or ecef")
|
||||||
|
|
||||||
|
lat = np.radians(lat)
|
||||||
|
lon = np.radians(lon)
|
||||||
|
|
||||||
|
self.ned2ecef_matrix = np.array([
|
||||||
|
[-np.sin(lat) * np.cos(lon), -np.sin(lon), -np.cos(lat) * np.cos(lon)],
|
||||||
|
[-np.sin(lat) * np.sin(lon), np.cos(lon), -np.cos(lat) * np.sin(lon)],
|
||||||
|
[np.cos(lat), 0, -np.sin(lat)]
|
||||||
|
])
|
||||||
|
self.ecef2ned_matrix = self.ned2ecef_matrix.T
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_geodetic(cls, geodetic):
|
||||||
|
"""
|
||||||
|
Create a LocalCoord instance from geodetic coordinates.
|
||||||
|
"""
|
||||||
|
return cls(geodetic=geodetic)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_ecef(cls, ecef):
|
||||||
|
"""
|
||||||
|
Create a LocalCoord instance from ECEF coordinates.
|
||||||
|
"""
|
||||||
|
return cls(ecef=ecef)
|
||||||
|
|
||||||
|
def ecef2ned_single(self, ecef):
|
||||||
|
"""
|
||||||
|
Convert a single ECEF point to NED coordinates relative to the origin.
|
||||||
|
"""
|
||||||
|
return self.ecef2ned_matrix @ (ecef - self.init_ecef)
|
||||||
|
|
||||||
|
def ned2ecef_single(self, ned):
|
||||||
|
"""
|
||||||
|
Convert a single NED point to ECEF coordinates.
|
||||||
|
"""
|
||||||
|
return self.ned2ecef_matrix @ ned + self.init_ecef
|
||||||
|
|
||||||
|
def geodetic2ned_single(self, geodetic):
|
||||||
|
"""
|
||||||
|
Convert a single geodetic point to NED coordinates.
|
||||||
|
"""
|
||||||
|
ecef = geodetic2ecef_single(geodetic)
|
||||||
|
return self.ecef2ned_single(ecef)
|
||||||
|
|
||||||
|
def ned2geodetic_single(self, ned):
|
||||||
|
"""
|
||||||
|
Convert a single NED point to geodetic coordinates.
|
||||||
|
"""
|
||||||
|
ecef = self.ned2ecef_single(ned)
|
||||||
|
return ecef2geodetic_single(ecef)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ned_from_ecef_matrix(self):
|
||||||
|
"""
|
||||||
|
Returns the rotation matrix from ECEF to NED coordinates.
|
||||||
|
"""
|
||||||
|
return self.ecef2ned_matrix
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ecef_from_ned_matrix(self):
|
||||||
|
"""
|
||||||
|
Returns the rotation matrix from NED to ECEF coordinates.
|
||||||
|
"""
|
||||||
|
return self.ned2ecef_matrix
|
||||||
|
|
||||||
|
|
||||||
|
def ecef_euler_from_ned_single(ecef_init, ned_pose):
|
||||||
|
"""
|
||||||
|
Convert NED Euler angles (roll, pitch, yaw) at a given ECEF origin
|
||||||
|
to equivalent ECEF Euler angles.
|
||||||
|
"""
|
||||||
|
converter = LocalCoord(ecef=ecef_init)
|
||||||
|
zero = np.array(ecef_init)
|
||||||
|
|
||||||
|
x0 = converter.ned2ecef_single([1, 0, 0]) - zero
|
||||||
|
y0 = converter.ned2ecef_single([0, 1, 0]) - zero
|
||||||
|
z0 = converter.ned2ecef_single([0, 0, 1]) - zero
|
||||||
|
|
||||||
|
phi, theta, psi = ned_pose
|
||||||
|
|
||||||
|
x1 = axis_angle_to_rot(z0, psi) @ x0
|
||||||
|
y1 = axis_angle_to_rot(z0, psi) @ y0
|
||||||
|
z1 = axis_angle_to_rot(z0, psi) @ z0
|
||||||
|
|
||||||
|
x2 = axis_angle_to_rot(y1, theta) @ x1
|
||||||
|
y2 = axis_angle_to_rot(y1, theta) @ y1
|
||||||
|
z2 = axis_angle_to_rot(y1, theta) @ z1
|
||||||
|
|
||||||
|
x3 = axis_angle_to_rot(x2, phi) @ x2
|
||||||
|
y3 = axis_angle_to_rot(x2, phi) @ y2
|
||||||
|
|
||||||
|
x0 = np.array([1.0, 0, 0])
|
||||||
|
y0 = np.array([0, 1.0, 0])
|
||||||
|
z0 = np.array([0, 0, 1.0])
|
||||||
|
|
||||||
|
psi_out = np.arctan2(np.dot(x3, y0), np.dot(x3, x0))
|
||||||
|
theta_out = np.arctan2(-np.dot(x3, z0), np.sqrt(np.dot(x3, x0)**2 + np.dot(x3, y0)**2))
|
||||||
|
|
||||||
|
y2 = axis_angle_to_rot(z0, psi_out) @ y0
|
||||||
|
z2 = axis_angle_to_rot(y2, theta_out) @ z0
|
||||||
|
|
||||||
|
phi_out = np.arctan2(np.dot(y3, z2), np.dot(y3, y2))
|
||||||
|
|
||||||
|
return np.array([phi_out, theta_out, psi_out])
|
||||||
|
|
||||||
|
|
||||||
|
def ned_euler_from_ecef_single(ecef_init, ecef_pose):
|
||||||
|
"""
|
||||||
|
Convert ECEF Euler angles (roll, pitch, yaw) at a given ECEF origin
|
||||||
|
to equivalent NED Euler angles.
|
||||||
|
"""
|
||||||
|
converter = LocalCoord(ecef=ecef_init)
|
||||||
|
|
||||||
|
x0 = np.array([1.0, 0, 0])
|
||||||
|
y0 = np.array([0, 1.0, 0])
|
||||||
|
z0 = np.array([0, 0, 1.0])
|
||||||
|
|
||||||
|
phi, theta, psi = ecef_pose
|
||||||
|
|
||||||
|
x1 = axis_angle_to_rot(z0, psi) @ x0
|
||||||
|
y1 = axis_angle_to_rot(z0, psi) @ y0
|
||||||
|
z1 = axis_angle_to_rot(z0, psi) @ z0
|
||||||
|
|
||||||
|
x2 = axis_angle_to_rot(y1, theta) @ x1
|
||||||
|
y2 = axis_angle_to_rot(y1, theta) @ y1
|
||||||
|
z2 = axis_angle_to_rot(y1, theta) @ z1
|
||||||
|
|
||||||
|
x3 = axis_angle_to_rot(x2, phi) @ x2
|
||||||
|
y3 = axis_angle_to_rot(x2, phi) @ y2
|
||||||
|
|
||||||
|
zero = np.array(ecef_init)
|
||||||
|
x0 = converter.ned2ecef_single([1, 0, 0]) - zero
|
||||||
|
y0 = converter.ned2ecef_single([0, 1, 0]) - zero
|
||||||
|
z0 = converter.ned2ecef_single([0, 0, 1]) - zero
|
||||||
|
|
||||||
|
psi_out = np.arctan2(np.dot(x3, y0), np.dot(x3, x0))
|
||||||
|
theta_out = np.arctan2(-np.dot(x3, z0), np.sqrt(np.dot(x3, x0)**2 + np.dot(x3, y0)**2))
|
||||||
|
|
||||||
|
y2 = axis_angle_to_rot(z0, psi_out) @ y0
|
||||||
|
z2 = axis_angle_to_rot(y2, theta_out) @ z0
|
||||||
|
|
||||||
|
phi_out = np.arctan2(np.dot(y3, z2), np.dot(y3, y2))
|
||||||
|
|
||||||
|
return np.array([phi_out, theta_out, psi_out])
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# distutils: language = c++
|
|
||||||
# cython: language_level = 3
|
|
||||||
from openpilot.common.transformations.transformations cimport Matrix3, Vector3, Quaternion
|
|
||||||
from openpilot.common.transformations.transformations cimport ECEF, NED, Geodetic
|
|
||||||
|
|
||||||
from openpilot.common.transformations.transformations cimport euler2quat as euler2quat_c
|
|
||||||
from openpilot.common.transformations.transformations cimport quat2euler as quat2euler_c
|
|
||||||
from openpilot.common.transformations.transformations cimport quat2rot as quat2rot_c
|
|
||||||
from openpilot.common.transformations.transformations cimport rot2quat as rot2quat_c
|
|
||||||
from openpilot.common.transformations.transformations cimport euler2rot as euler2rot_c
|
|
||||||
from openpilot.common.transformations.transformations cimport rot2euler as rot2euler_c
|
|
||||||
from openpilot.common.transformations.transformations cimport rot_matrix as rot_matrix_c
|
|
||||||
from openpilot.common.transformations.transformations cimport ecef_euler_from_ned as ecef_euler_from_ned_c
|
|
||||||
from openpilot.common.transformations.transformations cimport ned_euler_from_ecef as ned_euler_from_ecef_c
|
|
||||||
from openpilot.common.transformations.transformations cimport geodetic2ecef as geodetic2ecef_c
|
|
||||||
from openpilot.common.transformations.transformations cimport ecef2geodetic as ecef2geodetic_c
|
|
||||||
from openpilot.common.transformations.transformations cimport LocalCoord_c
|
|
||||||
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
cimport numpy as np
|
|
||||||
|
|
||||||
cdef np.ndarray[double, ndim=2] matrix2numpy(Matrix3 m):
|
|
||||||
return np.array([
|
|
||||||
[m(0, 0), m(0, 1), m(0, 2)],
|
|
||||||
[m(1, 0), m(1, 1), m(1, 2)],
|
|
||||||
[m(2, 0), m(2, 1), m(2, 2)],
|
|
||||||
])
|
|
||||||
|
|
||||||
cdef Matrix3 numpy2matrix(np.ndarray[double, ndim=2, mode="fortran"] m):
|
|
||||||
assert m.shape[0] == 3
|
|
||||||
assert m.shape[1] == 3
|
|
||||||
return Matrix3(<double*>m.data)
|
|
||||||
|
|
||||||
cdef ECEF list2ecef(ecef):
|
|
||||||
cdef ECEF e
|
|
||||||
e.x = ecef[0]
|
|
||||||
e.y = ecef[1]
|
|
||||||
e.z = ecef[2]
|
|
||||||
return e
|
|
||||||
|
|
||||||
cdef NED list2ned(ned):
|
|
||||||
cdef NED n
|
|
||||||
n.n = ned[0]
|
|
||||||
n.e = ned[1]
|
|
||||||
n.d = ned[2]
|
|
||||||
return n
|
|
||||||
|
|
||||||
cdef Geodetic list2geodetic(geodetic):
|
|
||||||
cdef Geodetic g
|
|
||||||
g.lat = geodetic[0]
|
|
||||||
g.lon = geodetic[1]
|
|
||||||
g.alt = geodetic[2]
|
|
||||||
return g
|
|
||||||
|
|
||||||
def euler2quat_single(euler):
|
|
||||||
cdef Vector3 e = Vector3(euler[0], euler[1], euler[2])
|
|
||||||
cdef Quaternion q = euler2quat_c(e)
|
|
||||||
return [q.w(), q.x(), q.y(), q.z()]
|
|
||||||
|
|
||||||
def quat2euler_single(quat):
|
|
||||||
cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3])
|
|
||||||
cdef Vector3 e = quat2euler_c(q)
|
|
||||||
return [e(0), e(1), e(2)]
|
|
||||||
|
|
||||||
def quat2rot_single(quat):
|
|
||||||
cdef Quaternion q = Quaternion(quat[0], quat[1], quat[2], quat[3])
|
|
||||||
cdef Matrix3 r = quat2rot_c(q)
|
|
||||||
return matrix2numpy(r)
|
|
||||||
|
|
||||||
def rot2quat_single(rot):
|
|
||||||
cdef Matrix3 r = numpy2matrix(np.asfortranarray(rot, dtype=np.double))
|
|
||||||
cdef Quaternion q = rot2quat_c(r)
|
|
||||||
return [q.w(), q.x(), q.y(), q.z()]
|
|
||||||
|
|
||||||
def euler2rot_single(euler):
|
|
||||||
cdef Vector3 e = Vector3(euler[0], euler[1], euler[2])
|
|
||||||
cdef Matrix3 r = euler2rot_c(e)
|
|
||||||
return matrix2numpy(r)
|
|
||||||
|
|
||||||
def rot2euler_single(rot):
|
|
||||||
cdef Matrix3 r = numpy2matrix(np.asfortranarray(rot, dtype=np.double))
|
|
||||||
cdef Vector3 e = rot2euler_c(r)
|
|
||||||
return [e(0), e(1), e(2)]
|
|
||||||
|
|
||||||
def rot_matrix(roll, pitch, yaw):
|
|
||||||
return matrix2numpy(rot_matrix_c(roll, pitch, yaw))
|
|
||||||
|
|
||||||
def ecef_euler_from_ned_single(ecef_init, ned_pose):
|
|
||||||
cdef ECEF init = list2ecef(ecef_init)
|
|
||||||
cdef Vector3 pose = Vector3(ned_pose[0], ned_pose[1], ned_pose[2])
|
|
||||||
|
|
||||||
cdef Vector3 e = ecef_euler_from_ned_c(init, pose)
|
|
||||||
return [e(0), e(1), e(2)]
|
|
||||||
|
|
||||||
def ned_euler_from_ecef_single(ecef_init, ecef_pose):
|
|
||||||
cdef ECEF init = list2ecef(ecef_init)
|
|
||||||
cdef Vector3 pose = Vector3(ecef_pose[0], ecef_pose[1], ecef_pose[2])
|
|
||||||
|
|
||||||
cdef Vector3 e = ned_euler_from_ecef_c(init, pose)
|
|
||||||
return [e(0), e(1), e(2)]
|
|
||||||
|
|
||||||
def geodetic2ecef_single(geodetic):
|
|
||||||
cdef Geodetic g = list2geodetic(geodetic)
|
|
||||||
cdef ECEF e = geodetic2ecef_c(g)
|
|
||||||
return [e.x, e.y, e.z]
|
|
||||||
|
|
||||||
def ecef2geodetic_single(ecef):
|
|
||||||
cdef ECEF e = list2ecef(ecef)
|
|
||||||
cdef Geodetic g = ecef2geodetic_c(e)
|
|
||||||
return [g.lat, g.lon, g.alt]
|
|
||||||
|
|
||||||
|
|
||||||
cdef class LocalCoord:
|
|
||||||
cdef LocalCoord_c * lc
|
|
||||||
|
|
||||||
def __init__(self, geodetic=None, ecef=None):
|
|
||||||
assert (geodetic is not None) or (ecef is not None)
|
|
||||||
if geodetic is not None:
|
|
||||||
self.lc = new LocalCoord_c(list2geodetic(geodetic))
|
|
||||||
elif ecef is not None:
|
|
||||||
self.lc = new LocalCoord_c(list2ecef(ecef))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ned2ecef_matrix(self):
|
|
||||||
return matrix2numpy(self.lc.ned2ecef_matrix)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ecef2ned_matrix(self):
|
|
||||||
return matrix2numpy(self.lc.ecef2ned_matrix)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ned_from_ecef_matrix(self):
|
|
||||||
return self.ecef2ned_matrix
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ecef_from_ned_matrix(self):
|
|
||||||
return self.ned2ecef_matrix
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_geodetic(cls, geodetic):
|
|
||||||
return cls(geodetic=geodetic)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_ecef(cls, ecef):
|
|
||||||
return cls(ecef=ecef)
|
|
||||||
|
|
||||||
def ecef2ned_single(self, ecef):
|
|
||||||
assert self.lc
|
|
||||||
cdef ECEF e = list2ecef(ecef)
|
|
||||||
cdef NED n = self.lc.ecef2ned(e)
|
|
||||||
return [n.n, n.e, n.d]
|
|
||||||
|
|
||||||
def ned2ecef_single(self, ned):
|
|
||||||
assert self.lc
|
|
||||||
cdef NED n = list2ned(ned)
|
|
||||||
cdef ECEF e = self.lc.ned2ecef(n)
|
|
||||||
return [e.x, e.y, e.z]
|
|
||||||
|
|
||||||
def geodetic2ned_single(self, geodetic):
|
|
||||||
assert self.lc
|
|
||||||
cdef Geodetic g = list2geodetic(geodetic)
|
|
||||||
cdef NED n = self.lc.geodetic2ned(g)
|
|
||||||
return [n.n, n.e, n.d]
|
|
||||||
|
|
||||||
def ned2geodetic_single(self, ned):
|
|
||||||
assert self.lc
|
|
||||||
cdef NED n = list2ned(ned)
|
|
||||||
cdef Geodetic g = self.lc.ned2geodetic(n)
|
|
||||||
return [g.lat, g.lon, g.alt]
|
|
||||||
|
|
||||||
def __dealloc__(self):
|
|
||||||
del self.lc
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
def sudo_write(val: str, path: str) -> None:
|
|
||||||
try:
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(str(val))
|
|
||||||
except PermissionError:
|
|
||||||
os.system(f"sudo chmod a+w {path}")
|
|
||||||
try:
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(str(val))
|
|
||||||
except PermissionError:
|
|
||||||
# fallback for debugfs files
|
|
||||||
os.system(f"sudo su -c 'echo {val} > {path}'")
|
|
||||||
|
|
||||||
def sudo_read(path: str) -> str:
|
|
||||||
try:
|
|
||||||
return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip()
|
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
class MovingAverage:
|
|
||||||
def __init__(self, window_size: int):
|
|
||||||
self.window_size: int = window_size
|
|
||||||
self.buffer: list[float] = [0.0] * window_size
|
|
||||||
self.index: int = 0
|
|
||||||
self.count: int = 0
|
|
||||||
self.sum: float = 0.0
|
|
||||||
|
|
||||||
def add_value(self, new_value: float):
|
|
||||||
# Update the sum: subtract the value being replaced and add the new value
|
|
||||||
self.sum -= self.buffer[self.index]
|
|
||||||
self.buffer[self.index] = new_value
|
|
||||||
self.sum += new_value
|
|
||||||
|
|
||||||
# Update the index in a circular manner
|
|
||||||
self.index = (self.index + 1) % self.window_size
|
|
||||||
|
|
||||||
# Track the number of added values (for partial windows)
|
|
||||||
self.count = min(self.count + 1, self.window_size)
|
|
||||||
|
|
||||||
def get_average(self) -> float:
|
|
||||||
if self.count == 0:
|
|
||||||
return float('nan')
|
|
||||||
return self.sum / self.count
|
|
||||||
+50
-3
@@ -7,14 +7,61 @@ import time
|
|||||||
import functools
|
import functools
|
||||||
from subprocess import Popen, PIPE, TimeoutExpired
|
from subprocess import Popen, PIPE, TimeoutExpired
|
||||||
import zstandard as zstd
|
import zstandard as zstd
|
||||||
from openpilot.common.swaglog import cloudlog
|
|
||||||
|
|
||||||
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
|
LOG_COMPRESSION_LEVEL = 10 # little benefit up to level 15. level ~17 is a small step change
|
||||||
|
|
||||||
|
|
||||||
|
def sudo_write(val: str, path: str) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(str(val))
|
||||||
|
except PermissionError:
|
||||||
|
os.system(f"sudo chmod a+w {path}")
|
||||||
|
try:
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(str(val))
|
||||||
|
except PermissionError:
|
||||||
|
# fallback for debugfs files
|
||||||
|
os.system(f"sudo su -c 'echo {val} > {path}'")
|
||||||
|
|
||||||
|
|
||||||
|
def sudo_read(path: str) -> str:
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(f"sudo cat {path}", shell=True, encoding='utf8').strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class MovingAverage:
|
||||||
|
def __init__(self, window_size: int):
|
||||||
|
self.window_size: int = window_size
|
||||||
|
self.buffer: list[float] = [0.0] * window_size
|
||||||
|
self.index: int = 0
|
||||||
|
self.count: int = 0
|
||||||
|
self.sum: float = 0.0
|
||||||
|
|
||||||
|
def add_value(self, new_value: float):
|
||||||
|
# Update the sum: subtract the value being replaced and add the new value
|
||||||
|
self.sum -= self.buffer[self.index]
|
||||||
|
self.buffer[self.index] = new_value
|
||||||
|
self.sum += new_value
|
||||||
|
|
||||||
|
# Update the index in a circular manner
|
||||||
|
self.index = (self.index + 1) % self.window_size
|
||||||
|
|
||||||
|
# Track the number of added values (for partial windows)
|
||||||
|
self.count = min(self.count + 1, self.window_size)
|
||||||
|
|
||||||
|
def get_average(self) -> float:
|
||||||
|
if self.count == 0:
|
||||||
|
return float('nan')
|
||||||
|
return self.sum / self.count
|
||||||
|
|
||||||
|
|
||||||
class CallbackReader:
|
class CallbackReader:
|
||||||
"""Wraps a file, but overrides the read method to also
|
"""Wraps a file, but overrides the read method to also
|
||||||
call a callback function with the number of bytes read so far."""
|
call a callback function with the number of bytes read so far."""
|
||||||
|
|
||||||
def __init__(self, f, callback, *args):
|
def __init__(self, f, callback, *args):
|
||||||
self.f = f
|
self.f = f
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
@@ -107,11 +154,11 @@ def retry(attempts=3, delay=1.0, ignore_failure=False):
|
|||||||
try:
|
try:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
cloudlog.exception(f"{func.__name__} failed, trying again")
|
print(f"{func.__name__} failed, trying again")
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
if ignore_failure:
|
if ignore_failure:
|
||||||
cloudlog.error(f"{func.__name__} failed after retry")
|
print(f"{func.__name__} failed after retry")
|
||||||
else:
|
else:
|
||||||
raise Exception(f"{func.__name__} failed after retry")
|
raise Exception(f"{func.__name__} failed after retry")
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
#define COMMA_VERSION "0.10.3"
|
#define COMMA_VERSION "0.10.4"
|
||||||
|
|||||||
+135
-134
@@ -4,22 +4,23 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
# 325 Supported Cars
|
# 327 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|
|
|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 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|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 2025|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">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 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|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|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">Buy Here</a></sub></details>|||
|
|Acura|TLX 2021|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">Buy Here</a></sub></details>|||
|
||||||
|Audi|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 USB-C coupler<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>|||
|
|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|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 USB-C coupler<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|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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"></img></a>||
|
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Equinox 2019-22">Buy Here</a></sub></details>|||
|
|Chevrolet|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>|||
|
||||||
@@ -31,33 +32,33 @@ 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 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>|||
|
|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"></img></a>||
|
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|CUPRA|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 USB-C coupler<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|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>|||
|
|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|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer 2020-24">Buy Here</a></sub></details>|||
|
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
|Ford|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 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Focus 2018[<sup>3</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 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>3</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 Hybrid 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|
||||||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga 2020-23">Buy Here</a></sub></details>|||
|
|Ford|Kuga 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 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 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2020-23">Buy Here</a></sub></details>|||
|
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||||
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2022">Buy Here</a></sub></details>|||
|
|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2022">Buy Here</a></sub></details>|||
|
||||||
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2023-24">Buy Here</a></sub></details>|||
|
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2018">Buy Here</a></sub></details>|||
|
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2018">Buy Here</a></sub></details>|||
|
||||||
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2019-21">Buy Here</a></sub></details>|||
|
|Genesis|G70 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2019-21">Buy Here</a></sub></details>|||
|
||||||
|Genesis|G70 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2022-23">Buy Here</a></sub></details>|||
|
|Genesis|G70 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Genesis G70 2022-23">Buy Here</a></sub></details>|||
|
||||||
@@ -79,7 +80,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|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|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</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"></img></a>||
|
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>5</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
|Honda|Civic Hatchback 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 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>|||
|
||||||
@@ -91,7 +92,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|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</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 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</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-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 CR-V Hybrid 2023-25">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</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|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|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>|||
|
||||||
@@ -192,165 +193,165 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
||||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</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=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2019-25">Buy Here</a></sub></details>|||
|
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2019-25">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus GS F 2016">Buy Here</a></sub></details>|||
|
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus GS F 2016">Buy Here</a></sub></details>|||
|
||||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2017-19">Buy Here</a></sub></details>|||
|
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2022-24">Buy Here</a></sub></details>|||
|
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2022-24">Buy Here</a></sub></details>|||
|
||||||
|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus LC 2024-25">Buy Here</a></sub></details>|||
|
|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus LC 2024-25">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</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=Lexus NX 2018-19">Buy Here</a></sub></details>|||
|
|Lexus|LS 2018|All except Lexus Safety System+ A|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus LS 2018">Buy Here</a></sub></details>|||
|
||||||
|
|Lexus|NX 2018-19|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=Lexus NX 2018-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX 2020-21">Buy Here</a></sub></details>|||
|
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX 2020-21">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</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=Lexus NX Hybrid 2018-19">Buy Here</a></sub></details>|||
|
|Lexus|NX Hybrid 2018-19|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=Lexus NX Hybrid 2018-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX Hybrid 2020-21">Buy Here</a></sub></details>|||
|
|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX Hybrid 2020-21">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RC 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=Lexus RC 2018-20">Buy Here</a></sub></details>|||
|
|Lexus|RC 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=Lexus RC 2018-20">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RC 2023|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 RC 2023">Buy Here</a></sub></details>|||
|
|Lexus|RC 2023|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 RC 2023">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX 2016|Lexus Safety System+|openpilot available[<sup>2</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=Lexus RX 2016">Buy Here</a></sub></details>|||
|
|Lexus|RX 2016|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2016">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX 2017-19|All|openpilot available[<sup>2</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=Lexus RX 2017-19">Buy Here</a></sub></details>|||
|
|Lexus|RX 2017-19|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=Lexus RX 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2020-22">Buy Here</a></sub></details>|||
|
|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[<sup>2</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=Lexus RX Hybrid 2016">Buy Here</a></sub></details>|||
|
|Lexus|RX Hybrid 2016|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2016">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX Hybrid 2017-19|All|openpilot available[<sup>2</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=Lexus RX Hybrid 2017-19">Buy Here</a></sub></details>|||
|
|Lexus|RX Hybrid 2017-19|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=Lexus RX Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|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>|||
|
|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 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>|||
|
|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|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|MAN|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
|Mazda|CX-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"></img></a>||
|
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Nissan[<sup>6</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 USB-C coupler<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)|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>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 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Nissan[<sup>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Nissan[<sup>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 USB-C coupler<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)|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 USB-C coupler<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>5</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 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|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 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>|||
|
||||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|Rivian|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 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||||
|SEAT|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 USB-C coupler<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|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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>|||
|
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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>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"></img></a>||
|
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>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|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 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|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>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 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>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|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 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|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 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|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>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"></img></a>||
|
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>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>|||
|
|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 USB-C coupler<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|Fabia 2022-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</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 USB-C coupler<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|Kamiq 2021-23[<sup>11,13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</sup>](#footnotes)|||
|
||||||
|Škoda|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 USB-C coupler<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|Karoq 2019-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Kodiaq 2017-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Octavia 2015-19[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Octavia RS 2016[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Octavia Scout 2017-19[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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 USB-C coupler<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|Scala 2020-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</sup>](#footnotes)|||
|
||||||
|Škoda|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 USB-C coupler<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>|||
|
|Škoda|Superb 2015-22[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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 USB-C coupler<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 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>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 USB-C coupler<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 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>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 USB-C coupler<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 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>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 USB-C coupler<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>|||
|
|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>|||
|
||||||
|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 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|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|openpilot available[<sup>2</sup>](#footnotes)|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>|||
|
|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>|||
|
||||||
|Toyota|Avalon 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|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 2017-18">Buy Here</a></sub></details>|||
|
|Toyota|Avalon 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Avalon 2019-21|All|openpilot available[<sup>2</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 Avalon 2019-21">Buy Here</a></sub></details>|||
|
|Toyota|Avalon 2019-21|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 Avalon 2019-21">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Avalon 2022|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 Avalon 2022">Buy Here</a></sub></details>|||
|
|Toyota|Avalon 2022|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 Avalon 2022">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Avalon Hybrid 2019-21|All|openpilot available[<sup>2</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 Avalon Hybrid 2019-21">Buy Here</a></sub></details>|||
|
|Toyota|Avalon Hybrid 2019-21|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 Avalon Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Avalon Hybrid 2022|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 Avalon Hybrid 2022">Buy Here</a></sub></details>|||
|
|Toyota|Avalon Hybrid 2022|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 Avalon Hybrid 2022">Buy Here</a></sub></details>|||
|
||||||
|Toyota|C-HR 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 2017-20">Buy Here</a></sub></details>|||
|
|Toyota|C-HR 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 2017-20">Buy Here</a></sub></details>|||
|
||||||
|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 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 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|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>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"></img></a>||
|
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>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 2021-24|All|openpilot|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2021-24">Buy Here</a></sub></details>|||
|
|Toyota|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|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross (Non-US only) 2020-23">Buy Here</a></sub></details>|||
|
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross (Non-US only) 2020-23">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross Hybrid (Non-US only) 2020-22">Buy Here</a></sub></details>|||
|
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross Hybrid (Non-US only) 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid 2020-22">Buy Here</a></sub></details>|||
|
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid (South America only) 2020-23">Buy Here</a></sub></details>|||
|
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid (South America only) 2020-23">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Highlander 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2020-23">Buy Here</a></sub></details>|||
|
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2020-23">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Highlander Hybrid 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2020-23">Buy Here</a></sub></details>|||
|
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Mirai 2021">Buy Here</a></sub></details>|||
|
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Mirai 2021">Buy Here</a></sub></details>|||
|
||||||
|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[<sup>2</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 Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Prius 2017-20|All|openpilot available[<sup>2</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 Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Prius Prime 2017-20|All|openpilot available[<sup>2</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 Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2022">Buy Here</a></sub></details>|||
|
|Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2022">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2023-25">Buy Here</a></sub></details>|||
|
|Toyota|RAV4 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2023-25">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|||
|
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<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|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|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 USB-C coupler<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|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||||
|Volkswagen|Jetta 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 USB-C coupler<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 2018-23">Buy Here</a></sub></details>|||
|
|Volkswagen|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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 USB-C coupler<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|Passat 2015-22[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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 USB-C coupler<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 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</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 USB-C coupler<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|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</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 USB-C coupler<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|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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>15</sup>](#footnotes)|||
|
||||||
|Volkswagen|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 USB-C coupler<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|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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|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 USB-C coupler<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|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</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
|
### Footnotes
|
||||||
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`. <br />
|
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`. <br />
|
||||||
<sup>2</sup>By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace stock ACC. <b><i>NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b> <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>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>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>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>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>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>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>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>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>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>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||||
<sup>12</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
<sup>12</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <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>13</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <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>14</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>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>15</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>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 />
|
|
||||||
|
|
||||||
## Community Maintained Cars
|
## 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/).
|
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
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
|
|||||||
export QCOM_PRIORITY=12
|
export QCOM_PRIORITY=12
|
||||||
|
|
||||||
if [ -z "$AGNOS_VERSION" ]; then
|
if [ -z "$AGNOS_VERSION" ]; then
|
||||||
export AGNOS_VERSION="15.1"
|
export AGNOS_VERSION="16"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export STAGING_ROOT="/data/safe_staging"
|
export STAGING_ROOT="/data/safe_staging"
|
||||||
|
|||||||
+1
-1
Submodule msgq_repo updated: a16cf1f608...20f2493855
+1
-1
Submodule opendbc_repo updated: 6171d1a976...5ef95213e6
+1
-1
Submodule panda updated: 1ffad74f88...81615ad9d5
+45
-43
@@ -14,7 +14,7 @@ dependencies = [
|
|||||||
"pyserial", # pigeond + qcomgpsd
|
"pyserial", # pigeond + qcomgpsd
|
||||||
"requests", # many one-off uses
|
"requests", # many one-off uses
|
||||||
"sympy", # rednose + friends
|
"sympy", # rednose + friends
|
||||||
"crcmod", # cars + qcomgpsd
|
"crcmod-plus", # cars + qcomgpsd
|
||||||
"tqdm", # cars (fw_versions.py) on start + many one-off uses
|
"tqdm", # cars (fw_versions.py) on start + many one-off uses
|
||||||
|
|
||||||
# hardwared
|
# hardwared
|
||||||
@@ -49,7 +49,7 @@ dependencies = [
|
|||||||
# logging
|
# logging
|
||||||
"pyzmq",
|
"pyzmq",
|
||||||
"sentry-sdk",
|
"sentry-sdk",
|
||||||
"xattr", # used in place of 'os.getxattr' for macos compatibility
|
"xattr", # used in place of 'os.getxattr' for macOS compatibility
|
||||||
|
|
||||||
# athena
|
# athena
|
||||||
"PyJWT",
|
"PyJWT",
|
||||||
@@ -72,7 +72,7 @@ dependencies = [
|
|||||||
"zstandard",
|
"zstandard",
|
||||||
|
|
||||||
# ui
|
# ui
|
||||||
"raylib < 5.5.0.3", # TODO: unpin when they fix https://github.com/electronstudio/raylib-python-cffi/issues/186
|
"raylib > 5.5.0.3",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"mapbox-earcut",
|
"mapbox-earcut",
|
||||||
]
|
]
|
||||||
@@ -87,7 +87,7 @@ docs = [
|
|||||||
testing = [
|
testing = [
|
||||||
"coverage",
|
"coverage",
|
||||||
"hypothesis ==6.47.*",
|
"hypothesis ==6.47.*",
|
||||||
"mypy",
|
"ty",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-cpp",
|
"pytest-cpp",
|
||||||
"pytest-subtests",
|
"pytest-subtests",
|
||||||
@@ -107,15 +107,13 @@ dev = [
|
|||||||
"av",
|
"av",
|
||||||
"azure-identity",
|
"azure-identity",
|
||||||
"azure-storage-blob",
|
"azure-storage-blob",
|
||||||
"dbus-next", # TODO: remove once we moved everything to jeepney
|
|
||||||
"dictdiffer",
|
"dictdiffer",
|
||||||
"jeepney",
|
"jeepney",
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
"opencv-python-headless",
|
"opencv-python-headless",
|
||||||
"parameterized >=0.8, <0.9",
|
"parameterized >=0.8, <0.9",
|
||||||
"pyautogui",
|
"pyautogui",
|
||||||
"pygame",
|
"pyopencl",
|
||||||
"pyopencl; platform_machine != 'aarch64'", # broken on arm64
|
|
||||||
"pytools>=2025.1.6; platform_machine != 'aarch64'",
|
"pytools>=2025.1.6; platform_machine != 'aarch64'",
|
||||||
"pywinctl",
|
"pywinctl",
|
||||||
"pyprof2calltree",
|
"pyprof2calltree",
|
||||||
@@ -180,42 +178,6 @@ ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,w
|
|||||||
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
||||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
||||||
|
|
||||||
[tool.mypy]
|
|
||||||
python_version = "3.11"
|
|
||||||
exclude = [
|
|
||||||
"cereal/",
|
|
||||||
"msgq/",
|
|
||||||
"msgq_repo/",
|
|
||||||
"opendbc/",
|
|
||||||
"opendbc_repo/",
|
|
||||||
"panda/",
|
|
||||||
"rednose/",
|
|
||||||
"rednose_repo/",
|
|
||||||
"tinygrad/",
|
|
||||||
"tinygrad_repo/",
|
|
||||||
"teleoprtc/",
|
|
||||||
"teleoprtc_repo/",
|
|
||||||
"third_party/",
|
|
||||||
]
|
|
||||||
|
|
||||||
# third-party packages
|
|
||||||
ignore_missing_imports=true
|
|
||||||
|
|
||||||
# helpful warnings
|
|
||||||
warn_redundant_casts=true
|
|
||||||
warn_unreachable=true
|
|
||||||
warn_unused_ignores=true
|
|
||||||
|
|
||||||
# restrict dynamic typing
|
|
||||||
warn_return_any=true
|
|
||||||
|
|
||||||
# allow implicit optionals for default args
|
|
||||||
implicit_optional = true
|
|
||||||
|
|
||||||
local_partial_types=true
|
|
||||||
explicit_package_bases=true
|
|
||||||
disable_error_code = "annotation-unchecked"
|
|
||||||
|
|
||||||
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
|
# https://beta.ruff.rs/docs/configuration/#using-pyprojecttoml
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
indent-width = 2
|
indent-width = 2
|
||||||
@@ -274,3 +236,43 @@ lint.flake8-implicit-str-concat.allow-multiline = false
|
|||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "preserve"
|
quote-style = "preserve"
|
||||||
|
|
||||||
|
[tool.ty.src]
|
||||||
|
exclude = [
|
||||||
|
"cereal/",
|
||||||
|
"msgq/",
|
||||||
|
"msgq_repo/",
|
||||||
|
"opendbc/",
|
||||||
|
"opendbc_repo/",
|
||||||
|
"panda/",
|
||||||
|
"rednose/",
|
||||||
|
"rednose_repo/",
|
||||||
|
"tinygrad/",
|
||||||
|
"tinygrad_repo/",
|
||||||
|
"teleoprtc/",
|
||||||
|
"teleoprtc_repo/",
|
||||||
|
"third_party/",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ty.rules]
|
||||||
|
# Ignore unresolved imports for Cython-compiled modules (.pyx)
|
||||||
|
unresolved-import = "ignore"
|
||||||
|
# Ignore unresolved attributes - many from capnp and Cython modules
|
||||||
|
unresolved-attribute = "ignore"
|
||||||
|
# Ignore invalid method overrides - signature variance issues
|
||||||
|
invalid-method-override = "ignore"
|
||||||
|
# Ignore possibly-missing-attribute - too many false positives
|
||||||
|
possibly-missing-attribute = "ignore"
|
||||||
|
# Ignore invalid assignment - often intentional monkey-patching
|
||||||
|
invalid-assignment = "ignore"
|
||||||
|
# Ignore no-matching-overload - numpy/ctypes overload matching issues
|
||||||
|
no-matching-overload = "ignore"
|
||||||
|
# Ignore invalid-argument-type - many false positives from raylib, ctypes, numpy
|
||||||
|
invalid-argument-type = "ignore"
|
||||||
|
# Ignore call-non-callable - false positives from dynamic types
|
||||||
|
call-non-callable = "ignore"
|
||||||
|
# Ignore unsupported-operator - false positives from dynamic types
|
||||||
|
unsupported-operator = "ignore"
|
||||||
|
# Ignore not-subscriptable - false positives from dynamic types
|
||||||
|
not-subscriptable = "ignore"
|
||||||
|
# not-iterable errors are now fixed
|
||||||
|
|||||||
+8
-9
@@ -4,18 +4,17 @@
|
|||||||
## release checklist
|
## release checklist
|
||||||
|
|
||||||
### Go to staging
|
### Go to staging
|
||||||
- [ ] make a GitHub issue to track release
|
- [ ] make a GitHub issue to track release with this checklist
|
||||||
- [ ] create release master branch
|
- [ ] create release master branch
|
||||||
- [ ] update RELEASES.md
|
- [ ] create a branch from upstream master named `zerotentwo` for release `v0.10.2`
|
||||||
|
- [ ] revert risky commits (double check with autonomy team)
|
||||||
|
- [ ] push the new branch
|
||||||
|
- [ ] push to staging:
|
||||||
|
- [ ] make sure you are on the newly created release master branch (`zerotentwo`)
|
||||||
|
- [ ] run `BRANCH=devel-staging release/build_stripped.sh`. Jenkins will then automatically build staging on device, run `test_onroad` and update the staging branch
|
||||||
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
- [ ] bump version on master: `common/version.h` and `RELEASES.md`
|
||||||
- [ ] build new userdata partition from `release3-staging`
|
|
||||||
- [ ] post on Discord, tag `@release crew`
|
- [ ] post on Discord, tag `@release crew`
|
||||||
|
|
||||||
Updating staging:
|
|
||||||
1. either rebase on master or cherry-pick changes
|
|
||||||
2. run this to update: `BRANCH=devel-staging release/build_devel.sh`
|
|
||||||
3. build new userdata partition from `release3-staging`
|
|
||||||
|
|
||||||
### Go to release
|
### Go to release
|
||||||
- [ ] before going to release, test the following:
|
- [ ] before going to release, test the following:
|
||||||
- [ ] update from previous release -> new release
|
- [ ] update from previous release -> new release
|
||||||
@@ -26,7 +25,7 @@ Updating staging:
|
|||||||
- [ ] check sentry, MTBF, etc.
|
- [ ] check sentry, MTBF, etc.
|
||||||
- [ ] stress test passes in production
|
- [ ] stress test passes in production
|
||||||
- [ ] publish the blog post
|
- [ ] publish the blog post
|
||||||
- [ ] `git reset --hard origin/release3-staging`
|
- [ ] `git reset --hard origin/release-mici-staging`
|
||||||
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
- [ ] tag the release: `git tag v0.X.X <commit-hash> && git push origin v0.X.X`
|
||||||
- [ ] create GitHub release
|
- [ ] create GitHub release
|
||||||
- [ ] final test install on `openpilot.comma.ai`
|
- [ ] final test install on `openpilot.comma.ai`
|
||||||
|
|||||||
Executable
+209
@@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fetch CI results from GitHub Actions and Jenkins."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
JENKINS_URL = "https://jenkins.comma.life"
|
||||||
|
DEFAULT_TIMEOUT = 1800 # 30 minutes
|
||||||
|
POLL_INTERVAL = 30 # seconds
|
||||||
|
LOG_TAIL_LINES = 10 # lines of log to include for failed jobs
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_info():
|
||||||
|
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True).strip()
|
||||||
|
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
|
||||||
|
return branch, commit
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_actions_status(commit_sha):
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "run", "list", "--commit", commit_sha, "--workflow", "tests.yaml", "--json", "databaseId,status,conclusion"],
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
runs = json.loads(result.stdout)
|
||||||
|
if not runs:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
run_id = runs[0]["databaseId"]
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "run", "view", str(run_id), "--json", "jobs"],
|
||||||
|
capture_output=True, text=True, check=True
|
||||||
|
)
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
jobs = {job["name"]: {"status": job["status"], "conclusion": job["conclusion"],
|
||||||
|
"duration": format_duration(job) if job["conclusion"] not in ("skipped", None) and job.get("startedAt") else "",
|
||||||
|
"id": job["databaseId"]}
|
||||||
|
for job in data.get("jobs", [])}
|
||||||
|
return jobs, run_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_job_log(run_id, job_id):
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "run", "view", str(run_id), "--job", str(job_id), "--log-failed"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
lines = result.stdout.strip().split('\n')
|
||||||
|
return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def format_duration(job):
|
||||||
|
start = datetime.fromisoformat(job["startedAt"].replace("Z", "+00:00"))
|
||||||
|
end = datetime.fromisoformat(job["completedAt"].replace("Z", "+00:00"))
|
||||||
|
secs = int((end - start).total_seconds())
|
||||||
|
return f"{secs // 60}m {secs % 60}s"
|
||||||
|
|
||||||
|
|
||||||
|
def get_jenkins_status(branch, commit_sha):
|
||||||
|
base_url = f"{JENKINS_URL}/job/openpilot/job/{branch}"
|
||||||
|
try:
|
||||||
|
# Get list of recent builds
|
||||||
|
with urllib.request.urlopen(f"{base_url}/api/json?tree=builds[number,url]", timeout=10) as resp:
|
||||||
|
builds = json.loads(resp.read().decode()).get("builds", [])
|
||||||
|
|
||||||
|
# Find build matching commit
|
||||||
|
for build in builds[:20]: # check last 20 builds
|
||||||
|
with urllib.request.urlopen(f"{build['url']}api/json", timeout=10) as resp:
|
||||||
|
data = json.loads(resp.read().decode())
|
||||||
|
for action in data.get("actions", []):
|
||||||
|
if action.get("_class") == "hudson.plugins.git.util.BuildData":
|
||||||
|
build_sha = action.get("lastBuiltRevision", {}).get("SHA1", "")
|
||||||
|
if build_sha.startswith(commit_sha) or commit_sha.startswith(build_sha):
|
||||||
|
# Get stages info
|
||||||
|
stages = []
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f"{build['url']}wfapi/describe", timeout=10) as resp2:
|
||||||
|
wf_data = json.loads(resp2.read().decode())
|
||||||
|
stages = [{"name": s["name"], "status": s["status"]} for s in wf_data.get("stages", [])]
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"number": data["number"],
|
||||||
|
"in_progress": data.get("inProgress", False),
|
||||||
|
"result": data.get("result"),
|
||||||
|
"url": data.get("url", ""),
|
||||||
|
"stages": stages,
|
||||||
|
}
|
||||||
|
return None # no build found for this commit
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
return None # branch doesn't exist on Jenkins
|
||||||
|
|
||||||
|
|
||||||
|
def get_jenkins_log(build_url):
|
||||||
|
url = f"{build_url}consoleText"
|
||||||
|
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||||
|
text = resp.read().decode(errors='replace')
|
||||||
|
lines = text.strip().split('\n')
|
||||||
|
return '\n'.join(lines[-LOG_TAIL_LINES:]) if len(lines) > LOG_TAIL_LINES else text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_complete(gh_status, jenkins_status):
|
||||||
|
gh_done = gh_status is None or all(j["status"] == "completed" for j in gh_status.values())
|
||||||
|
jenkins_done = jenkins_status is None or not jenkins_status.get("in_progress", True)
|
||||||
|
return gh_done and jenkins_done
|
||||||
|
|
||||||
|
|
||||||
|
def status_icon(status, conclusion=None):
|
||||||
|
if status == "completed":
|
||||||
|
return ":white_check_mark:" if conclusion == "success" else ":x:"
|
||||||
|
return ":hourglass:" if status == "in_progress" else ":grey_question:"
|
||||||
|
|
||||||
|
|
||||||
|
def format_markdown(gh_status, gh_run_id, jenkins_status, commit_sha, branch):
|
||||||
|
lines = ["# CI Results", "",
|
||||||
|
f"**Branch**: {branch}",
|
||||||
|
f"**Commit**: {commit_sha[:7]}",
|
||||||
|
f"**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ""]
|
||||||
|
|
||||||
|
lines.extend(["## GitHub Actions", "", "| Job | Status | Duration |", "|-----|--------|----------|"])
|
||||||
|
failed_gh_jobs = []
|
||||||
|
if gh_status:
|
||||||
|
for job_name, job in gh_status.items():
|
||||||
|
icon = status_icon(job["status"], job.get("conclusion"))
|
||||||
|
conclusion = job.get("conclusion") or job["status"]
|
||||||
|
lines.append(f"| {job_name} | {icon} {conclusion} | {job.get('duration', '')} |")
|
||||||
|
if job.get("conclusion") == "failure":
|
||||||
|
failed_gh_jobs.append((job_name, job.get("id")))
|
||||||
|
else:
|
||||||
|
lines.append("| - | No workflow runs found | |")
|
||||||
|
|
||||||
|
lines.extend(["", "## Jenkins", "", "| Stage | Status |", "|-------|--------|"])
|
||||||
|
failed_jenkins_stages = []
|
||||||
|
if jenkins_status:
|
||||||
|
stages = jenkins_status.get("stages", [])
|
||||||
|
if stages:
|
||||||
|
for stage in stages:
|
||||||
|
icon = ":white_check_mark:" if stage["status"] == "SUCCESS" else (
|
||||||
|
":x:" if stage["status"] == "FAILED" else ":hourglass:")
|
||||||
|
lines.append(f"| {stage['name']} | {icon} {stage['status'].lower()} |")
|
||||||
|
if stage["status"] == "FAILED":
|
||||||
|
failed_jenkins_stages.append(stage["name"])
|
||||||
|
else:
|
||||||
|
icon = ":hourglass:" if jenkins_status["in_progress"] else (
|
||||||
|
":white_check_mark:" if jenkins_status["result"] == "SUCCESS" else ":x:")
|
||||||
|
status = "in progress" if jenkins_status["in_progress"] else (jenkins_status["result"] or "unknown")
|
||||||
|
lines.append(f"| #{jenkins_status['number']} | {icon} {status.lower()} |")
|
||||||
|
if jenkins_status.get("url"):
|
||||||
|
lines.append(f"\n[View build]({jenkins_status['url']})")
|
||||||
|
else:
|
||||||
|
lines.append("| - | No builds found for branch |")
|
||||||
|
|
||||||
|
if failed_gh_jobs or failed_jenkins_stages:
|
||||||
|
lines.extend(["", "## Failure Logs", ""])
|
||||||
|
|
||||||
|
for job_name, job_id in failed_gh_jobs:
|
||||||
|
lines.append(f"### GitHub Actions: {job_name}")
|
||||||
|
log = get_github_job_log(gh_run_id, job_id)
|
||||||
|
lines.extend(["", "```", log, "```", ""])
|
||||||
|
|
||||||
|
for stage_name in failed_jenkins_stages:
|
||||||
|
lines.append(f"### Jenkins: {stage_name}")
|
||||||
|
log = get_jenkins_log(jenkins_status["url"])
|
||||||
|
lines.extend(["", "```", log, "```", ""])
|
||||||
|
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Fetch CI results from GitHub Actions and Jenkins")
|
||||||
|
parser.add_argument("--wait", action="store_true", help="Wait for CI to complete")
|
||||||
|
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout in seconds (default: 1800)")
|
||||||
|
parser.add_argument("-o", "--output", default="ci_results.md", help="Output file (default: ci_results.md)")
|
||||||
|
parser.add_argument("--branch", help="Branch to check (default: current branch)")
|
||||||
|
parser.add_argument("--commit", help="Commit SHA to check (default: HEAD)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
branch, commit = get_git_info()
|
||||||
|
branch = args.branch or branch
|
||||||
|
commit = args.commit or commit
|
||||||
|
print(f"Fetching CI results for {branch} @ {commit[:7]}")
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while True:
|
||||||
|
gh_status, gh_run_id = get_github_actions_status(commit)
|
||||||
|
jenkins_status = get_jenkins_status(branch, commit) if branch != "HEAD" else None
|
||||||
|
|
||||||
|
if not args.wait or is_complete(gh_status, jenkins_status):
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed = time.monotonic() - start_time
|
||||||
|
if elapsed >= args.timeout:
|
||||||
|
print(f"Timeout after {int(elapsed)}s")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"CI still running, waiting {POLL_INTERVAL}s... ({int(elapsed)}s elapsed)")
|
||||||
|
time.sleep(POLL_INTERVAL)
|
||||||
|
|
||||||
|
content = format_markdown(gh_status, gh_run_id, jenkins_status, commit, branch)
|
||||||
|
with open(args.output, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f"Results written to {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -55,7 +55,7 @@ function run_tests() {
|
|||||||
run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES
|
run "check_nomerge_comments" $DIR/check_nomerge_comments.sh $ALL_FILES
|
||||||
|
|
||||||
if [[ -z "$FAST" ]]; then
|
if [[ -z "$FAST" ]]; then
|
||||||
run "mypy" mypy $PYTHON_FILES
|
run "ty" ty check
|
||||||
run "codespell" codespell $ALL_FILES
|
run "codespell" codespell $ALL_FILES
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ function help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}${UNDERLINE}Tests:${NC}"
|
echo -e "${BOLD}${UNDERLINE}Tests:${NC}"
|
||||||
echo -e " ${BOLD}ruff${NC}"
|
echo -e " ${BOLD}ruff${NC}"
|
||||||
echo -e " ${BOLD}mypy${NC}"
|
echo -e " ${BOLD}ty${NC}"
|
||||||
echo -e " ${BOLD}codespell${NC}"
|
echo -e " ${BOLD}codespell${NC}"
|
||||||
echo -e " ${BOLD}check_added_large_files${NC}"
|
echo -e " ${BOLD}check_added_large_files${NC}"
|
||||||
echo -e " ${BOLD}check_shebang_scripts_are_executable${NC}"
|
echo -e " ${BOLD}check_shebang_scripts_are_executable${NC}"
|
||||||
@@ -81,11 +81,11 @@ function help() {
|
|||||||
echo " Specify tests to skip separated by spaces"
|
echo " Specify tests to skip separated by spaces"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BOLD}${UNDERLINE}Examples:${NC}"
|
echo -e "${BOLD}${UNDERLINE}Examples:${NC}"
|
||||||
echo " op lint mypy ruff"
|
echo " op lint ty ruff"
|
||||||
echo " Only run the mypy and ruff tests"
|
echo " Only run the ty and ruff tests"
|
||||||
echo ""
|
echo ""
|
||||||
echo " op lint --skip mypy ruff"
|
echo " op lint --skip ty ruff"
|
||||||
echo " Skip the mypy and ruff tests"
|
echo " Skip the ty and ruff tests"
|
||||||
echo ""
|
echo ""
|
||||||
echo " op lint"
|
echo " op lint"
|
||||||
echo " Run all the tests"
|
echo " Run all the tests"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f27352a18194a1c819e9eaea89cfc11d2964402df0a28efa3ba60ae2d972fe67
|
oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78
|
||||||
size 13108
|
size 23549
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:25d66e42a28a3367eb40724d28652889089aa762438b475645269e0319c46009
|
oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1
|
||||||
size 1431
|
size 1746
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5
|
||||||
|
size 809
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886
|
||||||
|
size 1388
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd
|
||||||
|
size 11855
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6ed258d8e0531c19705953ded065c6d5e14929728a2909d8d4e335898fa5d080
|
||||||
|
size 4056
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
from cereal import car, log
|
from cereal import car, log
|
||||||
import cereal.messaging as messaging
|
|
||||||
from opendbc.car import DT_CTRL, structs
|
from opendbc.car import DT_CTRL, structs
|
||||||
|
from opendbc.car.car_helpers import interfaces
|
||||||
from opendbc.car.interfaces import MAX_CTRL_SPEED
|
from opendbc.car.interfaces import MAX_CTRL_SPEED
|
||||||
|
from opendbc.car.toyota.values import ToyotaFlags
|
||||||
|
|
||||||
from openpilot.selfdrive.selfdrived.events import Events
|
from openpilot.selfdrive.selfdrived.events import Events
|
||||||
|
|
||||||
@@ -11,33 +12,6 @@ EventName = log.OnroadEvent.EventName
|
|||||||
NetworkLocation = structs.CarParams.NetworkLocation
|
NetworkLocation = structs.CarParams.NetworkLocation
|
||||||
|
|
||||||
|
|
||||||
# TODO: the goal is to abstract this file into the CarState struct and make events generic
|
|
||||||
class MockCarState:
|
|
||||||
def __init__(self):
|
|
||||||
self.sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal'])
|
|
||||||
|
|
||||||
def update(self, CS: car.CarState):
|
|
||||||
self.sm.update(0)
|
|
||||||
gps_sock = 'gpsLocationExternal' if self.sm.recv_frame['gpsLocationExternal'] > 1 else 'gpsLocation'
|
|
||||||
|
|
||||||
CS.vEgo = self.sm[gps_sock].speed
|
|
||||||
CS.vEgoRaw = self.sm[gps_sock].speed
|
|
||||||
|
|
||||||
return CS
|
|
||||||
|
|
||||||
|
|
||||||
BRAND_EXTRA_GEARS = {
|
|
||||||
'ford': [GearShifter.low, GearShifter.manumatic],
|
|
||||||
'nissan': [GearShifter.brake],
|
|
||||||
'chrysler': [GearShifter.low],
|
|
||||||
'honda': [GearShifter.sport],
|
|
||||||
'toyota': [GearShifter.sport],
|
|
||||||
'gm': [GearShifter.sport, GearShifter.low, GearShifter.eco, GearShifter.manumatic],
|
|
||||||
'volkswagen': [GearShifter.eco, GearShifter.sport, GearShifter.manumatic],
|
|
||||||
'hyundai': [GearShifter.sport, GearShifter.manumatic]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CarSpecificEvents:
|
class CarSpecificEvents:
|
||||||
def __init__(self, CP: structs.CarParams):
|
def __init__(self, CP: structs.CarParams):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
@@ -48,14 +22,12 @@ class CarSpecificEvents:
|
|||||||
self.silent_steer_warning = True
|
self.silent_steer_warning = True
|
||||||
|
|
||||||
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
|
def update(self, CS: car.CarState, CS_prev: car.CarState, CC: car.CarControl):
|
||||||
extra_gears = BRAND_EXTRA_GEARS.get(self.CP.brand, None)
|
|
||||||
|
|
||||||
if self.CP.brand in ('body', 'mock'):
|
if self.CP.brand in ('body', 'mock'):
|
||||||
events = Events()
|
return Events()
|
||||||
|
|
||||||
elif self.CP.brand == 'chrysler':
|
events = self.create_common_events(CS, CS_prev)
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
|
||||||
|
|
||||||
|
if self.CP.brand == 'chrysler':
|
||||||
# Low speed steer alert hysteresis logic
|
# Low speed steer alert hysteresis logic
|
||||||
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
|
if self.CP.minSteerSpeed > 0. and CS.vEgo < (self.CP.minSteerSpeed + 0.5):
|
||||||
self.low_speed_alert = True
|
self.low_speed_alert = True
|
||||||
@@ -65,8 +37,6 @@ class CarSpecificEvents:
|
|||||||
events.add(EventName.belowSteerSpeed)
|
events.add(EventName.belowSteerSpeed)
|
||||||
|
|
||||||
elif self.CP.brand == 'honda':
|
elif self.CP.brand == 'honda':
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=False)
|
|
||||||
|
|
||||||
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
|
if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed:
|
||||||
events.add(EventName.belowEngageSpeed)
|
events.add(EventName.belowEngageSpeed)
|
||||||
|
|
||||||
@@ -87,10 +57,9 @@ class CarSpecificEvents:
|
|||||||
|
|
||||||
elif self.CP.brand == 'toyota':
|
elif self.CP.brand == 'toyota':
|
||||||
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
# TODO: when we check for unexpected disengagement, check gear not S1, S2, S3
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
|
||||||
|
|
||||||
if self.CP.openpilotLongitudinalControl:
|
if self.CP.openpilotLongitudinalControl:
|
||||||
if CS.cruiseState.standstill and not CS.brakePressed:
|
# Only can leave standstill when planner wants to move
|
||||||
|
if CS.cruiseState.standstill and not CS.brakePressed and (CC.cruiseControl.resume or self.CP.flags & ToyotaFlags.HYBRID.value):
|
||||||
events.add(EventName.resumeRequired)
|
events.add(EventName.resumeRequired)
|
||||||
if CS.vEgo < self.CP.minEnableSpeed:
|
if CS.vEgo < self.CP.minEnableSpeed:
|
||||||
events.add(EventName.belowEngageSpeed)
|
events.add(EventName.belowEngageSpeed)
|
||||||
@@ -102,8 +71,6 @@ class CarSpecificEvents:
|
|||||||
events.add(EventName.manualRestart)
|
events.add(EventName.manualRestart)
|
||||||
|
|
||||||
elif self.CP.brand == 'gm':
|
elif self.CP.brand == 'gm':
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
|
||||||
|
|
||||||
# Enabling at a standstill with brake is allowed
|
# Enabling at a standstill with brake is allowed
|
||||||
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
|
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs
|
||||||
if CS.vEgo < self.CP.minEnableSpeed and not (CS.standstill and CS.brake >= 20 and
|
if CS.vEgo < self.CP.minEnableSpeed and not (CS.standstill and CS.brake >= 20 and
|
||||||
@@ -113,8 +80,6 @@ class CarSpecificEvents:
|
|||||||
events.add(EventName.resumeRequired)
|
events.add(EventName.resumeRequired)
|
||||||
|
|
||||||
elif self.CP.brand == 'volkswagen':
|
elif self.CP.brand == 'volkswagen':
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise)
|
|
||||||
|
|
||||||
if self.CP.openpilotLongitudinalControl:
|
if self.CP.openpilotLongitudinalControl:
|
||||||
if CS.vEgo < self.CP.minEnableSpeed + 0.5:
|
if CS.vEgo < self.CP.minEnableSpeed + 0.5:
|
||||||
events.add(EventName.belowEngageSpeed)
|
events.add(EventName.belowEngageSpeed)
|
||||||
@@ -122,27 +87,26 @@ class CarSpecificEvents:
|
|||||||
events.add(EventName.speedTooLow)
|
events.add(EventName.speedTooLow)
|
||||||
|
|
||||||
# TODO: this needs to be implemented generically in carState struct
|
# TODO: this needs to be implemented generically in carState struct
|
||||||
# if CC.eps_timer_soft_disable_alert: # type: ignore[attr-defined]
|
# if CC.eps_timer_soft_disable_alert:
|
||||||
# events.add(EventName.steerTimeLimit)
|
# events.add(EventName.steerTimeLimit)
|
||||||
|
|
||||||
elif self.CP.brand == 'hyundai':
|
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears, pcm_enable=self.CP.pcmCruise, allow_button_cancel=False)
|
|
||||||
|
|
||||||
else:
|
|
||||||
events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears)
|
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState, extra_gears: list | None = None, pcm_enable=True,
|
def create_common_events(self, CS: structs.CarState, CS_prev: car.CarState):
|
||||||
allow_button_cancel=True):
|
|
||||||
events = Events()
|
events = Events()
|
||||||
|
|
||||||
|
CI = interfaces[self.CP.carFingerprint]
|
||||||
|
# TODO: cleanup the honda-specific logic
|
||||||
|
pcm_enable = self.CP.pcmCruise and self.CP.brand != 'honda'
|
||||||
|
# TODO: on some hyundai cars, the cancel button is also the pause/resume button,
|
||||||
|
# so only use it for cancel when running openpilot longitudinal
|
||||||
|
allow_button_cancel = self.CP.brand != 'hyundai'
|
||||||
|
|
||||||
if CS.doorOpen:
|
if CS.doorOpen:
|
||||||
events.add(EventName.doorOpen)
|
events.add(EventName.doorOpen)
|
||||||
if CS.seatbeltUnlatched:
|
if CS.seatbeltUnlatched:
|
||||||
events.add(EventName.seatbeltNotLatched)
|
events.add(EventName.seatbeltNotLatched)
|
||||||
if CS.gearShifter != GearShifter.drive and (extra_gears is None or
|
if CS.gearShifter != GearShifter.drive and CS.gearShifter not in CI.DRIVABLE_GEARS:
|
||||||
CS.gearShifter not in extra_gears):
|
|
||||||
events.add(EventName.wrongGear)
|
events.add(EventName.wrongGear)
|
||||||
if CS.gearShifter == GearShifter.reverse:
|
if CS.gearShifter == GearShifter.reverse:
|
||||||
events.add(EventName.reverseGear)
|
events.add(EventName.reverseGear)
|
||||||
@@ -156,6 +120,8 @@ class CarSpecificEvents:
|
|||||||
events.add(EventName.stockFcw)
|
events.add(EventName.stockFcw)
|
||||||
if CS.stockAeb:
|
if CS.stockAeb:
|
||||||
events.add(EventName.stockAeb)
|
events.add(EventName.stockAeb)
|
||||||
|
if CS.stockLkas:
|
||||||
|
events.add(EventName.stockLkas)
|
||||||
if CS.vEgo > MAX_CTRL_SPEED:
|
if CS.vEgo > MAX_CTRL_SPEED:
|
||||||
events.add(EventName.speedTooHigh)
|
events.add(EventName.speedTooHigh)
|
||||||
if CS.cruiseState.nonAdaptive:
|
if CS.cruiseState.nonAdaptive:
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from opendbc.car.car_helpers import get_car, interfaces
|
|||||||
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
from opendbc.car.interfaces import CarInterfaceBase, RadarInterfaceBase
|
||||||
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
from openpilot.selfdrive.pandad import can_capnp_to_list, can_list_to_can_capnp
|
||||||
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
from openpilot.selfdrive.car.cruise import VCruiseHelper
|
||||||
from openpilot.selfdrive.car.car_specific import MockCarState
|
|
||||||
|
|
||||||
REPLAY = "REPLAY" in os.environ
|
REPLAY = "REPLAY" in os.environ
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ class Car:
|
|||||||
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
|
safety_config.safetyModel = structs.CarParams.SafetyModel.noOutput
|
||||||
self.CP.safetyConfigs = [safety_config]
|
self.CP.safetyConfigs = [safety_config]
|
||||||
|
|
||||||
if self.CP.secOcRequired and not is_release:
|
if self.CP.secOcRequired:
|
||||||
# Copy user key if available
|
# Copy user key if available
|
||||||
try:
|
try:
|
||||||
with open("/cache/params/SecOCKey") as f:
|
with open("/cache/params/SecOCKey") as f:
|
||||||
@@ -150,7 +149,6 @@ class Car:
|
|||||||
self.params.put_nonblocking("CarParamsCache", cp_bytes)
|
self.params.put_nonblocking("CarParamsCache", cp_bytes)
|
||||||
self.params.put_nonblocking("CarParamsPersistent", cp_bytes)
|
self.params.put_nonblocking("CarParamsPersistent", cp_bytes)
|
||||||
|
|
||||||
self.mock_carstate = MockCarState()
|
|
||||||
self.v_cruise_helper = VCruiseHelper(self.CP)
|
self.v_cruise_helper = VCruiseHelper(self.CP)
|
||||||
|
|
||||||
self.is_metric = self.params.get_bool("IsMetric")
|
self.is_metric = self.params.get_bool("IsMetric")
|
||||||
@@ -167,8 +165,6 @@ class Car:
|
|||||||
|
|
||||||
# Update carState from CAN
|
# Update carState from CAN
|
||||||
CS = self.CI.update(can_list)
|
CS = self.CI.update(can_list)
|
||||||
if self.CP.brand == 'mock':
|
|
||||||
CS = self.mock_carstate.update(CS)
|
|
||||||
|
|
||||||
# Update radar tracks from CAN
|
# Update radar tracks from CAN
|
||||||
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
RD: structs.RadarDataT | None = self.RI.update(can_list)
|
||||||
|
|||||||
@@ -59,34 +59,35 @@ class LatControlTorque(LatControl):
|
|||||||
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay):
|
def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, curvature_limited, lat_delay):
|
||||||
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
pid_log = log.ControlsState.LateralTorqueState.new_message()
|
||||||
pid_log.version = VERSION
|
pid_log.version = VERSION
|
||||||
|
measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
||||||
|
measurement = measured_curvature * CS.vEgo ** 2
|
||||||
|
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
||||||
|
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
||||||
|
|
||||||
|
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
|
||||||
|
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
|
||||||
|
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
|
||||||
|
|
||||||
|
delay_frames = int(np.clip(lat_delay / self.dt + 1, 1, self.lat_accel_request_buffer_len))
|
||||||
|
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
|
||||||
|
setpoint = expected_lateral_accel
|
||||||
|
error = setpoint - measurement
|
||||||
|
|
||||||
|
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))
|
||||||
|
raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt)
|
||||||
|
desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk)
|
||||||
|
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
||||||
|
ff = gravity_adjusted_future_lateral_accel
|
||||||
|
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
||||||
|
ff -= self.torque_params.latAccelOffset
|
||||||
|
ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||||
|
|
||||||
if not active:
|
if not active:
|
||||||
output_torque = 0.0
|
output_torque = 0.0
|
||||||
pid_log.active = False
|
pid_log.active = False
|
||||||
else:
|
else:
|
||||||
measured_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll)
|
|
||||||
roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY
|
|
||||||
curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0))
|
|
||||||
lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2
|
|
||||||
|
|
||||||
delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len))
|
|
||||||
expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames]
|
|
||||||
lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2))
|
|
||||||
raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt)
|
|
||||||
desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk)
|
|
||||||
future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2
|
|
||||||
self.lat_accel_request_buffer.append(future_desired_lateral_accel)
|
|
||||||
gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation
|
|
||||||
setpoint = expected_lateral_accel
|
|
||||||
|
|
||||||
measurement = measured_curvature * CS.vEgo ** 2
|
|
||||||
error = setpoint - measurement
|
|
||||||
|
|
||||||
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
# do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly
|
||||||
pid_log.error = float(error)
|
pid_log.error = float(error)
|
||||||
ff = gravity_adjusted_future_lateral_accel
|
|
||||||
# latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll
|
|
||||||
ff -= self.torque_params.latAccelOffset
|
|
||||||
ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
|
||||||
|
|
||||||
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||||
output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator)
|
output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'pandad_python', 'np_version')
|
Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version')
|
||||||
|
|
||||||
gen = "c_generated_code"
|
gen = "c_generated_code"
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ lenv.Clean(generated_files, Dir(gen))
|
|||||||
generated_long = lenv.Command(generated_files,
|
generated_long = lenv.Command(generated_files,
|
||||||
source_list,
|
source_list,
|
||||||
f"cd {Dir('.').abspath} && python3 long_mpc.py")
|
f"cd {Dir('.').abspath} && python3 long_mpc.py")
|
||||||
lenv.Depends(generated_long, [msgq_python, common_python, pandad_python])
|
lenv.Depends(generated_long, [msgq_python, common_python])
|
||||||
|
|
||||||
lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES")
|
lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES")
|
||||||
lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES")
|
lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ X_EGO_OBSTACLE_COST = 3.
|
|||||||
X_EGO_COST = 0.
|
X_EGO_COST = 0.
|
||||||
V_EGO_COST = 0.
|
V_EGO_COST = 0.
|
||||||
A_EGO_COST = 0.
|
A_EGO_COST = 0.
|
||||||
J_EGO_COST = 5.0
|
J_EGO_COST = 5.
|
||||||
A_CHANGE_COST = 200.
|
A_CHANGE_COST = 200.
|
||||||
DANGER_ZONE_COST = 100.
|
DANGER_ZONE_COST = 100.
|
||||||
CRASH_DISTANCE = .25
|
CRASH_DISTANCE = .25
|
||||||
@@ -43,7 +43,6 @@ LEAD_DANGER_FACTOR = 0.75
|
|||||||
LIMIT_COST = 1e6
|
LIMIT_COST = 1e6
|
||||||
ACADOS_SOLVER_TYPE = 'SQP_RTI'
|
ACADOS_SOLVER_TYPE = 'SQP_RTI'
|
||||||
|
|
||||||
|
|
||||||
# Fewer timestamps don't hurt performance and lead to
|
# Fewer timestamps don't hurt performance and lead to
|
||||||
# much better convergence of the MPC with low iterations
|
# much better convergence of the MPC with low iterations
|
||||||
N = 12
|
N = 12
|
||||||
@@ -57,6 +56,7 @@ COMFORT_BRAKE = 2.5
|
|||||||
STOP_DISTANCE = 6.0
|
STOP_DISTANCE = 6.0
|
||||||
CRUISE_MIN_ACCEL = -1.2
|
CRUISE_MIN_ACCEL = -1.2
|
||||||
CRUISE_MAX_ACCEL = 1.6
|
CRUISE_MAX_ACCEL = 1.6
|
||||||
|
MIN_X_LEAD_FACTOR = 0.5
|
||||||
|
|
||||||
def get_jerk_factor(personality=log.LongitudinalPersonality.standard):
|
def get_jerk_factor(personality=log.LongitudinalPersonality.standard):
|
||||||
if personality==log.LongitudinalPersonality.relaxed:
|
if personality==log.LongitudinalPersonality.relaxed:
|
||||||
@@ -85,20 +85,12 @@ def get_stopped_equivalence_factor(v_lead):
|
|||||||
def get_safe_obstacle_distance(v_ego, t_follow):
|
def get_safe_obstacle_distance(v_ego, t_follow):
|
||||||
return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE
|
return (v_ego**2) / (2 * COMFORT_BRAKE) + t_follow * v_ego + STOP_DISTANCE
|
||||||
|
|
||||||
def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
|
||||||
if t_follow is None:
|
|
||||||
t_follow = get_T_FOLLOW()
|
|
||||||
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_long_model():
|
def gen_long_model():
|
||||||
model = AcadosModel()
|
model = AcadosModel()
|
||||||
model.name = MODEL_NAME
|
model.name = MODEL_NAME
|
||||||
|
|
||||||
# set up states & controls
|
# states
|
||||||
x_ego = SX.sym('x_ego')
|
x_ego, v_ego, a_ego = SX.sym('x_ego'), SX.sym('v_ego'), SX.sym('a_ego')
|
||||||
v_ego = SX.sym('v_ego')
|
|
||||||
a_ego = SX.sym('a_ego')
|
|
||||||
model.x = vertcat(x_ego, v_ego, a_ego)
|
model.x = vertcat(x_ego, v_ego, a_ego)
|
||||||
|
|
||||||
# controls
|
# controls
|
||||||
@@ -126,7 +118,6 @@ def gen_long_model():
|
|||||||
model.f_expl_expr = f_expl
|
model.f_expl_expr = f_expl
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
def gen_long_ocp():
|
def gen_long_ocp():
|
||||||
ocp = AcadosOcp()
|
ocp = AcadosOcp()
|
||||||
ocp.model = gen_long_model()
|
ocp.model = gen_long_model()
|
||||||
@@ -222,30 +213,31 @@ def gen_long_ocp():
|
|||||||
|
|
||||||
|
|
||||||
class LongitudinalMpc:
|
class LongitudinalMpc:
|
||||||
def __init__(self, mode='acc', dt=DT_MDL):
|
def __init__(self, dt=DT_MDL):
|
||||||
self.mode = mode
|
|
||||||
self.dt = dt
|
self.dt = dt
|
||||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||||
self.reset()
|
self.reset()
|
||||||
self.source = SOURCES[2]
|
self.source = SOURCES[2]
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
|
||||||
self.solver.reset()
|
self.solver.reset()
|
||||||
# self.solver.options_set('print_level', 2)
|
|
||||||
|
self.x_sol = np.zeros((N+1, X_DIM))
|
||||||
|
self.u_sol = np.zeros((N, 1))
|
||||||
self.v_solution = np.zeros(N+1)
|
self.v_solution = np.zeros(N+1)
|
||||||
self.a_solution = np.zeros(N+1)
|
self.a_solution = np.zeros(N+1)
|
||||||
self.prev_a = np.array(self.a_solution)
|
|
||||||
self.j_solution = np.zeros(N)
|
self.j_solution = np.zeros(N)
|
||||||
|
self.prev_a = np.array(self.a_solution)
|
||||||
self.yref = np.zeros((N+1, COST_DIM))
|
self.yref = np.zeros((N+1, COST_DIM))
|
||||||
|
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
self.solver.cost_set(i, "yref", self.yref[i])
|
self.solver.cost_set(i, "yref", self.yref[i])
|
||||||
self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM])
|
self.solver.cost_set(N, "yref", self.yref[N][:COST_E_DIM])
|
||||||
self.x_sol = np.zeros((N+1, X_DIM))
|
|
||||||
self.u_sol = np.zeros((N,1))
|
|
||||||
self.params = np.zeros((N+1, PARAM_DIM))
|
self.params = np.zeros((N+1, PARAM_DIM))
|
||||||
for i in range(N+1):
|
for i in range(N+1):
|
||||||
self.solver.set(i, 'x', np.zeros(X_DIM))
|
self.solver.set(i, 'x', np.zeros(X_DIM))
|
||||||
|
|
||||||
self.last_cloudlog_t = 0
|
self.last_cloudlog_t = 0
|
||||||
self.status = False
|
self.status = False
|
||||||
self.crash_cnt = 0.0
|
self.crash_cnt = 0.0
|
||||||
@@ -276,16 +268,9 @@ class LongitudinalMpc:
|
|||||||
|
|
||||||
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
def set_weights(self, prev_accel_constraint=True, personality=log.LongitudinalPersonality.standard):
|
||||||
jerk_factor = get_jerk_factor(personality)
|
jerk_factor = get_jerk_factor(personality)
|
||||||
if self.mode == 'acc':
|
a_change_cost = A_CHANGE_COST if prev_accel_constraint else 0
|
||||||
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]
|
||||||
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]
|
||||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
|
||||||
elif self.mode == 'blended':
|
|
||||||
a_change_cost = 40.0 if prev_accel_constraint else 0
|
|
||||||
cost_weights = [0., 0.1, 0.2, 5.0, a_change_cost, 1.0]
|
|
||||||
constraint_cost_weights = [LIMIT_COST, LIMIT_COST, LIMIT_COST, DANGER_ZONE_COST]
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner cost set')
|
|
||||||
self.set_cost_weights(cost_weights, constraint_cost_weights)
|
self.set_cost_weights(cost_weights, constraint_cost_weights)
|
||||||
|
|
||||||
def set_cur_state(self, v, a):
|
def set_cur_state(self, v, a):
|
||||||
@@ -320,14 +305,14 @@ class LongitudinalMpc:
|
|||||||
|
|
||||||
# MPC will not converge if immediate crash is expected
|
# MPC will not converge if immediate crash is expected
|
||||||
# Clip lead distance to what is still possible to brake for
|
# Clip lead distance to what is still possible to brake for
|
||||||
min_x_lead = ((v_ego + v_lead)/2) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
|
min_x_lead = MIN_X_LEAD_FACTOR * (v_ego + v_lead) * (v_ego - v_lead) / (-ACCEL_MIN * 2)
|
||||||
x_lead = np.clip(x_lead, min_x_lead, 1e8)
|
x_lead = np.clip(x_lead, min_x_lead, 1e8)
|
||||||
v_lead = np.clip(v_lead, 0.0, 1e8)
|
v_lead = np.clip(v_lead, 0.0, 1e8)
|
||||||
a_lead = np.clip(a_lead, -10., 5.)
|
a_lead = np.clip(a_lead, -10., 5.)
|
||||||
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
lead_xv = self.extrapolate_lead(x_lead, v_lead, a_lead, a_lead_tau)
|
||||||
return lead_xv
|
return lead_xv
|
||||||
|
|
||||||
def update(self, radarstate, v_cruise, x, v, a, j, personality=log.LongitudinalPersonality.standard):
|
def update(self, radarstate, v_cruise, personality=log.LongitudinalPersonality.standard):
|
||||||
t_follow = get_T_FOLLOW(personality)
|
t_follow = get_T_FOLLOW(personality)
|
||||||
v_ego = self.x0[1]
|
v_ego = self.x0[1]
|
||||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||||
@@ -341,56 +326,28 @@ class LongitudinalMpc:
|
|||||||
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
|
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
|
||||||
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
|
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
|
||||||
|
|
||||||
self.params[:,0] = ACCEL_MIN
|
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||||
self.params[:,1] = ACCEL_MAX
|
# when the leads are no factor.
|
||||||
|
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||||
|
# TODO does this make sense when max_a is negative?
|
||||||
|
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||||
|
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_lower, v_upper)
|
||||||
|
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
|
||||||
|
|
||||||
# Update in ACC mode or ACC/e2e blend
|
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
|
||||||
if self.mode == 'acc':
|
self.source = SOURCES[np.argmin(x_obstacles[0])]
|
||||||
self.params[:,5] = LEAD_DANGER_FACTOR
|
|
||||||
|
|
||||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
self.yref[:,:] = 0.0
|
||||||
# when the leads are no factor.
|
|
||||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
|
||||||
# TODO does this make sense when max_a is negative?
|
|
||||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
|
||||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
|
||||||
v_lower,
|
|
||||||
v_upper)
|
|
||||||
cruise_obstacle = np.cumsum(T_DIFFS * v_cruise_clipped) + get_safe_obstacle_distance(v_cruise_clipped, t_follow)
|
|
||||||
x_obstacles = np.column_stack([lead_0_obstacle, lead_1_obstacle, cruise_obstacle])
|
|
||||||
self.source = SOURCES[np.argmin(x_obstacles[0])]
|
|
||||||
|
|
||||||
# These are not used in ACC mode
|
|
||||||
x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0
|
|
||||||
|
|
||||||
elif self.mode == 'blended':
|
|
||||||
self.params[:,5] = 1.0
|
|
||||||
|
|
||||||
x_obstacles = np.column_stack([lead_0_obstacle,
|
|
||||||
lead_1_obstacle])
|
|
||||||
cruise_target = T_IDXS * np.clip(v_cruise, v_ego - 2.0, 1e3) + x[0]
|
|
||||||
xforward = ((v[1:] + v[:-1]) / 2) * (T_IDXS[1:] - T_IDXS[:-1])
|
|
||||||
x = np.cumsum(np.insert(xforward, 0, x[0]))
|
|
||||||
|
|
||||||
x_and_cruise = np.column_stack([x, cruise_target])
|
|
||||||
x = np.min(x_and_cruise, axis=1)
|
|
||||||
|
|
||||||
self.source = 'e2e' if x_and_cruise[1,0] < x_and_cruise[1,1] else 'cruise'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f'Planner mode {self.mode} not recognized in planner update')
|
|
||||||
|
|
||||||
self.yref[:,1] = x
|
|
||||||
self.yref[:,2] = v
|
|
||||||
self.yref[:,3] = a
|
|
||||||
self.yref[:,5] = j
|
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
self.solver.set(i, "yref", self.yref[i])
|
self.solver.set(i, "yref", self.yref[i])
|
||||||
self.solver.set(N, "yref", self.yref[N][:COST_E_DIM])
|
self.solver.set(N, "yref", self.yref[N][:COST_E_DIM])
|
||||||
|
|
||||||
|
self.params[:,0] = ACCEL_MIN
|
||||||
|
self.params[:,1] = ACCEL_MAX
|
||||||
self.params[:,2] = np.min(x_obstacles, axis=1)
|
self.params[:,2] = np.min(x_obstacles, axis=1)
|
||||||
self.params[:,3] = np.copy(self.prev_a)
|
self.params[:,3] = np.copy(self.prev_a)
|
||||||
self.params[:,4] = t_follow
|
self.params[:,4] = t_follow
|
||||||
|
self.params[:,5] = LEAD_DANGER_FACTOR
|
||||||
|
|
||||||
self.run()
|
self.run()
|
||||||
if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and
|
if (np.any(lead_xv_0[FCW_IDXS,0] - self.x_sol[FCW_IDXS,0] < CRASH_DISTANCE) and
|
||||||
@@ -399,18 +356,7 @@ class LongitudinalMpc:
|
|||||||
else:
|
else:
|
||||||
self.crash_cnt = 0
|
self.crash_cnt = 0
|
||||||
|
|
||||||
# Check if it got within lead comfort range
|
|
||||||
# TODO This should be done cleaner
|
|
||||||
if self.mode == 'blended':
|
|
||||||
if any((lead_0_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0):
|
|
||||||
self.source = 'lead0'
|
|
||||||
if any((lead_1_obstacle - get_safe_obstacle_distance(self.x_sol[:,1], t_follow))- self.x_sol[:,0] < 0.0) and \
|
|
||||||
(lead_1_obstacle[0] - lead_0_obstacle[0]):
|
|
||||||
self.source = 'lead1'
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# t0 = time.monotonic()
|
|
||||||
# reset = 0
|
|
||||||
for i in range(N+1):
|
for i in range(N+1):
|
||||||
self.solver.set(i, 'p', self.params[i])
|
self.solver.set(i, 'p', self.params[i])
|
||||||
self.solver.constraints_set(0, "lbx", self.x0)
|
self.solver.constraints_set(0, "lbx", self.x0)
|
||||||
@@ -422,13 +368,6 @@ class LongitudinalMpc:
|
|||||||
self.time_linearization = float(self.solver.get_stats('time_lin')[0])
|
self.time_linearization = float(self.solver.get_stats('time_lin')[0])
|
||||||
self.time_integrator = float(self.solver.get_stats('time_sim')[0])
|
self.time_integrator = float(self.solver.get_stats('time_sim')[0])
|
||||||
|
|
||||||
# qp_iter = self.solver.get_stats('statistics')[-1][-1] # SQP_RTI specific
|
|
||||||
# print(f"long_mpc timings: tot {self.solve_time:.2e}, qp {self.time_qp_solution:.2e}, lin {self.time_linearization:.2e}, \
|
|
||||||
# integrator {self.time_integrator:.2e}, qp_iter {qp_iter}")
|
|
||||||
# res = self.solver.get_residuals()
|
|
||||||
# print(f"long_mpc residuals: {res[0]:.2e}, {res[1]:.2e}, {res[2]:.2e}, {res[3]:.2e}")
|
|
||||||
# self.solver.print_statistics()
|
|
||||||
|
|
||||||
for i in range(N+1):
|
for i in range(N+1):
|
||||||
self.x_sol[i] = self.solver.get(i, 'x')
|
self.x_sol[i] = self.solver.get(i, 'x')
|
||||||
for i in range(N):
|
for i in range(N):
|
||||||
@@ -446,12 +385,8 @@ class LongitudinalMpc:
|
|||||||
self.last_cloudlog_t = t
|
self.last_cloudlog_t = t
|
||||||
cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}")
|
cloudlog.warning(f"Long mpc reset, solution_status: {self.solution_status}")
|
||||||
self.reset()
|
self.reset()
|
||||||
# reset = 1
|
|
||||||
# print(f"long_mpc timings: total internal {self.solve_time:.2e}, external: {(time.monotonic() - t0):.2e} qp {self.time_qp_solution:.2e}, \
|
|
||||||
# lin {self.time_linearization:.2e} qp_iter {qp_iter}, reset {reset}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ocp = gen_long_ocp()
|
ocp = gen_long_ocp()
|
||||||
AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)
|
AcadosOcpSolver.generate(ocp, json_file=JSON_FILE)
|
||||||
# AcadosOcpSolver.build(ocp.code_export_directory, with_cython=True)
|
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||||
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc
|
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, SOURCES
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
|
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
|
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
|
||||||
LON_MPC_STEP = 0.2 # first step is 0.2s
|
|
||||||
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
|
A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6]
|
||||||
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
|
A_CRUISE_MAX_BP = [0., 10.0, 25., 40.]
|
||||||
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
|
||||||
@@ -26,14 +25,12 @@ MIN_ALLOW_THROTTLE_SPEED = 2.5
|
|||||||
_A_TOTAL_MAX_V = [1.7, 3.2]
|
_A_TOTAL_MAX_V = [1.7, 3.2]
|
||||||
_A_TOTAL_MAX_BP = [20., 40.]
|
_A_TOTAL_MAX_BP = [20., 40.]
|
||||||
|
|
||||||
|
|
||||||
def get_max_accel(v_ego):
|
def get_max_accel(v_ego):
|
||||||
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
|
return np.interp(v_ego, A_CRUISE_MAX_BP, A_CRUISE_MAX_VALS)
|
||||||
|
|
||||||
def get_coast_accel(pitch):
|
def get_coast_accel(pitch):
|
||||||
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
|
return np.sin(pitch) * -5.65 - 0.3 # fitted from data using xx/projects/allow_throttle/compute_coast_accel.py
|
||||||
|
|
||||||
|
|
||||||
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
|
def limit_accel_in_turns(v_ego, angle_steers, a_target, CP):
|
||||||
"""
|
"""
|
||||||
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
|
This function returns a limited long acceleration allowed, depending on the existing lateral acceleration
|
||||||
@@ -52,8 +49,6 @@ class LongitudinalPlanner:
|
|||||||
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
|
def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
self.mpc = LongitudinalMpc(dt=dt)
|
self.mpc = LongitudinalMpc(dt=dt)
|
||||||
# TODO remove mpc modes when TR released
|
|
||||||
self.mpc.mode = 'acc'
|
|
||||||
self.fcw = False
|
self.fcw = False
|
||||||
self.dt = dt
|
self.dt = dt
|
||||||
self.allow_throttle = True
|
self.allow_throttle = True
|
||||||
@@ -67,7 +62,6 @@ class LongitudinalPlanner:
|
|||||||
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
self.v_desired_trajectory = np.zeros(CONTROL_N)
|
||||||
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
self.a_desired_trajectory = np.zeros(CONTROL_N)
|
||||||
self.j_desired_trajectory = np.zeros(CONTROL_N)
|
self.j_desired_trajectory = np.zeros(CONTROL_N)
|
||||||
self.solverExecutionTime = 0.0
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_model(model_msg):
|
def parse_model(model_msg):
|
||||||
@@ -90,8 +84,6 @@ class LongitudinalPlanner:
|
|||||||
return x, v, a, j, throttle_prob
|
return x, v, a, j, throttle_prob
|
||||||
|
|
||||||
def update(self, sm):
|
def update(self, sm):
|
||||||
mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc'
|
|
||||||
|
|
||||||
if len(sm['carControl'].orientationNED) == 3:
|
if len(sm['carControl'].orientationNED) == 3:
|
||||||
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
accel_coast = get_coast_accel(sm['carControl'].orientationNED[1])
|
||||||
else:
|
else:
|
||||||
@@ -113,12 +105,9 @@ class LongitudinalPlanner:
|
|||||||
# No change cost when user is controlling the speed, or when standstill
|
# No change cost when user is controlling the speed, or when standstill
|
||||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||||
|
|
||||||
if mode == 'acc':
|
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||||
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)
|
||||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
|
||||||
else:
|
|
||||||
accel_clip = [ACCEL_MIN, ACCEL_MAX]
|
|
||||||
|
|
||||||
if reset_state:
|
if reset_state:
|
||||||
self.v_desired_filter.x = v_ego
|
self.v_desired_filter.x = v_ego
|
||||||
@@ -127,7 +116,7 @@ class LongitudinalPlanner:
|
|||||||
|
|
||||||
# Prevent divergence, smooth in current v_ego
|
# Prevent divergence, smooth in current v_ego
|
||||||
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
|
self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego))
|
||||||
x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'])
|
_, _, _, _, throttle_prob = self.parse_model(sm['modelV2'])
|
||||||
# Don't clip at low speeds since throttle_prob doesn't account for creep
|
# Don't clip at low speeds since throttle_prob doesn't account for creep
|
||||||
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED
|
||||||
|
|
||||||
@@ -141,7 +130,7 @@ class LongitudinalPlanner:
|
|||||||
|
|
||||||
self.mpc.set_weights(prev_accel_constraint, personality=sm['selfdriveState'].personality)
|
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.set_cur_state(self.v_desired_filter.x, self.a_desired)
|
||||||
self.mpc.update(sm['radarState'], v_cruise, x, v, a, j, 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.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)
|
self.a_desired_trajectory = np.interp(CONTROL_N_T_IDX, T_IDXS_MPC, self.mpc.a_solution)
|
||||||
@@ -163,12 +152,13 @@ class LongitudinalPlanner:
|
|||||||
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
|
||||||
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
output_should_stop_e2e = sm['modelV2'].action.shouldStop
|
||||||
|
|
||||||
if mode == 'acc':
|
if (output_a_target_e2e < output_a_target_mpc) and sm['selfdriveState'].experimentalMode:
|
||||||
|
output_a_target = output_a_target_e2e
|
||||||
|
self.output_should_stop = output_should_stop_e2e
|
||||||
|
self.mpc.source = SOURCES[3]
|
||||||
|
else:
|
||||||
output_a_target = output_a_target_mpc
|
output_a_target = output_a_target_mpc
|
||||||
self.output_should_stop = output_should_stop_mpc
|
self.output_should_stop = output_should_stop_mpc
|
||||||
else:
|
|
||||||
output_a_target = min(output_a_target_mpc, output_a_target_e2e)
|
|
||||||
self.output_should_stop = output_should_stop_e2e or output_should_stop_mpc
|
|
||||||
|
|
||||||
for idx in range(2):
|
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)
|
accel_clip[idx] = np.clip(accel_clip[idx], self.prev_accel_clip[idx] - 0.05, self.prev_accel_clip[idx] + 0.05)
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ from parameterized import parameterized_class
|
|||||||
|
|
||||||
from cereal import log
|
from cereal import log
|
||||||
|
|
||||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import desired_follow_distance, get_T_FOLLOW
|
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import get_safe_obstacle_distance, get_stopped_equivalence_factor, get_T_FOLLOW
|
||||||
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
|
from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
|
||||||
|
|
||||||
|
|
||||||
|
def desired_follow_distance(v_ego, v_lead, t_follow=None):
|
||||||
|
if t_follow is None:
|
||||||
|
t_follow = get_T_FOLLOW()
|
||||||
|
return get_safe_obstacle_distance(v_ego, t_follow) - get_stopped_equivalence_factor(v_lead)
|
||||||
|
|
||||||
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
|
def run_following_distance_simulation(v_lead, t_end=100.0, e2e=False, personality=0):
|
||||||
man = Maneuver(
|
man = Maneuver(
|
||||||
'',
|
'',
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from cereal import car, log
|
||||||
|
from opendbc.car.car_helpers import interfaces
|
||||||
|
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||||
|
from opendbc.car.vehicle_model import VehicleModel
|
||||||
|
from openpilot.common.realtime import DT_CTRL
|
||||||
|
from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque, LAT_ACCEL_REQUEST_BUFFER_SECONDS
|
||||||
|
|
||||||
|
def get_controller(car_name):
|
||||||
|
CarInterface = interfaces[car_name]
|
||||||
|
CP = CarInterface.get_non_essential_params(car_name)
|
||||||
|
CI = CarInterface(CP)
|
||||||
|
VM = VehicleModel(CP)
|
||||||
|
controller = LatControlTorque(CP.as_reader(), CI, DT_CTRL)
|
||||||
|
return controller, VM
|
||||||
|
|
||||||
|
class TestLatControlTorqueBuffer:
|
||||||
|
|
||||||
|
@parameterized.expand([(TOYOTA.TOYOTA_COROLLA_TSS2,)])
|
||||||
|
def test_request_buffer_consistency(self, car_name):
|
||||||
|
buffer_steps = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / DT_CTRL)
|
||||||
|
controller, VM = get_controller(car_name)
|
||||||
|
|
||||||
|
CS = car.CarState.new_message()
|
||||||
|
CS.vEgo = 30
|
||||||
|
CS.steeringPressed = False
|
||||||
|
params = log.LiveParametersData.new_message()
|
||||||
|
|
||||||
|
for _ in range(buffer_steps):
|
||||||
|
controller.update(True, CS, VM, params, False, 0.001, False, 0.2)
|
||||||
|
assert all(val != 0 for val in controller.lat_accel_request_buffer)
|
||||||
|
|
||||||
|
for _ in range(buffer_steps):
|
||||||
|
controller.update(False, CS, VM, params, False, 0.0, False, 0.2)
|
||||||
|
assert all(val == 0 for val in controller.lat_accel_request_buffer)
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from cereal.services import SERVICE_LIST, QueueSize
|
||||||
|
from openpilot.tools.lib.logreader import LogReader
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Analyze message sizes from a log route")
|
||||||
|
parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255",
|
||||||
|
help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
lr = LogReader(args.route)
|
||||||
|
|
||||||
|
szs = {}
|
||||||
|
for msg in tqdm(lr):
|
||||||
|
sz = len(msg.as_builder().to_bytes())
|
||||||
|
msg_type = msg.which()
|
||||||
|
if msg_type not in szs:
|
||||||
|
szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1}
|
||||||
|
else:
|
||||||
|
szs[msg_type]['min'] = min(szs[msg_type]['min'], sz)
|
||||||
|
szs[msg_type]['max'] = max(szs[msg_type]['max'], sz)
|
||||||
|
szs[msg_type]['sum'] += sz
|
||||||
|
szs[msg_type]['count'] += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}")
|
||||||
|
print("-" * 132)
|
||||||
|
def sort_key(x):
|
||||||
|
k, v = x
|
||||||
|
avg = v['sum'] / v['count']
|
||||||
|
freq = SERVICE_LIST.get(k, None)
|
||||||
|
freq_val = freq.frequency if freq else 0.0
|
||||||
|
kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0
|
||||||
|
return kb_per_min
|
||||||
|
total_kb_per_min = 0.0
|
||||||
|
RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default
|
||||||
|
for k, v in sorted(szs.items(), key=sort_key, reverse=True):
|
||||||
|
avg = v['sum'] / v['count']
|
||||||
|
service = SERVICE_LIST.get(k, None)
|
||||||
|
freq_val = service.frequency if service else 0.0
|
||||||
|
queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL
|
||||||
|
kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0
|
||||||
|
kb_per_sec = kb_per_min / 60
|
||||||
|
minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf')
|
||||||
|
seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf')
|
||||||
|
total_kb_per_min += kb_per_min
|
||||||
|
min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf"
|
||||||
|
sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf"
|
||||||
|
print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}")
|
||||||
|
|
||||||
|
# Summary section
|
||||||
|
print()
|
||||||
|
print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min")
|
||||||
|
|
||||||
|
# Calculate memory usage: old (10MB for all) vs new (from services.py)
|
||||||
|
OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default
|
||||||
|
old_total = len(SERVICE_LIST) * OLD_SIZE
|
||||||
|
|
||||||
|
new_total = sum(s.queue_size for s in SERVICE_LIST.values())
|
||||||
|
|
||||||
|
# Count by queue size
|
||||||
|
size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0}
|
||||||
|
for s in SERVICE_LIST.values():
|
||||||
|
size_counts[s.queue_size] += 1
|
||||||
|
|
||||||
|
savings_pct = (1 - new_total / old_total) * 100
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{'Queue Size Comparison':<40}")
|
||||||
|
print("-" * 60)
|
||||||
|
print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB")
|
||||||
|
print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB")
|
||||||
|
print(f"{'Savings:':<30} {savings_pct:>10.1f}%")
|
||||||
|
print()
|
||||||
|
print(f"{'Breakdown:':<30}")
|
||||||
|
print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services")
|
||||||
|
print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services")
|
||||||
|
print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services")
|
||||||
@@ -101,11 +101,11 @@ if __name__ == "__main__":
|
|||||||
uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
|
uds_client = UdsClient(panda, 0x7D0, bus=args.bus)
|
||||||
|
|
||||||
print("\n[START DIAGNOSTIC SESSION]")
|
print("\n[START DIAGNOSTIC SESSION]")
|
||||||
session_type : SESSION_TYPE = 0x07 # type: ignore
|
session_type : SESSION_TYPE = 0x07
|
||||||
uds_client.diagnostic_session_control(session_type)
|
uds_client.diagnostic_session_control(session_type)
|
||||||
|
|
||||||
print("[HARDWARE/SOFTWARE VERSION]")
|
print("[HARDWARE/SOFTWARE VERSION]")
|
||||||
fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100 # type: ignore
|
fw_version_data_id : DATA_IDENTIFIER_TYPE = 0xf100
|
||||||
fw_version = uds_client.read_data_by_identifier(fw_version_data_id)
|
fw_version = uds_client.read_data_by_identifier(fw_version_data_id)
|
||||||
print(fw_version)
|
print(fw_version)
|
||||||
if fw_version not in SUPPORTED_FW_VERSIONS.keys():
|
if fw_version not in SUPPORTED_FW_VERSIONS.keys():
|
||||||
@@ -113,7 +113,7 @@ if __name__ == "__main__":
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("[GET CONFIGURATION]")
|
print("[GET CONFIGURATION]")
|
||||||
config_data_id : DATA_IDENTIFIER_TYPE = 0x0142 # type: ignore
|
config_data_id : DATA_IDENTIFIER_TYPE = 0x0142
|
||||||
current_config = uds_client.read_data_by_identifier(config_data_id)
|
current_config = uds_client.read_data_by_identifier(config_data_id)
|
||||||
config_values = SUPPORTED_FW_VERSIONS[fw_version]
|
config_values = SUPPORTED_FW_VERSIONS[fw_version]
|
||||||
new_config = config_values.default_config if args.default else config_values.tracks_enabled
|
new_config = config_values.default_config if args.default else config_values.tracks_enabled
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ if __name__ == "__main__":
|
|||||||
sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8")
|
sw_ver = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_VERSION_NUMBER).decode("utf-8")
|
||||||
component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8")
|
component = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.SYSTEM_NAME_OR_ENGINE_TYPE).decode("utf-8")
|
||||||
odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00')
|
odx_file = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.ODX_FILE).decode("utf-8").rstrip('\x00')
|
||||||
current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING) # type: ignore
|
current_coding = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING)
|
||||||
coding_text = current_coding.hex()
|
coding_text = current_coding.hex()
|
||||||
|
|
||||||
print("\nEPS diagnostic data\n")
|
print("\nEPS diagnostic data\n")
|
||||||
@@ -126,9 +126,9 @@ if __name__ == "__main__":
|
|||||||
new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:]
|
new_coding = current_coding[0:coding_byte] + new_byte.to_bytes(1, "little") + current_coding[coding_byte+1:]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED) # type: ignore
|
seed = uds_client.security_access(ACCESS_TYPE_LEVEL_1.REQUEST_SEED)
|
||||||
key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that
|
key = struct.unpack("!I", seed)[0] + 28183 # yeah, it's like that
|
||||||
uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key)) # type: ignore
|
uds_client.security_access(ACCESS_TYPE_LEVEL_1.SEND_KEY, struct.pack("!I", key))
|
||||||
except (NegativeResponseError, MessageTimeoutError):
|
except (NegativeResponseError, MessageTimeoutError):
|
||||||
print("Security access failed!")
|
print("Security access failed!")
|
||||||
print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)")
|
print("Open the hood and retry (disables the \"diagnostic firewall\" on newer vehicles)")
|
||||||
@@ -148,7 +148,7 @@ if __name__ == "__main__":
|
|||||||
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
|
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.PROGRAMMING_DATE, prog_date)
|
||||||
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
|
tester_num = uds_client.read_data_by_identifier(DATA_IDENTIFIER_TYPE.CALIBRATION_REPAIR_SHOP_CODE_OR_CALIBRATION_EQUIPMENT_SERIAL_NUMBER)
|
||||||
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num)
|
uds_client.write_data_by_identifier(DATA_IDENTIFIER_TYPE.REPAIR_SHOP_CODE_OR_TESTER_SERIAL_NUMBER, tester_num)
|
||||||
uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding) # type: ignore
|
uds_client.write_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING, new_coding)
|
||||||
except (NegativeResponseError, MessageTimeoutError):
|
except (NegativeResponseError, MessageTimeoutError):
|
||||||
print("Writing new configuration failed!")
|
print("Writing new configuration failed!")
|
||||||
print("Make sure the comma processes are stopped: tmux kill-session -t comma")
|
print("Make sure the comma processes are stopped: tmux kill-session -t comma")
|
||||||
@@ -156,7 +156,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Read back result just to make 100% sure everything worked
|
# Read back result just to make 100% sure everything worked
|
||||||
current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex() # type: ignore
|
current_coding_text = uds_client.read_data_by_identifier(VOLKSWAGEN_DATA_IDENTIFIER_TYPE.CODING).hex()
|
||||||
print(f" New coding: {current_coding_text}")
|
print(f" New coding: {current_coding_text}")
|
||||||
except (NegativeResponseError, MessageTimeoutError):
|
except (NegativeResponseError, MessageTimeoutError):
|
||||||
print("Reading back updated coding failed!")
|
print("Reading back updated coding failed!")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# type: ignore
|
|
||||||
'''
|
'''
|
||||||
System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs.
|
System tools like top/htop can only show current cpu usage values, so I write this script to do statistics jobs.
|
||||||
Features:
|
Features:
|
||||||
|
|||||||
@@ -99,8 +99,7 @@ def cycle_alerts(duration=200, is_metric=False):
|
|||||||
alert = AM.process_alerts(frame, [])
|
alert = AM.process_alerts(frame, [])
|
||||||
print(alert)
|
print(alert)
|
||||||
for _ in range(duration):
|
for _ in range(duration):
|
||||||
dat = messaging.new_message()
|
dat = messaging.new_message('selfdriveState')
|
||||||
dat.init('selfdriveState')
|
|
||||||
dat.selfdriveState.enabled = False
|
dat.selfdriveState.enabled = False
|
||||||
|
|
||||||
if alert:
|
if alert:
|
||||||
@@ -112,8 +111,7 @@ def cycle_alerts(duration=200, is_metric=False):
|
|||||||
dat.selfdriveState.alertSound = alert.audible_alert
|
dat.selfdriveState.alertSound = alert.audible_alert
|
||||||
pm.send('selfdriveState', dat)
|
pm.send('selfdriveState', dat)
|
||||||
|
|
||||||
dat = messaging.new_message()
|
dat = messaging.new_message('deviceState')
|
||||||
dat.init('deviceState')
|
|
||||||
dat.deviceState.started = True
|
dat.deviceState.started = True
|
||||||
pm.send('deviceState', dat)
|
pm.send('deviceState', dat)
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ def get_fingerprint(lr):
|
|||||||
|
|
||||||
# TODO: also print the fw fingerprint merged with the existing ones
|
# TODO: also print the fw fingerprint merged with the existing ones
|
||||||
# show FW fingerprint
|
# show FW fingerprint
|
||||||
print("\nFW fingerprint:\n")
|
if fw:
|
||||||
for f in fw:
|
print("\nFW fingerprint:\n")
|
||||||
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
|
for f in fw:
|
||||||
print(f" {f.fwVersion},")
|
print(f" (Ecu.{f.ecu}, {hex(f.address)}, {None if f.subAddress == 0 else f.subAddress}): [")
|
||||||
print(" ],")
|
print(f" {f.fwVersion},")
|
||||||
|
print(" ],")
|
||||||
print()
|
print()
|
||||||
print(f"VIN: {vin}")
|
print(f"VIN: {vin}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# type: ignore
|
|
||||||
import random
|
import random
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# type: ignore
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# type: ignore
|
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ DEBUG = os.getenv("DEBUG") is not None
|
|||||||
|
|
||||||
|
|
||||||
def is_calibration_valid(rpy: np.ndarray) -> bool:
|
def is_calibration_valid(rpy: np.ndarray) -> bool:
|
||||||
return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1]) # type: ignore
|
return (PITCH_LIMITS[0] < rpy[1] < PITCH_LIMITS[1]) and (YAW_LIMITS[0] < rpy[2] < YAW_LIMITS[1])
|
||||||
|
|
||||||
|
|
||||||
def sanity_clip(rpy: np.ndarray) -> np.ndarray:
|
def sanity_clip(rpy: np.ndarray) -> np.ndarray:
|
||||||
@@ -92,7 +92,7 @@ class Calibrator:
|
|||||||
valid_blocks: int = 0,
|
valid_blocks: int = 0,
|
||||||
wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT,
|
wide_from_device_euler_init: np.ndarray = WIDE_FROM_DEVICE_EULER_INIT,
|
||||||
height_init: np.ndarray = HEIGHT_INIT,
|
height_init: np.ndarray = HEIGHT_INIT,
|
||||||
smooth_from: np.ndarray = None) -> None:
|
smooth_from: np.ndarray | None = None) -> None:
|
||||||
if not np.isfinite(rpy_init).all():
|
if not np.isfinite(rpy_init).all():
|
||||||
self.rpy = RPY_INIT.copy()
|
self.rpy = RPY_INIT.copy()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class PointBuckets:
|
|||||||
def add_point(self, x: float, y: float) -> None:
|
def add_point(self, x: float, y: float) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_points(self, num_points: int = None) -> Any:
|
def get_points(self, num_points: int | None = None) -> Any:
|
||||||
points = np.vstack([x.arr for x in self.buckets.values()])
|
points = np.vstack([x.arr for x in self.buckets.values()])
|
||||||
if num_points is None:
|
if num_points is None:
|
||||||
return points
|
return points
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ class VehicleParamsLearner:
|
|||||||
|
|
||||||
if not self.active:
|
if not self.active:
|
||||||
# Reset time when stopped so uncertainty doesn't grow
|
# Reset time when stopped so uncertainty doesn't grow
|
||||||
self.kf.filter.set_filter_time(t) # type: ignore
|
self.kf.filter.set_filter_time(t)
|
||||||
self.kf.filter.reset_rewind() # type: ignore
|
self.kf.filter.reset_rewind()
|
||||||
|
|
||||||
def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder:
|
def get_msg(self, valid: bool, debug: bool = False) -> capnp._DynamicStructBuilder:
|
||||||
x = self.kf.x
|
x = self.kf.x
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100])
|
|||||||
MIN_ENGAGE_BUFFER = 2 # secs
|
MIN_ENGAGE_BUFFER = 2 # secs
|
||||||
|
|
||||||
VERSION = 1 # bump this to invalidate old parameter caches
|
VERSION = 1 # bump this to invalidate old parameter caches
|
||||||
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
|
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda', 'volkswagen']
|
||||||
|
|
||||||
|
|
||||||
def slope2rot(slope):
|
def slope2rot(slope):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc')
|
||||||
lenv = env.Clone()
|
lenv = env.Clone()
|
||||||
lenvCython = envCython.Clone()
|
lenvCython = envCython.Clone()
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl'
|
|||||||
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
|
VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl'
|
||||||
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
|
POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl'
|
||||||
|
|
||||||
LAT_SMOOTH_SECONDS = 0.1
|
LAT_SMOOTH_SECONDS = 0.0
|
||||||
LONG_SMOOTH_SECONDS = 0.3
|
LONG_SMOOTH_SECONDS = 0.3
|
||||||
MIN_LAT_CONTROL_SPEED = 0.3
|
MIN_LAT_CONTROL_SPEED = 0.3
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3446bf8b22e50e47669a25bf32460ae8baf8547037f346753e19ecbfcf6d4e59
|
oid sha256:35e4a5d4c4d481f915e42358af4665b2c92b8f5c1efd1c0731f21b876ad1d856
|
||||||
size 6954368
|
size 6954249
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f8fe9a71b0fd428a045a82ed50790179f77aa664391198f078e11e7b2cb2c2d7
|
oid sha256:1edea5bb56f876db4cec97c150799513f6a59373f3ad152d55e4dcaab1b809e3
|
||||||
size 13926324
|
size 13926324
|
||||||
|
|||||||
@@ -35,14 +35,7 @@ class DRIVER_MONITOR_SETTINGS:
|
|||||||
self._EYE_THRESHOLD = 0.65
|
self._EYE_THRESHOLD = 0.65
|
||||||
self._SG_THRESHOLD = 0.9
|
self._SG_THRESHOLD = 0.9
|
||||||
self._BLINK_THRESHOLD = 0.865
|
self._BLINK_THRESHOLD = 0.865
|
||||||
|
self._PHONE_THRESH = 0.5
|
||||||
self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4
|
|
||||||
self._PHONE_THRESH2 = 15.0
|
|
||||||
self._PHONE_MAX_OFFSET = 0.06
|
|
||||||
self._PHONE_MIN_OFFSET = 0.025
|
|
||||||
self._PHONE_DATA_AVG = 0.05
|
|
||||||
self._PHONE_DATA_VAR = 3*0.005
|
|
||||||
self._PHONE_MAX_COUNT = int(360 / self._DT_DMON)
|
|
||||||
|
|
||||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||||
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
self._POSE_PITCH_THRESHOLD_SLACK = 0.3237
|
||||||
@@ -152,11 +145,10 @@ class DriverMonitoring:
|
|||||||
|
|
||||||
# init driver status
|
# init driver status
|
||||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||||
phone_filter_raw_priors = (self.settings._PHONE_DATA_AVG, self.settings._PHONE_DATA_VAR, 2)
|
|
||||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||||
self.phone = DriverProb(raw_priors=phone_filter_raw_priors, max_trackable=self.settings._PHONE_MAX_COUNT)
|
|
||||||
self.pose = DriverPose(settings=self.settings)
|
self.pose = DriverPose(settings=self.settings)
|
||||||
self.blink = DriverBlink()
|
self.blink = DriverBlink()
|
||||||
|
self.phone_prob = 0.
|
||||||
|
|
||||||
self.always_on = always_on
|
self.always_on = always_on
|
||||||
self.distracted_types = []
|
self.distracted_types = []
|
||||||
@@ -257,12 +249,7 @@ class DriverMonitoring:
|
|||||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||||
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||||
|
|
||||||
if self.phone.prob_calibrated:
|
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||||
using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \
|
|
||||||
* self.settings._PHONE_THRESH2
|
|
||||||
else:
|
|
||||||
using_phone = self.phone.prob > self.settings._PHONE_THRESH
|
|
||||||
if using_phone:
|
|
||||||
distracted_types.append(DistractedType.DISTRACTED_PHONE)
|
distracted_types.append(DistractedType.DISTRACTED_PHONE)
|
||||||
|
|
||||||
return distracted_types
|
return distracted_types
|
||||||
@@ -301,7 +288,7 @@ class DriverMonitoring:
|
|||||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||||
self.phone.prob = driver_data.phoneProb
|
self.phone_prob = driver_data.phoneProb
|
||||||
|
|
||||||
self.distracted_types = self._get_distracted_types()
|
self.distracted_types = self._get_distracted_types()
|
||||||
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
|
self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types
|
||||||
@@ -315,11 +302,9 @@ class DriverMonitoring:
|
|||||||
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
|
if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted):
|
||||||
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
self.pose.pitch_offseter.push_and_update(self.pose.pitch)
|
||||||
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
self.pose.yaw_offseter.push_and_update(self.pose.yaw)
|
||||||
self.phone.prob_offseter.push_and_update(self.phone.prob)
|
|
||||||
|
|
||||||
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \
|
||||||
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
||||||
self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT
|
|
||||||
|
|
||||||
if self.face_detected and not self.driver_distracted:
|
if self.face_detected and not self.driver_distracted:
|
||||||
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD:
|
||||||
@@ -425,8 +410,6 @@ class DriverMonitoring:
|
|||||||
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
|
"posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n,
|
||||||
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
|
"poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(),
|
||||||
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
|
"poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n,
|
||||||
"phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(),
|
|
||||||
"phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n,
|
|
||||||
"stepChange": self.step_change,
|
"stepChange": self.step_change,
|
||||||
"awarenessActive": self.awareness_active,
|
"awarenessActive": self.awareness_active,
|
||||||
"awarenessPassive": self.awareness_passive,
|
"awarenessPassive": self.awareness_passive,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
Import('env', 'envCython', 'common', 'messaging')
|
Import('env', 'common', 'messaging')
|
||||||
|
|
||||||
libs = ['usb-1.0', common, messaging, 'pthread']
|
libs = ['usb-1.0', common, messaging, 'pthread']
|
||||||
panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc'])
|
panda = env.Library('panda', ['panda.cc', 'panda_comms.cc', 'spi.cc'])
|
||||||
|
|
||||||
env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs)
|
env.Program('pandad', ['main.cc', 'pandad.cc', 'panda_safety.cc'], LIBS=[panda] + libs)
|
||||||
env.Library('libcan_list_to_can_capnp', ['can_list_to_can_capnp.cc'])
|
|
||||||
|
|
||||||
pandad_python = envCython.Program('pandad_api_impl.so', 'pandad_api_impl.pyx', LIBS=["can_list_to_can_capnp", 'capnp', 'kj'] + envCython["LIBS"])
|
|
||||||
Export('pandad_python')
|
|
||||||
|
|
||||||
if GetOption('extras'):
|
if GetOption('extras'):
|
||||||
env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs)
|
env.Program('tests/test_pandad_usbprotocol', ['tests/test_pandad_usbprotocol.cc'], LIBS=[panda] + libs)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Cython, now uses scons to build
|
|
||||||
from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list
|
from openpilot.selfdrive.pandad.pandad_api_impl import can_list_to_can_capnp, can_capnp_to_list
|
||||||
assert can_list_to_can_capnp
|
assert can_list_to_can_capnp
|
||||||
assert can_capnp_to_list
|
assert can_capnp_to_list
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
#include "cereal/messaging/messaging.h"
|
|
||||||
#include "selfdrive/pandad/can_types.h"
|
|
||||||
|
|
||||||
void can_list_to_can_capnp_cpp(const std::vector<CanFrame> &can_list, std::string &out, bool sendcan, bool valid) {
|
|
||||||
MessageBuilder msg;
|
|
||||||
auto event = msg.initEvent(valid);
|
|
||||||
|
|
||||||
auto canData = sendcan ? event.initSendcan(can_list.size()) : event.initCan(can_list.size());
|
|
||||||
int j = 0;
|
|
||||||
for (auto it = can_list.begin(); it != can_list.end(); it++, j++) {
|
|
||||||
auto c = canData[j];
|
|
||||||
c.setAddress(it->address);
|
|
||||||
c.setDat(kj::arrayPtr((uint8_t*)it->dat.data(), it->dat.size()));
|
|
||||||
c.setSrc(it->src);
|
|
||||||
}
|
|
||||||
const uint64_t msg_size = capnp::computeSerializedSizeInWords(msg) * sizeof(capnp::word);
|
|
||||||
out.resize(msg_size);
|
|
||||||
kj::ArrayOutputStream output_stream(kj::ArrayPtr<capnp::byte>((unsigned char *)out.data(), msg_size));
|
|
||||||
capnp::writeMessage(output_stream, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts a vector of Cap'n Proto serialized can strings into a vector of CanData structures.
|
|
||||||
void can_capnp_to_can_list_cpp(const std::vector<std::string> &strings, std::vector<CanData> &can_list, bool sendcan) {
|
|
||||||
AlignedBuffer aligned_buf;
|
|
||||||
can_list.reserve(strings.size());
|
|
||||||
|
|
||||||
for (const auto &str : strings) {
|
|
||||||
// extract the messages
|
|
||||||
capnp::FlatArrayMessageReader reader(aligned_buf.align(str.data(), str.size()));
|
|
||||||
cereal::Event::Reader event = reader.getRoot<cereal::Event>();
|
|
||||||
|
|
||||||
auto frames = sendcan ? event.getSendcan() : event.getCan();
|
|
||||||
|
|
||||||
// Add new CanData entry
|
|
||||||
CanData &can_data = can_list.emplace_back();
|
|
||||||
can_data.nanos = event.getLogMonoTime();
|
|
||||||
can_data.frames.reserve(frames.size());
|
|
||||||
|
|
||||||
// Populate CAN frames
|
|
||||||
for (const auto &frame : frames) {
|
|
||||||
CanFrame &can_frame = can_data.frames.emplace_back();
|
|
||||||
can_frame.src = frame.getSrc();
|
|
||||||
can_frame.address = frame.getAddress();
|
|
||||||
|
|
||||||
// Copy CAN data
|
|
||||||
auto dat = frame.getDat();
|
|
||||||
can_frame.dat.assign(dat.begin(), dat.end());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
struct CanFrame {
|
|
||||||
long src;
|
|
||||||
uint32_t address;
|
|
||||||
std::vector<uint8_t> dat;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CanData {
|
|
||||||
uint64_t nanos;
|
|
||||||
std::vector<CanFrame> frames;
|
|
||||||
};
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include "cereal/gen/cpp/car.capnp.h"
|
#include "cereal/gen/cpp/car.capnp.h"
|
||||||
#include "cereal/messaging/messaging.h"
|
#include "cereal/messaging/messaging.h"
|
||||||
|
#include "cereal/services.h"
|
||||||
#include "common/ratekeeper.h"
|
#include "common/ratekeeper.h"
|
||||||
#include "common/swaglog.h"
|
#include "common/swaglog.h"
|
||||||
#include "common/timing.h"
|
#include "common/timing.h"
|
||||||
@@ -82,7 +83,7 @@ void can_send_thread(std::vector<Panda *> pandas, bool fake_send) {
|
|||||||
|
|
||||||
AlignedBuffer aligned_buf;
|
AlignedBuffer aligned_buf;
|
||||||
std::unique_ptr<Context> context(Context::create());
|
std::unique_ptr<Context> context(Context::create());
|
||||||
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "sendcan"));
|
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "sendcan", "127.0.0.1", false, true, services.at("sendcan").queue_size));
|
||||||
assert(subscriber != NULL);
|
assert(subscriber != NULL);
|
||||||
subscriber->setTimeout(100);
|
subscriber->setTimeout(100);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import time
|
||||||
|
from cereal import log
|
||||||
|
|
||||||
|
NO_TRAVERSAL_LIMIT = 2**64 - 1
|
||||||
|
|
||||||
|
# Cache schema fields for faster access (avoids string lookup on each field access)
|
||||||
|
_cached_reader_fields = None # (address_field, dat_field, src_field) for reading
|
||||||
|
_cached_writer_fields = None # (address_field, dat_field, src_field) for writing
|
||||||
|
|
||||||
|
|
||||||
|
def _get_reader_fields(schema):
|
||||||
|
"""Get cached schema field objects for reading."""
|
||||||
|
global _cached_reader_fields
|
||||||
|
if _cached_reader_fields is None:
|
||||||
|
fields = schema.fields
|
||||||
|
_cached_reader_fields = (fields['address'], fields['dat'], fields['src'])
|
||||||
|
return _cached_reader_fields
|
||||||
|
|
||||||
|
|
||||||
|
def _get_writer_fields(schema):
|
||||||
|
"""Get cached schema field objects for writing."""
|
||||||
|
global _cached_writer_fields
|
||||||
|
if _cached_writer_fields is None:
|
||||||
|
fields = schema.fields
|
||||||
|
_cached_writer_fields = (fields['address'], fields['dat'], fields['src'])
|
||||||
|
return _cached_writer_fields
|
||||||
|
|
||||||
|
|
||||||
|
def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True):
|
||||||
|
"""Convert list of CAN messages to Cap'n Proto serialized bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
can_msgs: List of tuples [(address, data_bytes, src), ...]
|
||||||
|
msgtype: 'can' or 'sendcan'
|
||||||
|
valid: Whether the event is valid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cap'n Proto serialized bytes
|
||||||
|
"""
|
||||||
|
global _cached_writer_fields
|
||||||
|
|
||||||
|
dat = log.Event.new_message(valid=valid, logMonoTime=int(time.monotonic() * 1e9))
|
||||||
|
can_data = dat.init(msgtype, len(can_msgs))
|
||||||
|
|
||||||
|
# Cache schema fields on first call
|
||||||
|
if _cached_writer_fields is None and len(can_msgs) > 0:
|
||||||
|
_cached_writer_fields = _get_writer_fields(can_data[0].schema)
|
||||||
|
|
||||||
|
if _cached_writer_fields is not None:
|
||||||
|
addr_f, dat_f, src_f = _cached_writer_fields
|
||||||
|
for i, msg in enumerate(can_msgs):
|
||||||
|
f = can_data[i]
|
||||||
|
f._set_by_field(addr_f, msg[0])
|
||||||
|
f._set_by_field(dat_f, msg[1])
|
||||||
|
f._set_by_field(src_f, msg[2])
|
||||||
|
|
||||||
|
return dat.to_bytes()
|
||||||
|
|
||||||
|
|
||||||
|
def can_capnp_to_list(strings, msgtype='can'):
|
||||||
|
"""Convert Cap'n Proto serialized bytes to list of CAN messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strings: Tuple/list of serialized Cap'n Proto bytes
|
||||||
|
msgtype: 'can' or 'sendcan'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of tuples [(nanos, [(address, data, src), ...]), ...]
|
||||||
|
"""
|
||||||
|
global _cached_reader_fields
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for s in strings:
|
||||||
|
with log.Event.from_bytes(s, traversal_limit_in_words=NO_TRAVERSAL_LIMIT) as event:
|
||||||
|
frames = getattr(event, msgtype)
|
||||||
|
|
||||||
|
# Cache schema fields on first frame for faster access
|
||||||
|
if _cached_reader_fields is None and len(frames) > 0:
|
||||||
|
_cached_reader_fields = _get_reader_fields(frames[0].schema)
|
||||||
|
|
||||||
|
if _cached_reader_fields is not None:
|
||||||
|
addr_f, dat_f, src_f = _cached_reader_fields
|
||||||
|
frame_list = [(f._get_by_field(addr_f), f._get_by_field(dat_f), f._get_by_field(src_f)) for f in frames]
|
||||||
|
else:
|
||||||
|
frame_list = []
|
||||||
|
|
||||||
|
result.append((event.logMonoTime, frame_list))
|
||||||
|
return result
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# distutils: language = c++
|
|
||||||
# cython: language_level=3
|
|
||||||
from cython.operator cimport dereference as deref, preincrement as preinc
|
|
||||||
from libcpp.vector cimport vector
|
|
||||||
from libcpp.string cimport string
|
|
||||||
from libcpp cimport bool
|
|
||||||
from libc.stdint cimport uint8_t, uint32_t, uint64_t
|
|
||||||
|
|
||||||
cdef extern from "selfdrive/pandad/can_types.h":
|
|
||||||
cdef struct CanFrame:
|
|
||||||
long src
|
|
||||||
uint32_t address
|
|
||||||
vector[uint8_t] dat
|
|
||||||
|
|
||||||
cdef struct CanData:
|
|
||||||
uint64_t nanos
|
|
||||||
vector[CanFrame] frames
|
|
||||||
|
|
||||||
cdef extern from "can_list_to_can_capnp.cc":
|
|
||||||
void can_list_to_can_capnp_cpp(const vector[CanFrame] &can_list, string &out, bool sendcan, bool valid) nogil
|
|
||||||
void can_capnp_to_can_list_cpp(const vector[string] &strings, vector[CanData] &can_data, bool sendcan)
|
|
||||||
|
|
||||||
def can_list_to_can_capnp(can_msgs, msgtype='can', valid=True):
|
|
||||||
cdef CanFrame *f
|
|
||||||
cdef vector[CanFrame] can_list
|
|
||||||
cdef uint32_t cpp_can_msgs_len = len(can_msgs)
|
|
||||||
|
|
||||||
with nogil:
|
|
||||||
can_list.reserve(cpp_can_msgs_len)
|
|
||||||
|
|
||||||
for can_msg in can_msgs:
|
|
||||||
f = &(can_list.emplace_back())
|
|
||||||
f.address = can_msg[0]
|
|
||||||
f.dat = can_msg[1]
|
|
||||||
f.src = can_msg[2]
|
|
||||||
|
|
||||||
cdef string out
|
|
||||||
cdef bool is_sendcan = (msgtype == 'sendcan')
|
|
||||||
cdef bool is_valid = valid
|
|
||||||
with nogil:
|
|
||||||
can_list_to_can_capnp_cpp(can_list, out, is_sendcan, is_valid)
|
|
||||||
return out
|
|
||||||
|
|
||||||
def can_capnp_to_list(strings, msgtype='can'):
|
|
||||||
cdef vector[CanData] data
|
|
||||||
can_capnp_to_can_list_cpp(strings, data, msgtype == 'sendcan')
|
|
||||||
|
|
||||||
result = []
|
|
||||||
cdef CanData *d
|
|
||||||
cdef vector[CanData].iterator it = data.begin()
|
|
||||||
while it != data.end():
|
|
||||||
d = &deref(it)
|
|
||||||
frames = [(f.address, (<char *>&f.dat[0])[:f.dat.size()], f.src) for f in d.frames]
|
|
||||||
result.append((d.nanos, frames))
|
|
||||||
preinc(it)
|
|
||||||
return result
|
|
||||||
@@ -13,7 +13,7 @@ with open(os.path.join(BASEDIR, "selfdrive/selfdrived/alerts_offroad.json")) as
|
|||||||
OFFROAD_ALERTS = json.load(f)
|
OFFROAD_ALERTS = json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def set_offroad_alert(alert: str, show_alert: bool, extra_text: str = None) -> None:
|
def set_offroad_alert(alert: str, show_alert: bool, extra_text: str | None = None) -> None:
|
||||||
if show_alert:
|
if show_alert:
|
||||||
a = copy.copy(OFFROAD_ALERTS[alert])
|
a = copy.copy(OFFROAD_ALERTS[alert])
|
||||||
a['extra'] = extra_text or ''
|
a['extra'] = extra_text or ''
|
||||||
|
|||||||
@@ -254,15 +254,6 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S
|
|||||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
|
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4)
|
||||||
|
|
||||||
|
|
||||||
def steer_saturated_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
|
||||||
steer_text2 = "Steer Left" if sm['carControl'].actuators.torque > 0 else "Steer Right"
|
|
||||||
return Alert(
|
|
||||||
"Take Control",
|
|
||||||
steer_text2,
|
|
||||||
AlertStatus.userPrompt, AlertSize.mid,
|
|
||||||
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.)
|
|
||||||
|
|
||||||
|
|
||||||
def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
|
||||||
first_word = 'Recalibrating' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibrating'
|
first_word = 'Recalibrating' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibrating'
|
||||||
return Alert(
|
return Alert(
|
||||||
@@ -486,6 +477,15 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"),
|
ET.NO_ENTRY: NoEntryAlert("Stock AEB: Risk of Collision"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
EventName.stockLkas: {
|
||||||
|
ET.PERMANENT: Alert(
|
||||||
|
"TAKE CONTROL",
|
||||||
|
"Stock LKAS: Lane Departure Detected",
|
||||||
|
AlertStatus.critical, AlertSize.full,
|
||||||
|
Priority.HIGH, VisualAlert.fcw, AudibleAlert.none, 2.),
|
||||||
|
ET.NO_ENTRY: NoEntryAlert("Stock LKAS: Lane Departure Detected"),
|
||||||
|
},
|
||||||
|
|
||||||
EventName.fcw: {
|
EventName.fcw: {
|
||||||
ET.PERMANENT: Alert(
|
ET.PERMANENT: Alert(
|
||||||
"BRAKE!",
|
"BRAKE!",
|
||||||
@@ -941,13 +941,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
# - CAN data is received, but some message are not received at the right frequency
|
# - CAN data is received, but some message are not received at the right frequency
|
||||||
# If you're not writing a new car port, this is usually cause by faulty wiring
|
# If you're not writing a new car port, this is usually cause by faulty wiring
|
||||||
EventName.canError: {
|
EventName.canError: {
|
||||||
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("CAN Error"),
|
ET.IMMEDIATE_DISABLE: ImmediateDisableAlert("Unknown Vehicle Variant"),
|
||||||
ET.PERMANENT: Alert(
|
ET.PERMANENT: Alert(
|
||||||
"CAN Error: Check Connections",
|
"Unknown Vehicle Variant",
|
||||||
"",
|
"",
|
||||||
AlertStatus.normal, AlertSize.small,
|
AlertStatus.normal, AlertSize.small,
|
||||||
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.),
|
Priority.LOW, VisualAlert.none, AudibleAlert.none, 1., creation_delay=1.),
|
||||||
ET.NO_ENTRY: NoEntryAlert("CAN Error: Check Connections"),
|
ET.NO_ENTRY: NoEntryAlert("Unknown Vehicle Variant"),
|
||||||
},
|
},
|
||||||
|
|
||||||
EventName.canBusMissing: {
|
EventName.canBusMissing: {
|
||||||
@@ -1075,7 +1075,11 @@ if HARDWARE.get_device_type() == 'mici':
|
|||||||
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
|
Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1),
|
||||||
},
|
},
|
||||||
EventName.steerSaturated: {
|
EventName.steerSaturated: {
|
||||||
ET.WARNING: steer_saturated_alert,
|
ET.WARNING: Alert(
|
||||||
|
"take control",
|
||||||
|
"turn exceeds limit",
|
||||||
|
AlertStatus.userPrompt, AlertSize.mid,
|
||||||
|
Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.),
|
||||||
},
|
},
|
||||||
EventName.calibrationIncomplete: {
|
EventName.calibrationIncomplete: {
|
||||||
ET.PERMANENT: calibration_incomplete_alert,
|
ET.PERMANENT: calibration_incomplete_alert,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class FuzzyGenerator:
|
|||||||
except capnp.lib.capnp.KjException:
|
except capnp.lib.capnp.KjException:
|
||||||
return self.generate_struct(field.schema)
|
return self.generate_struct(field.schema)
|
||||||
|
|
||||||
def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str = None) -> st.SearchStrategy[dict[str, Any]]:
|
def generate_struct(self, schema: capnp.lib.capnp._StructSchema, event: str | None = None) -> st.SearchStrategy[dict[str, Any]]:
|
||||||
single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else ()
|
single_fill: tuple[str, ...] = (event,) if event else (self.draw(st.sampled_from(schema.union_fields)),) if schema.union_fields else ()
|
||||||
fields_to_generate = schema.non_union_fields + single_fill
|
fields_to_generate = schema.non_union_fields + single_fill
|
||||||
return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate if not field.endswith('DEPRECATED')})
|
return st.fixed_dictionaries({field: self.generate_field(schema.fields[field]) for field in fields_to_generate if not field.endswith('DEPRECATED')})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from typing import cast
|
||||||
import capnp
|
import capnp
|
||||||
import functools
|
import functools
|
||||||
import traceback
|
import traceback
|
||||||
@@ -67,7 +68,7 @@ def migrate(lr: LogIterable, migration_funcs: list[MigrationFunc]):
|
|||||||
if migration.product in grouped: # skip if product already exists
|
if migration.product in grouped: # skip if product already exists
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sorted_indices = sorted(ii for i in migration.inputs for ii in grouped[i])
|
sorted_indices = sorted(ii for i in cast(list[str], migration.inputs) for ii in grouped.get(i, []))
|
||||||
msg_gen = [(i, lr[i]) for i in sorted_indices]
|
msg_gen = [(i, lr[i]) for i in sorted_indices]
|
||||||
r_ops, a_ops, d_ops = migration(msg_gen)
|
r_ops, a_ops, d_ops = migration(msg_gen)
|
||||||
replace_ops.extend(r_ops)
|
replace_ops.extend(r_ops)
|
||||||
|
|||||||
@@ -610,9 +610,9 @@ def replay_process_with_name(name: str | Iterable[str], lr: LogIterable, *args,
|
|||||||
|
|
||||||
|
|
||||||
def replay_process(
|
def replay_process(
|
||||||
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] = None,
|
cfg: ProcessConfig | Iterable[ProcessConfig], lr: LogIterable, frs: dict[str, FrameReader] | None = None,
|
||||||
fingerprint: str = None, return_all_logs: bool = False, custom_params: dict[str, Any] = None,
|
fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None,
|
||||||
captured_output_store: dict[str, dict[str, str]] = None, disable_progress: bool = False
|
captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
if isinstance(cfg, Iterable):
|
if isinstance(cfg, Iterable):
|
||||||
cfgs = list(cfg)
|
cfgs = list(cfg)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
e0ad86508edb61b3eaa1b84662c515d2c3368295
|
77951c4ccd0916b87c8dfda9faa33cd2d5d2cc11
|
||||||
@@ -16,7 +16,7 @@ from openpilot.tools.lib.openpilotci import get_url
|
|||||||
|
|
||||||
|
|
||||||
def regen_segment(
|
def regen_segment(
|
||||||
lr: LogIterable, frs: dict[str, Any] = None,
|
lr: LogIterable, frs: dict[str, Any] | None = None,
|
||||||
processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False
|
processes: Iterable[ProcessConfig] = CONFIGS, disable_tqdm: bool = False
|
||||||
) -> list[capnp._DynamicStructReader]:
|
) -> list[capnp._DynamicStructReader]:
|
||||||
all_msgs = sorted(lr, key=lambda m: m.logMonoTime)
|
all_msgs = sorted(lr, key=lambda m: m.logMonoTime)
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ segments = [
|
|||||||
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"),
|
||||||
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"),
|
||||||
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"),
|
||||||
("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"),
|
# TODO: get new RAV4 route without enableDsu
|
||||||
|
# ("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"),
|
||||||
("TOYOTA3", "regen1455E3B4BDF|2025-04-09--03-26-06--0"),
|
("TOYOTA3", "regen1455E3B4BDF|2025-04-09--03-26-06--0"),
|
||||||
("HONDA", "regenB328FF8BA0A|2025-04-08--22-57-45--0"),
|
("HONDA", "regenB328FF8BA0A|2025-04-08--22-57-45--0"),
|
||||||
("HONDA2", "regen6170C8C9A35|2025-04-08--22-57-46--0"),
|
("HONDA2", "regen6170C8C9A35|2025-04-08--22-57-46--0"),
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ class TestOnroad:
|
|||||||
params.put_bool("RecordFront", True)
|
params.put_bool("RecordFront", True)
|
||||||
set_params_enabled()
|
set_params_enabled()
|
||||||
os.environ['REPLAY'] = '1'
|
os.environ['REPLAY'] = '1'
|
||||||
|
os.environ['MSGQ_PREALLOC'] = '1'
|
||||||
os.environ['TESTING_CLOSET'] = '1'
|
os.environ['TESTING_CLOSET'] = '1'
|
||||||
if os.path.exists(Paths.log_root()):
|
if os.path.exists(Paths.log_root()):
|
||||||
shutil.rmtree(Paths.log_root())
|
shutil.rmtree(Paths.log_root())
|
||||||
@@ -283,11 +284,12 @@ class TestOnroad:
|
|||||||
print("------------------------------------------------")
|
print("------------------------------------------------")
|
||||||
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
|
offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET)
|
||||||
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
|
mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]]
|
||||||
print("Memory usage: ", mems)
|
print("Overall memory usage: ", mems)
|
||||||
|
print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode())
|
||||||
|
|
||||||
# check for big leaks. note that memory usage is
|
# check for big leaks. note that memory usage is
|
||||||
# expected to go up while the MSGQ buffers fill up
|
# expected to go up while the MSGQ buffers fill up
|
||||||
assert np.average(mems) <= 85, "Average memory usage above 85%"
|
assert np.average(mems) <= 80, "Average memory usage too high"
|
||||||
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
|
assert np.max(np.diff(mems)) <= 4, "Max memory increase too high"
|
||||||
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"
|
assert np.average(np.diff(mems)) <= 1, "Average memory increase too high"
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ SOURCES: list[AzureContainer] = [
|
|||||||
|
|
||||||
DEST = OpenpilotCIContainer
|
DEST = OpenpilotCIContainer
|
||||||
|
|
||||||
def upload_route(path: str, exclude_patterns: Iterable[str] = None) -> None:
|
def upload_route(path: str, exclude_patterns: Iterable[str] | None = None) -> None:
|
||||||
if exclude_patterns is None:
|
if exclude_patterns is None:
|
||||||
exclude_patterns = [r'dcamera\.hevc']
|
exclude_patterns = [r'dcamera\.hevc']
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class HomeLayout(Widget):
|
|||||||
|
|
||||||
self.current_state = HomeLayoutState.HOME
|
self.current_state = HomeLayoutState.HOME
|
||||||
self.last_refresh = 0
|
self.last_refresh = 0
|
||||||
self.settings_callback: callable | None = None
|
self.settings_callback: Callable[[], None] | None = None
|
||||||
|
|
||||||
self.update_available = False
|
self.update_available = False
|
||||||
self.alert_count = 0
|
self.alert_count = 0
|
||||||
|
|||||||
@@ -145,20 +145,18 @@ class SettingsLayout(Widget):
|
|||||||
if panel.instance:
|
if panel.instance:
|
||||||
panel.instance.render(content_rect)
|
panel.instance.render(content_rect)
|
||||||
|
|
||||||
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
|
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
|
||||||
# Check close button
|
# Check close button
|
||||||
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
|
||||||
if self._close_callback:
|
if self._close_callback:
|
||||||
self._close_callback()
|
self._close_callback()
|
||||||
return True
|
return
|
||||||
|
|
||||||
# Check navigation buttons
|
# Check navigation buttons
|
||||||
for panel_type, panel_info in self._panels.items():
|
for panel_type, panel_info in self._panels.items():
|
||||||
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect):
|
||||||
self.set_current_panel(panel_type)
|
self.set_current_panel(panel_type)
|
||||||
return True
|
return
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_current_panel(self, panel_type: PanelType):
|
def set_current_panel(self, panel_type: PanelType):
|
||||||
if panel_type != self._current_panel:
|
if panel_type != self._current_panel:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class PrimeState:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
|
||||||
self.prime_type: PrimeType = self._load_initial_state()
|
self.prime_type: PrimeType = self._load_initial_state()
|
||||||
|
|
||||||
self._running = False
|
self._running = False
|
||||||
@@ -50,7 +52,7 @@ class PrimeState:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
identity_token = get_token(dongle_id)
|
identity_token = get_token(dongle_id)
|
||||||
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token)
|
response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token, session=self._session)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
is_paired = data.get("is_paired", False)
|
is_paired = data.get("is_paired", False)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
import weakref
|
import weakref
|
||||||
|
import math
|
||||||
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.system.hardware import HARDWARE
|
||||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
from openpilot.system.ui.widgets.button import SmallButton
|
from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||||
from openpilot.system.ui.widgets.slider import SmallSlider
|
from openpilot.system.ui.widgets.slider import SmallSlider
|
||||||
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage
|
||||||
@@ -24,11 +27,12 @@ class OnboardingState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class DriverCameraSetupDialog(DriverCameraDialog):
|
class DriverCameraSetupDialog(DriverCameraDialog):
|
||||||
def __init__(self, confirm_callback: Callable):
|
def __init__(self):
|
||||||
super().__init__(no_escape=True)
|
super().__init__(no_escape=True)
|
||||||
self.driver_state_renderer = DriverStateRenderer(confirm_mode=True, confirm_callback=confirm_callback)
|
self.driver_state_renderer = DriverStateRenderer(inset=True)
|
||||||
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200))
|
self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120))
|
||||||
self.driver_state_renderer.load_icons()
|
self.driver_state_renderer.load_icons()
|
||||||
|
self.driver_state_renderer.set_force_active(True)
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||||
@@ -41,15 +45,15 @@ class DriverCameraSetupDialog(DriverCameraDialog):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
# Position dmoji on opposite side from driver
|
# Position dmoji on opposite side from driver
|
||||||
# TODO: we don't have design for RHD yet
|
is_rhd = self.driver_state_renderer.is_rhd
|
||||||
is_rhd = False
|
self.driver_state_renderer.set_position(
|
||||||
driver_state_rect = (
|
rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8,
|
||||||
rect.x if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width,
|
rect.y + 8,
|
||||||
rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2,
|
|
||||||
)
|
)
|
||||||
self.driver_state_renderer.set_position(*driver_state_rect)
|
|
||||||
self.driver_state_renderer.render()
|
self.driver_state_renderer.render()
|
||||||
|
|
||||||
|
self._draw_face_detection(rect)
|
||||||
|
|
||||||
rl.end_scissor_mode()
|
rl.end_scissor_mode()
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
@@ -88,18 +92,54 @@ class TrainingGuidePreDMTutorial(SetupTermsPage):
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class DMBadFaceDetected(SetupTermsPage):
|
||||||
|
def __init__(self, continue_callback, back_callback):
|
||||||
|
super().__init__(continue_callback, back_callback, continue_text="power off")
|
||||||
|
self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60))
|
||||||
|
self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _content_height(self):
|
||||||
|
return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset()
|
||||||
|
|
||||||
|
def _render_content(self, scroll_offset):
|
||||||
|
self._title_header.render(rl.Rectangle(
|
||||||
|
self._rect.x + 16,
|
||||||
|
self._rect.y + 16 + scroll_offset,
|
||||||
|
self._title_header.rect.width,
|
||||||
|
self._title_header.rect.height,
|
||||||
|
))
|
||||||
|
|
||||||
|
self._dm_label.render(rl.Rectangle(
|
||||||
|
self._rect.x + 16,
|
||||||
|
self._title_header.rect.y + self._title_header.rect.height + 16,
|
||||||
|
self._rect.width - 32,
|
||||||
|
self._dm_label.get_content_height(int(self._rect.width - 32)),
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
class TrainingGuideDMTutorial(Widget):
|
class TrainingGuideDMTutorial(Widget):
|
||||||
|
PROGRESS_DURATION = 4
|
||||||
|
LOOKING_THRESHOLD_DEG = 30.0
|
||||||
|
|
||||||
def __init__(self, continue_callback):
|
def __init__(self, continue_callback):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60))
|
self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48))
|
||||||
|
self._back_button.set_click_callback(self._show_bad_face_page)
|
||||||
|
self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35))
|
||||||
|
|
||||||
# Wrap the continue callback to restore settings
|
# Wrap the continue callback to restore settings
|
||||||
def wrapped_continue_callback():
|
def wrapped_continue_callback():
|
||||||
device.set_offroad_brightness(None)
|
device.set_offroad_brightness(None)
|
||||||
device.reset_interactive_timeout()
|
|
||||||
continue_callback()
|
continue_callback()
|
||||||
|
|
||||||
self._dialog = DriverCameraSetupDialog(wrapped_continue_callback)
|
self._good_button.set_click_callback(wrapped_continue_callback)
|
||||||
|
self._good_button.set_enabled(False)
|
||||||
|
|
||||||
|
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||||
|
self._dialog = DriverCameraSetupDialog()
|
||||||
|
self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page)
|
||||||
|
self._should_show_bad_face_page = False
|
||||||
|
|
||||||
# Disable driver monitoring model when device times out for inactivity
|
# Disable driver monitoring model when device times out for inactivity
|
||||||
def inactivity_callback():
|
def inactivity_callback():
|
||||||
@@ -107,30 +147,113 @@ class TrainingGuideDMTutorial(Widget):
|
|||||||
|
|
||||||
device.add_interactive_timeout_callback(inactivity_callback)
|
device.add_interactive_timeout_callback(inactivity_callback)
|
||||||
|
|
||||||
|
def _show_bad_face_page(self):
|
||||||
|
self._bad_face_page.show_event()
|
||||||
|
self.hide_event()
|
||||||
|
self._should_show_bad_face_page = True
|
||||||
|
|
||||||
|
def _hide_bad_face_page(self):
|
||||||
|
self._bad_face_page.hide_event()
|
||||||
|
self.show_event()
|
||||||
|
self._should_show_bad_face_page = False
|
||||||
|
|
||||||
def show_event(self):
|
def show_event(self):
|
||||||
super().show_event()
|
super().show_event()
|
||||||
self._dialog.show_event()
|
self._dialog.show_event()
|
||||||
|
self._progress.x = 0.0
|
||||||
|
|
||||||
device.set_offroad_brightness(100)
|
device.set_offroad_brightness(100)
|
||||||
device.reset_interactive_timeout(300) # 5 minutes
|
|
||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
super()._update_state()
|
super()._update_state()
|
||||||
if device.awake:
|
if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"):
|
||||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
ui_state.params.put_bool_nonblocking("IsDriverViewEnabled", True)
|
||||||
|
|
||||||
|
sm = ui_state.sm
|
||||||
|
if sm.recv_frame.get("driverMonitoringState", 0) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
dm_state = sm["driverMonitoringState"]
|
||||||
|
driver_data = self._dialog.driver_state_renderer.get_driver_data()
|
||||||
|
|
||||||
|
if len(driver_data.faceOrientation) == 3:
|
||||||
|
pitch, yaw, _ = driver_data.faceOrientation
|
||||||
|
looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG
|
||||||
|
else:
|
||||||
|
looking_center = False
|
||||||
|
|
||||||
|
# stay at 100% once reached
|
||||||
|
if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99:
|
||||||
|
slow = self._progress.x < 0.25
|
||||||
|
duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION
|
||||||
|
self._progress.x += 1.0 / (duration * gui_app.target_fps)
|
||||||
|
self._progress.x = min(1.0, self._progress.x)
|
||||||
|
else:
|
||||||
|
self._progress.update(0.0)
|
||||||
|
|
||||||
|
self._good_button.set_enabled(self._progress.x >= 0.999)
|
||||||
|
|
||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
|
if self._should_show_bad_face_page:
|
||||||
|
return self._bad_face_page.render(self._rect)
|
||||||
|
|
||||||
self._dialog.render(self._rect)
|
self._dialog.render(self._rect)
|
||||||
|
|
||||||
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - self._title_header.rect.height * 1.5 - 32),
|
rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80),
|
||||||
int(self._rect.width), int(self._title_header.rect.height * 1.5 + 32),
|
int(self._rect.width), 80, rl.BLANK, rl.BLACK)
|
||||||
rl.BLANK, rl.Color(0, 0, 0, 150))
|
|
||||||
self._title_header.render(rl.Rectangle(
|
# draw white ring around dm icon to indicate progress
|
||||||
self._rect.x + 16,
|
ring_thickness = 8
|
||||||
self._rect.y + self._rect.height - self._title_header.rect.height - 16,
|
|
||||||
self._title_header.rect.width,
|
# DM icon is 120x120, positioned on opposite side from driver
|
||||||
self._title_header.rect.height,
|
dm_size = 120
|
||||||
))
|
is_rhd = self._dialog.driver_state_renderer._is_rhd
|
||||||
|
dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8)
|
||||||
|
dm_center_y = self._rect.y + dm_size / 2 + 8
|
||||||
|
icon_edge_radius = dm_size / 2
|
||||||
|
outer_radius = icon_edge_radius + 1 # 2px outward from icon edge
|
||||||
|
inner_radius = outer_radius - ring_thickness # Inset by ring_thickness
|
||||||
|
start_angle = 90.0 # Start from bottom
|
||||||
|
end_angle = start_angle + self._progress.x * 360.0 # Clockwise
|
||||||
|
|
||||||
|
# Fade in alpha
|
||||||
|
current_angle = end_angle - start_angle
|
||||||
|
alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255]))
|
||||||
|
|
||||||
|
# White to green
|
||||||
|
color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0)
|
||||||
|
r = int(np.interp(color_t, [0.0, 1.0], [255, 0]))
|
||||||
|
g = int(np.interp(color_t, [0.0, 1.0], [255, 255]))
|
||||||
|
b = int(np.interp(color_t, [0.0, 1.0], [255, 64]))
|
||||||
|
ring_color = rl.Color(r, g, b, alpha)
|
||||||
|
|
||||||
|
rl.draw_ring(
|
||||||
|
rl.Vector2(dm_center_x, dm_center_y),
|
||||||
|
inner_radius,
|
||||||
|
outer_radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
36,
|
||||||
|
ring_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._dialog._camera_view.frame:
|
||||||
|
self._back_button.render(rl.Rectangle(
|
||||||
|
self._rect.x + 8,
|
||||||
|
self._rect.y + self._rect.height - self._back_button.rect.height,
|
||||||
|
self._back_button.rect.width,
|
||||||
|
self._back_button.rect.height,
|
||||||
|
))
|
||||||
|
|
||||||
|
self._good_button.render(rl.Rectangle(
|
||||||
|
self._rect.x + self._rect.width - self._good_button.rect.width - 8,
|
||||||
|
self._rect.y + self._rect.height - self._good_button.rect.height,
|
||||||
|
self._good_button.rect.width,
|
||||||
|
self._good_button.rect.height,
|
||||||
|
))
|
||||||
|
|
||||||
|
# rounded border
|
||||||
|
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK)
|
||||||
|
|
||||||
|
|
||||||
class TrainingGuideRecordFront(SetupTermsPage):
|
class TrainingGuideRecordFront(SetupTermsPage):
|
||||||
@@ -223,6 +346,14 @@ class TrainingGuide(Widget):
|
|||||||
TrainingGuideRecordFront(continue_callback=on_continue),
|
TrainingGuideRecordFront(continue_callback=on_continue),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
super().show_event()
|
||||||
|
device.set_override_interactive_timeout(300)
|
||||||
|
|
||||||
|
def hide_event(self):
|
||||||
|
super().hide_event()
|
||||||
|
device.set_override_interactive_timeout(None)
|
||||||
|
|
||||||
def _advance_step(self):
|
def _advance_step(self):
|
||||||
if self._step < len(self._steps) - 1:
|
if self._step < len(self._steps) - 1:
|
||||||
self._step += 1
|
self._step += 1
|
||||||
@@ -319,6 +450,14 @@ class OnboardingWindow(Widget):
|
|||||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
super().show_event()
|
||||||
|
device.set_override_interactive_timeout(300)
|
||||||
|
|
||||||
|
def hide_event(self):
|
||||||
|
super().hide_event()
|
||||||
|
device.set_override_interactive_timeout(None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def completed(self) -> bool:
|
def completed(self) -> bool:
|
||||||
return self._accepted_terms and self._training_done
|
return self._accepted_terms and self._training_done
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import requests
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
@@ -44,6 +45,7 @@ class FirehoseLayoutBase(Widget):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._params = Params()
|
self._params = Params()
|
||||||
|
self._session = requests.Session() # reuse session to reduce SSL handshake overhead
|
||||||
self._segment_count = self._get_segment_count()
|
self._segment_count = self._get_segment_count()
|
||||||
|
|
||||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||||
@@ -203,7 +205,7 @@ class FirehoseLayoutBase(Widget):
|
|||||||
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID:
|
||||||
return
|
return
|
||||||
identity_token = get_token(dongle_id)
|
identity_token = get_token(dongle_id)
|
||||||
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token)
|
response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token, session=self._session)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self._segment_count = data.get("firehose", 0)
|
self._segment_count = data.get("firehose", 0)
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ class NetworkLayoutMici(NavWidget):
|
|||||||
}.get(self._wifi_manager.current_network_metered, 'default'))
|
}.get(self._wifi_manager.current_network_metered, 'default'))
|
||||||
|
|
||||||
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
def _switch_to_panel(self, panel_type: NetworkPanelType):
|
||||||
|
if panel_type == NetworkPanelType.WIFI:
|
||||||
|
self._wifi_ui.show_event()
|
||||||
self._current_panel = panel_type
|
self._current_panel = panel_type
|
||||||
|
|
||||||
def _render(self, rect: rl.Rectangle):
|
def _render(self, rect: rl.Rectangle):
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ class WifiIcon(Widget):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.set_rect(rl.Rectangle(0, 0, 89, 64))
|
self.set_rect(rl.Rectangle(0, 0, 89, 64))
|
||||||
|
|
||||||
self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 89, 64)
|
|
||||||
self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 89, 64)
|
|
||||||
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64)
|
self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 89, 64)
|
||||||
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64)
|
self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 89, 64)
|
||||||
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64)
|
self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 89, 64)
|
||||||
@@ -57,17 +55,13 @@ class WifiIcon(Widget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Determine which wifi strength icon to use
|
# Determine which wifi strength icon to use
|
||||||
strength = round(self._network.strength / 100 * 4)
|
strength = round(self._network.strength / 100 * 2)
|
||||||
if strength == 4:
|
if strength == 2:
|
||||||
strength_icon = self._wifi_full_txt
|
strength_icon = self._wifi_full_txt
|
||||||
elif strength == 3:
|
elif strength == 1:
|
||||||
strength_icon = self._wifi_medium_txt
|
strength_icon = self._wifi_medium_txt
|
||||||
elif strength == 2:
|
|
||||||
strength_icon = self._wifi_low_txt
|
|
||||||
elif self._network.strength < 0:
|
|
||||||
strength_icon = self._wifi_slash_txt
|
|
||||||
else:
|
else:
|
||||||
strength_icon = self._wifi_none_txt
|
strength_icon = self._wifi_low_txt
|
||||||
|
|
||||||
icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2)
|
icon_x = int(self._rect.x + (self._rect.width - strength_icon.width * self._scale) // 2)
|
||||||
icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
|
icon_y = int(self._rect.y + (self._rect.height - strength_icon.height * self._scale) // 2)
|
||||||
@@ -83,8 +77,6 @@ class WifiIcon(Widget):
|
|||||||
|
|
||||||
class WifiItem(BigDialogOptionButton):
|
class WifiItem(BigDialogOptionButton):
|
||||||
LEFT_MARGIN = 20
|
LEFT_MARGIN = 20
|
||||||
HEIGHT = 54
|
|
||||||
SELECTED_HEIGHT = 74
|
|
||||||
|
|
||||||
def __init__(self, network: Network):
|
def __init__(self, network: Network):
|
||||||
super().__init__(network.ssid)
|
super().__init__(network.ssid)
|
||||||
@@ -97,10 +89,6 @@ class WifiItem(BigDialogOptionButton):
|
|||||||
self._wifi_icon = WifiIcon()
|
self._wifi_icon = WifiIcon()
|
||||||
self._wifi_icon.set_current_network(network)
|
self._wifi_icon.set_current_network(network)
|
||||||
|
|
||||||
def set_selected(self, selected: bool):
|
|
||||||
super().set_selected(selected)
|
|
||||||
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
|
|
||||||
|
|
||||||
def set_current_network(self, network: Network):
|
def set_current_network(self, network: Network):
|
||||||
self._network = network
|
self._network = network
|
||||||
self._wifi_icon.set_current_network(network)
|
self._wifi_icon.set_current_network(network)
|
||||||
@@ -120,11 +108,11 @@ class WifiItem(BigDialogOptionButton):
|
|||||||
))
|
))
|
||||||
|
|
||||||
if self._selected:
|
if self._selected:
|
||||||
self._label.set_font_size(74)
|
self._label.set_font_size(self.SELECTED_HEIGHT)
|
||||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
self._label.set_font_weight(FontWeight.DISPLAY)
|
self._label.set_font_weight(FontWeight.DISPLAY)
|
||||||
else:
|
else:
|
||||||
self._label.set_font_size(54)
|
self._label.set_font_size(self.HEIGHT)
|
||||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
||||||
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||||
|
|
||||||
@@ -322,6 +310,9 @@ class NetworkInfoPage(NavWidget):
|
|||||||
|
|
||||||
|
|
||||||
class WifiUIMici(BigMultiOptionDialog):
|
class WifiUIMici(BigMultiOptionDialog):
|
||||||
|
# Wait this long after user interacts with widget to update network list
|
||||||
|
INACTIVITY_TIMEOUT = 1
|
||||||
|
|
||||||
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
|
def __init__(self, wifi_manager: WifiManager, back_callback: Callable):
|
||||||
super().__init__([], None, None, right_btn_callback=None)
|
super().__init__([], None, None, right_btn_callback=None)
|
||||||
|
|
||||||
@@ -330,7 +321,6 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
|
|
||||||
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page)
|
self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page)
|
||||||
self._network_info_page.set_connecting(lambda: self._connecting)
|
self._network_info_page.set_connecting(lambda: self._connecting)
|
||||||
self._should_open_network_info_page = False # wait for scroll_to animation
|
|
||||||
|
|
||||||
self._loading_animation = LoadingAnimation()
|
self._loading_animation = LoadingAnimation()
|
||||||
|
|
||||||
@@ -338,6 +328,10 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
self._connecting: str | None = None
|
self._connecting: str | None = None
|
||||||
self._networks: dict[str, Network] = {}
|
self._networks: dict[str, Network] = {}
|
||||||
|
|
||||||
|
# widget state
|
||||||
|
self._last_interaction_time = -float('inf')
|
||||||
|
self._restore_selection = False
|
||||||
|
|
||||||
self._wifi_manager.add_callbacks(
|
self._wifi_manager.add_callbacks(
|
||||||
need_auth=self._on_need_auth,
|
need_auth=self._on_need_auth,
|
||||||
activated=self._on_activated,
|
activated=self._on_activated,
|
||||||
@@ -350,18 +344,12 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
# Call super to prepare scroller; selection scroll is handled dynamically
|
# Call super to prepare scroller; selection scroll is handled dynamically
|
||||||
super().show_event()
|
super().show_event()
|
||||||
self._wifi_manager.set_active(True)
|
self._wifi_manager.set_active(True)
|
||||||
self._scroller.show_event()
|
self._last_interaction_time = -float('inf')
|
||||||
|
|
||||||
def hide_event(self):
|
def hide_event(self):
|
||||||
super().hide_event()
|
super().hide_event()
|
||||||
self._wifi_manager.set_active(False)
|
self._wifi_manager.set_active(False)
|
||||||
|
|
||||||
def _update_state(self):
|
|
||||||
super()._update_state()
|
|
||||||
if self._should_open_network_info_page:
|
|
||||||
self._should_open_network_info_page = False
|
|
||||||
self._open_network_manage_page()
|
|
||||||
|
|
||||||
def _open_network_manage_page(self, result=None):
|
def _open_network_manage_page(self, result=None):
|
||||||
self._network_info_page.update_networks(self._networks)
|
self._network_info_page.update_networks(self._networks)
|
||||||
gui_app.set_modal_overlay(self._network_info_page)
|
gui_app.set_modal_overlay(self._network_info_page)
|
||||||
@@ -380,6 +368,10 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
self._network_info_page.update_networks(self._networks)
|
self._network_info_page.update_networks(self._networks)
|
||||||
|
|
||||||
def _update_buttons(self):
|
def _update_buttons(self):
|
||||||
|
# Don't update buttons while user is actively interacting
|
||||||
|
if rl.get_time() - self._last_interaction_time < self.INACTIVITY_TIMEOUT:
|
||||||
|
return
|
||||||
|
|
||||||
for network in self._networks.values():
|
for network in self._networks.values():
|
||||||
# pop and re-insert to eliminate stuttering on update (prevents position lost for a frame)
|
# pop and re-insert to eliminate stuttering on update (prevents position lost for a frame)
|
||||||
network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None)
|
network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None)
|
||||||
@@ -390,23 +382,27 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
else:
|
else:
|
||||||
network_button = WifiItem(network)
|
network_button = WifiItem(network)
|
||||||
|
|
||||||
def show_network_info_page(_network):
|
self._scroller.add_widget(network_button)
|
||||||
self._network_info_page.set_current_network(_network)
|
|
||||||
self._should_open_network_info_page = True
|
|
||||||
|
|
||||||
network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net))
|
|
||||||
|
|
||||||
self.add_button(network_button)
|
|
||||||
|
|
||||||
# remove networks no longer present
|
# remove networks no longer present
|
||||||
self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks]
|
self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks]
|
||||||
|
|
||||||
|
# try to restore previous selection to prevent jumping from adding/removing/reordering buttons
|
||||||
|
self._restore_selection = True
|
||||||
|
|
||||||
def _connect_with_password(self, ssid: str, password: str):
|
def _connect_with_password(self, ssid: str, password: str):
|
||||||
if password:
|
if password:
|
||||||
self._connecting = ssid
|
self._connecting = ssid
|
||||||
self._wifi_manager.connect_to_network(ssid, password)
|
self._wifi_manager.connect_to_network(ssid, password)
|
||||||
self._update_buttons()
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _on_option_selected(self, option: str):
|
||||||
|
super()._on_option_selected(option)
|
||||||
|
|
||||||
|
if option in self._networks:
|
||||||
|
self._network_info_page.set_current_network(self._networks[option])
|
||||||
|
self._open_network_manage_page()
|
||||||
|
|
||||||
def _connect_to_network(self, ssid: str):
|
def _connect_to_network(self, ssid: str):
|
||||||
network = self._networks.get(ssid)
|
network = self._networks.get(ssid)
|
||||||
if network is None:
|
if network is None:
|
||||||
@@ -440,7 +436,19 @@ class WifiUIMici(BigMultiOptionDialog):
|
|||||||
def _on_disconnected(self):
|
def _on_disconnected(self):
|
||||||
self._connecting = None
|
self._connecting = None
|
||||||
|
|
||||||
|
def _update_state(self):
|
||||||
|
super()._update_state()
|
||||||
|
if self.is_pressed:
|
||||||
|
self._last_interaction_time = rl.get_time()
|
||||||
|
|
||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
|
# Update Scroller layout and restore current selection whenever buttons are updated, before first render
|
||||||
|
current_selection = self.get_selected_option()
|
||||||
|
if self._restore_selection and current_selection in self._networks:
|
||||||
|
self._scroller._layout()
|
||||||
|
BigMultiOptionDialog._on_option_selected(self, current_selection)
|
||||||
|
self._restore_selection = None
|
||||||
|
|
||||||
super()._render(_)
|
super()._render(_)
|
||||||
|
|
||||||
if not self._networks:
|
if not self._networks:
|
||||||
|
|||||||
@@ -78,13 +78,13 @@ class TogglesLayoutMici(NavWidget):
|
|||||||
# CP gating for experimental mode
|
# CP gating for experimental mode
|
||||||
if ui_state.CP is not None:
|
if ui_state.CP is not None:
|
||||||
if ui_state.has_longitudinal_control:
|
if ui_state.has_longitudinal_control:
|
||||||
self._experimental_btn.set_enabled(True)
|
self._experimental_btn.set_visible(True)
|
||||||
self._personality_toggle.set_enabled(True)
|
self._personality_toggle.set_visible(True)
|
||||||
else:
|
else:
|
||||||
# no long for now
|
# no long for now
|
||||||
self._experimental_btn.set_enabled(False)
|
self._experimental_btn.set_visible(False)
|
||||||
self._experimental_btn.set_checked(False)
|
self._experimental_btn.set_checked(False)
|
||||||
self._personality_toggle.set_enabled(False)
|
self._personality_toggle.set_visible(False)
|
||||||
ui_state.params.remove("ExperimentalMode")
|
ui_state.params.remove("ExperimentalMode")
|
||||||
|
|
||||||
# Refresh toggles from params to mirror external changes
|
# Refresh toggles from params to mirror external changes
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ class DriverCameraDialog(NavWidget):
|
|||||||
super().show_event()
|
super().show_event()
|
||||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||||
self._publish_alert_sound(None)
|
self._publish_alert_sound(None)
|
||||||
device.reset_interactive_timeout(300)
|
device.set_override_interactive_timeout(300)
|
||||||
ui_state.params.remove("DriverTooDistracted")
|
ui_state.params.remove("DriverTooDistracted")
|
||||||
self._pm = messaging.PubMaster(['selfdriveState'])
|
self._pm = messaging.PubMaster(['selfdriveState'])
|
||||||
|
|
||||||
def hide_event(self):
|
def hide_event(self):
|
||||||
super().hide_event()
|
super().hide_event()
|
||||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||||
device.reset_interactive_timeout()
|
device.set_override_interactive_timeout(None)
|
||||||
|
|
||||||
def _handle_mouse_release(self, _):
|
def _handle_mouse_release(self, _):
|
||||||
ui_state.params.remove("DriverTooDistracted")
|
ui_state.params.remove("DriverTooDistracted")
|
||||||
@@ -89,12 +89,13 @@ class DriverCameraDialog(NavWidget):
|
|||||||
self._publish_alert_sound(None)
|
self._publish_alert_sound(None)
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
self._draw_face_detection(rect)
|
driver_data = self._draw_face_detection(rect)
|
||||||
|
if driver_data is not None:
|
||||||
|
self._draw_eyes(rect, driver_data)
|
||||||
|
|
||||||
# Position dmoji on opposite side from driver
|
# Position dmoji on opposite side from driver
|
||||||
dm_state = ui_state.sm["driverMonitoringState"]
|
|
||||||
driver_state_rect = (
|
driver_state_rect = (
|
||||||
rect.x if dm_state.isRHD else rect.x + rect.width - self.driver_state_renderer.rect.width,
|
rect.x if self.driver_state_renderer.is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width,
|
||||||
rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2,
|
rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2,
|
||||||
)
|
)
|
||||||
self.driver_state_renderer.set_position(*driver_state_rect)
|
self.driver_state_renderer.set_position(*driver_state_rect)
|
||||||
@@ -138,7 +139,7 @@ class DriverCameraDialog(NavWidget):
|
|||||||
|
|
||||||
# Show first event (only one should be active at a time)
|
# Show first event (only one should be active at a time)
|
||||||
event_name_str = str(dm_state.events[0].name).split('.')[-1]
|
event_name_str = str(dm_state.events[0].name).split('.')[-1]
|
||||||
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if dm_state.isRHD else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT
|
||||||
|
|
||||||
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height)
|
||||||
gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
|
gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD,
|
||||||
@@ -159,12 +160,10 @@ class DriverCameraDialog(NavWidget):
|
|||||||
if self._glasses_texture is None:
|
if self._glasses_texture is None:
|
||||||
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
||||||
|
|
||||||
def _draw_face_detection(self, rect: rl.Rectangle) -> None:
|
def _draw_face_detection(self, rect: rl.Rectangle):
|
||||||
driver_state = ui_state.sm["driverStateV2"]
|
dm_state = ui_state.sm["driverMonitoringState"]
|
||||||
is_rhd = driver_state.wheelOnRightProb > 0.5
|
driver_data = self.driver_state_renderer.get_driver_data()
|
||||||
driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData
|
if not dm_state.faceDetected:
|
||||||
face_detect = driver_data.faceProb > 0.7
|
|
||||||
if not face_detect:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get face position and orientation
|
# Get face position and orientation
|
||||||
@@ -188,7 +187,7 @@ class DriverCameraDialog(NavWidget):
|
|||||||
scale_y = rect.height / 1080.0
|
scale_y = rect.height / 1080.0
|
||||||
fbox_x = rect.x + rect.width / 2 + offset_x * scale_x
|
fbox_x = rect.x + rect.width / 2 + offset_x * scale_x
|
||||||
fbox_y = rect.y + rect.height / 2 + offset_y * scale_y
|
fbox_y = rect.y + rect.height / 2 + offset_y * scale_y
|
||||||
box_size = 50
|
box_size = 75
|
||||||
line_thickness = 3
|
line_thickness = 3
|
||||||
|
|
||||||
line_color = rl.Color(255, 255, 255, int(alpha * 255))
|
line_color = rl.Color(255, 255, 255, int(alpha * 255))
|
||||||
@@ -199,7 +198,9 @@ class DriverCameraDialog(NavWidget):
|
|||||||
line_thickness,
|
line_thickness,
|
||||||
line_color,
|
line_color,
|
||||||
)
|
)
|
||||||
|
return driver_data
|
||||||
|
|
||||||
|
def _draw_eyes(self, rect: rl.Rectangle, driver_data):
|
||||||
# Draw eye indicators based on eye probabilities
|
# Draw eye indicators based on eye probabilities
|
||||||
eye_offset_x = 10
|
eye_offset_x = 10
|
||||||
eye_offset_y = 10
|
eye_offset_y = 10
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import pyray as rl
|
import pyray as rl
|
||||||
from collections.abc import Callable
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import math
|
import math
|
||||||
from cereal import log
|
from cereal import log
|
||||||
from openpilot.system.hardware import PC
|
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
from openpilot.system.ui.widgets import Widget
|
from openpilot.system.ui.widgets import Widget
|
||||||
@@ -22,15 +20,11 @@ class DriverStateRenderer(Widget):
|
|||||||
LINES_ANGLE_INCREMENT = 5
|
LINES_ANGLE_INCREMENT = 5
|
||||||
LINES_STALE_ANGLES = 3.0 # seconds
|
LINES_STALE_ANGLES = 3.0 # seconds
|
||||||
|
|
||||||
def __init__(self, lines: bool = False, confirm_mode: bool = False, confirm_callback: Callable | None = None):
|
def __init__(self, lines: bool = False, inset: bool = False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.set_rect(rl.Rectangle(0, 0, self.BASE_SIZE, self.BASE_SIZE))
|
self.set_rect(rl.Rectangle(0, 0, self.BASE_SIZE, self.BASE_SIZE))
|
||||||
self._lines = lines or confirm_mode
|
self._lines = lines
|
||||||
|
self._inset = inset
|
||||||
# In confirm mode, user must fill out the circle to confirm some action in the UI
|
|
||||||
self._confirm_mode = confirm_mode
|
|
||||||
self._confirm_callback = confirm_callback
|
|
||||||
self._confirm_angles: dict[int, float] = {} # angle: timestamp
|
|
||||||
|
|
||||||
# In line mode, track smoothed angles
|
# In line mode, track smoothed angles
|
||||||
assert 360 % self.LINES_ANGLE_INCREMENT == 0
|
assert 360 % self.LINES_ANGLE_INCREMENT == 0
|
||||||
@@ -54,12 +48,20 @@ class DriverStateRenderer(Widget):
|
|||||||
|
|
||||||
def load_icons(self):
|
def load_icons(self):
|
||||||
"""Load or reload the driver face icon texture"""
|
"""Load or reload the driver face icon texture"""
|
||||||
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", self._rect.width, self._rect.height)
|
cone_and_person_size = round(52 / self.BASE_SIZE * self._rect.width)
|
||||||
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", self._rect.width, self._rect.height)
|
|
||||||
|
# If inset is enabled, push cone and person smaller by 2x the current inset space
|
||||||
|
if self._inset:
|
||||||
|
# Current inset space = (rect.width - cone_and_person_size) / 2
|
||||||
|
current_inset = (self._rect.width - cone_and_person_size) / 2
|
||||||
|
# Reduce size by 2x the current inset (1x on each side)
|
||||||
|
cone_and_person_size = round(cone_and_person_size - current_inset * 2)
|
||||||
|
|
||||||
|
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size)
|
||||||
|
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
|
||||||
center_size = round(36 / self.BASE_SIZE * self._rect.width)
|
center_size = round(36 / self.BASE_SIZE * self._rect.width)
|
||||||
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
|
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
|
||||||
background_size = round(52 / self.BASE_SIZE * self._rect.width)
|
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height)
|
||||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", background_size, background_size)
|
|
||||||
|
|
||||||
def set_should_draw(self, should_draw: bool):
|
def set_should_draw(self, should_draw: bool):
|
||||||
self._should_draw = should_draw
|
self._should_draw = should_draw
|
||||||
@@ -78,16 +80,22 @@ class DriverStateRenderer(Widget):
|
|||||||
"""Returns True if dmoji should appear active (either actually active or forced)"""
|
"""Returns True if dmoji should appear active (either actually active or forced)"""
|
||||||
return bool(self._force_active or self._is_active)
|
return bool(self._force_active or self._is_active)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rhd(self) -> bool:
|
||||||
|
return self._is_rhd
|
||||||
|
|
||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED)
|
rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED)
|
||||||
|
|
||||||
rl.draw_texture(self._dm_background,
|
rl.draw_texture(self._dm_background,
|
||||||
int(self._rect.x + (self._rect.width - self._dm_background.width) / 2),
|
int(self._rect.x),
|
||||||
int(self._rect.y + (self._rect.height - self._dm_background.height) / 2),
|
int(self._rect.y),
|
||||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)))
|
||||||
|
|
||||||
rl.draw_texture(self._dm_person, int(self._rect.x), int(self._rect.y),
|
rl.draw_texture(self._dm_person,
|
||||||
|
int(self._rect.x + (self._rect.width - self._dm_person.width) / 2),
|
||||||
|
int(self._rect.y + (self._rect.height - self._dm_person.height) / 2),
|
||||||
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x)))
|
||||||
|
|
||||||
if self.effective_active:
|
if self.effective_active:
|
||||||
@@ -120,38 +128,18 @@ class DriverStateRenderer(Widget):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# remove old angles
|
# remove old angles
|
||||||
now = rl.get_time()
|
|
||||||
self._confirm_angles = {angle: t for angle, t in self._confirm_angles.items() if now - t < self.LINES_STALE_ANGLES}
|
|
||||||
|
|
||||||
looking_center = self._looking_center_filter.x > 0.2
|
|
||||||
for angle, f in self._head_angles.items():
|
for angle, f in self._head_angles.items():
|
||||||
dst_from_current = ((angle - self._rotation_filter.x) % 360) - 180
|
dst_from_current = ((angle - self._rotation_filter.x) % 360) - 180
|
||||||
target = 1.0 if abs(dst_from_current) <= self.LINES_ANGLE_INCREMENT * 5 else 0.0
|
target = 1.0 if abs(dst_from_current) <= self.LINES_ANGLE_INCREMENT * 5 else 0.0
|
||||||
if not self._face_detected:
|
if not self._face_detected:
|
||||||
target = 0.0
|
target = 0.0
|
||||||
|
|
||||||
if self._confirm_mode:
|
|
||||||
# Extra careful to not add angles when looking near center
|
|
||||||
if target > 0 and not looking_center:
|
|
||||||
self._confirm_angles[angle] = now
|
|
||||||
|
|
||||||
# User is looking at area already confirmed, reduce target to indicate where they are
|
|
||||||
if angle in self._confirm_angles and target == 0:
|
|
||||||
target = 0.65
|
|
||||||
|
|
||||||
# Reduce all line lengths when looking center
|
# Reduce all line lengths when looking center
|
||||||
if self._looking_center:
|
if self._looking_center:
|
||||||
target = np.interp(self._looking_center_filter.x, [0.0, 1.0], [target, 0.45])
|
target = np.interp(self._looking_center_filter.x, [0.0, 1.0], [target, 0.45])
|
||||||
|
|
||||||
f.update(target)
|
f.update(target)
|
||||||
self._draw_line(angle, f, self._looking_center and angle not in self._confirm_angles)
|
self._draw_line(angle, f, self._looking_center)
|
||||||
|
|
||||||
# if all lines placed, reset for next time and call callback
|
|
||||||
if self._confirm_mode:
|
|
||||||
if len(self._confirm_angles) >= 360 // self.LINES_ANGLE_INCREMENT:
|
|
||||||
self._confirm_angles = {}
|
|
||||||
if self._confirm_callback is not None:
|
|
||||||
self._confirm_callback()
|
|
||||||
|
|
||||||
def _draw_line(self, angle: int, f: FirstOrderFilter, grey: bool):
|
def _draw_line(self, angle: int, f: FirstOrderFilter, grey: bool):
|
||||||
line_length = self._rect.width / 6
|
line_length = self._rect.width / 6
|
||||||
@@ -171,10 +159,9 @@ class DriverStateRenderer(Widget):
|
|||||||
if f.x > 0.01:
|
if f.x > 0.01:
|
||||||
rl.draw_line_ex((start_x, start_y), (end_x, end_y), 12, color)
|
rl.draw_line_ex((start_x, start_y), (end_x, end_y), 12, color)
|
||||||
|
|
||||||
def _update_state(self):
|
def get_driver_data(self):
|
||||||
sm = ui_state.sm
|
sm = ui_state.sm
|
||||||
|
|
||||||
# Get monitoring state
|
|
||||||
dm_state = sm["driverMonitoringState"]
|
dm_state = sm["driverMonitoringState"]
|
||||||
self._is_active = dm_state.isActiveMode
|
self._is_active = dm_state.isActiveMode
|
||||||
self._is_rhd = dm_state.isRHD
|
self._is_rhd = dm_state.isRHD
|
||||||
@@ -182,6 +169,11 @@ class DriverStateRenderer(Widget):
|
|||||||
|
|
||||||
driverstate = sm["driverStateV2"]
|
driverstate = sm["driverStateV2"]
|
||||||
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
|
driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData
|
||||||
|
return driver_data
|
||||||
|
|
||||||
|
def _update_state(self):
|
||||||
|
# Get monitoring state
|
||||||
|
driver_data = self.get_driver_data()
|
||||||
driver_orient = driver_data.faceOrientation
|
driver_orient = driver_data.faceOrientation
|
||||||
|
|
||||||
if len(driver_orient) != 3:
|
if len(driver_orient) != 3:
|
||||||
@@ -218,10 +210,7 @@ class DriverStateRenderer(Widget):
|
|||||||
rotation = math.degrees(math.atan2(pitch, yaw))
|
rotation = math.degrees(math.atan2(pitch, yaw))
|
||||||
angle_diff = rotation - self._rotation_filter.x
|
angle_diff = rotation - self._rotation_filter.x
|
||||||
angle_diff = ((angle_diff + 180) % 360) - 180
|
angle_diff = ((angle_diff + 180) % 360) - 180
|
||||||
if PC and self._confirm_mode:
|
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||||
self._rotation_filter.x += 2
|
|
||||||
else:
|
|
||||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
|
||||||
|
|
||||||
if not self.should_draw:
|
if not self.should_draw:
|
||||||
self._fade_filter.update(0.0)
|
self._fade_filter.update(0.0)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class BigCircleButton(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class BigCircleToggle(BigCircleButton):
|
class BigCircleToggle(BigCircleButton):
|
||||||
def __init__(self, icon: str, toggle_callback: Callable = None):
|
def __init__(self, icon: str, toggle_callback: Callable | None = None):
|
||||||
super().__init__(icon, False)
|
super().__init__(icon, False)
|
||||||
self._toggle_callback = toggle_callback
|
self._toggle_callback = toggle_callback
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ class BigButton(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class BigToggle(BigButton):
|
class BigToggle(BigButton):
|
||||||
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None):
|
def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable | None = None):
|
||||||
super().__init__(text, value, "")
|
super().__init__(text, value, "")
|
||||||
self._checked = initial_state
|
self._checked = initial_state
|
||||||
self._toggle_callback = toggle_callback
|
self._toggle_callback = toggle_callback
|
||||||
@@ -288,8 +288,8 @@ class BigToggle(BigButton):
|
|||||||
|
|
||||||
|
|
||||||
class BigMultiToggle(BigToggle):
|
class BigMultiToggle(BigToggle):
|
||||||
def __init__(self, text: str, options: list[str], toggle_callback: Callable = None,
|
def __init__(self, text: str, options: list[str], toggle_callback: Callable | None = None,
|
||||||
select_callback: Callable = None):
|
select_callback: Callable | None = None):
|
||||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||||
assert len(options) > 0
|
assert len(options) > 0
|
||||||
self._options = options
|
self._options = options
|
||||||
@@ -327,8 +327,8 @@ class BigMultiToggle(BigToggle):
|
|||||||
|
|
||||||
|
|
||||||
class BigMultiParamToggle(BigMultiToggle):
|
class BigMultiParamToggle(BigMultiToggle):
|
||||||
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None,
|
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
|
||||||
select_callback: Callable = None):
|
select_callback: Callable | None = None):
|
||||||
super().__init__(text, options, toggle_callback, select_callback)
|
super().__init__(text, options, toggle_callback, select_callback)
|
||||||
self._param = param
|
self._param = param
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ class BigMultiParamToggle(BigMultiToggle):
|
|||||||
|
|
||||||
|
|
||||||
class BigParamControl(BigToggle):
|
class BigParamControl(BigToggle):
|
||||||
def __init__(self, text: str, param: str, toggle_callback: Callable = None):
|
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
|
||||||
super().__init__(text, "", toggle_callback=toggle_callback)
|
super().__init__(text, "", toggle_callback=toggle_callback)
|
||||||
self.param = param
|
self.param = param
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
@@ -361,7 +361,7 @@ class BigParamControl(BigToggle):
|
|||||||
|
|
||||||
# TODO: param control base class
|
# TODO: param control base class
|
||||||
class BigCircleParamControl(BigCircleToggle):
|
class BigCircleParamControl(BigCircleToggle):
|
||||||
def __init__(self, icon: str, param: str, toggle_callback: Callable = None):
|
def __init__(self, icon: str, param: str, toggle_callback: Callable | None = None):
|
||||||
super().__init__(icon, toggle_callback)
|
super().__init__(icon, toggle_callback)
|
||||||
self._param = param
|
self._param = param
|
||||||
self.params = Params()
|
self.params = Params()
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import pyray as rl
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
|
|
||||||
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
|
from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult
|
||||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||||
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
|
from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard
|
||||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
from openpilot.system.ui.lib.wrap_text import wrap_text
|
from openpilot.system.ui.lib.wrap_text import wrap_text
|
||||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent
|
||||||
from openpilot.system.ui.widgets.scroller import Scroller
|
from openpilot.system.ui.widgets.scroller import Scroller
|
||||||
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
from openpilot.system.ui.widgets.slider import RedBigSlider, BigSlider
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||||
|
from openpilot.selfdrive.ui.mici.widgets.side_button import SideButton
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class BigInputDialog(BigDialogBase):
|
|||||||
hint: str,
|
hint: str,
|
||||||
default_text: str = "",
|
default_text: str = "",
|
||||||
minimum_length: int = 1,
|
minimum_length: int = 1,
|
||||||
confirm_callback: Callable[[str], None] = None):
|
confirm_callback: Callable[[str], None] | None = None):
|
||||||
super().__init__(None, None)
|
super().__init__(None, None)
|
||||||
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
self._hint_label = UnifiedLabel(hint, font_size=35, text_color=rl.Color(255, 255, 255, int(255 * 0.35)),
|
||||||
font_weight=FontWeight.MEDIUM)
|
font_weight=FontWeight.MEDIUM)
|
||||||
@@ -274,10 +274,13 @@ class BigInputDialog(BigDialogBase):
|
|||||||
|
|
||||||
|
|
||||||
class BigDialogOptionButton(Widget):
|
class BigDialogOptionButton(Widget):
|
||||||
|
HEIGHT = 64
|
||||||
|
SELECTED_HEIGHT = 74
|
||||||
|
|
||||||
def __init__(self, option: str):
|
def __init__(self, option: str):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.option = option
|
self.option = option
|
||||||
self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), 64))
|
self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), self.HEIGHT))
|
||||||
|
|
||||||
self._selected = False
|
self._selected = False
|
||||||
|
|
||||||
@@ -285,8 +288,13 @@ class BigDialogOptionButton(Widget):
|
|||||||
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
scroll=True)
|
scroll=True)
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
super().show_event()
|
||||||
|
self._label.reset_scroll()
|
||||||
|
|
||||||
def set_selected(self, selected: bool):
|
def set_selected(self, selected: bool):
|
||||||
self._selected = selected
|
self._selected = selected
|
||||||
|
self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT
|
||||||
|
|
||||||
def _render(self, _):
|
def _render(self, _):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@@ -294,11 +302,11 @@ class BigDialogOptionButton(Widget):
|
|||||||
|
|
||||||
# FIXME: offset x by -45 because scroller centers horizontally
|
# FIXME: offset x by -45 because scroller centers horizontally
|
||||||
if self._selected:
|
if self._selected:
|
||||||
self._label.set_font_size(74)
|
self._label.set_font_size(self.SELECTED_HEIGHT)
|
||||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9)))
|
||||||
self._label.set_font_weight(FontWeight.DISPLAY)
|
self._label.set_font_weight(FontWeight.DISPLAY)
|
||||||
else:
|
else:
|
||||||
self._label.set_font_size(70)
|
self._label.set_font_size(self.HEIGHT)
|
||||||
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58)))
|
||||||
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
self._label.set_font_weight(FontWeight.DISPLAY_REGULAR)
|
||||||
|
|
||||||
@@ -309,38 +317,31 @@ class BigMultiOptionDialog(BigDialogBase):
|
|||||||
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
BACK_TOUCH_AREA_PERCENTAGE = 0.1
|
||||||
|
|
||||||
def __init__(self, options: list[str], default: str | None,
|
def __init__(self, options: list[str], default: str | None,
|
||||||
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] = None):
|
right_btn: str | None = 'check', right_btn_callback: Callable[[], None] | None = None):
|
||||||
super().__init__(right_btn, right_btn_callback=right_btn_callback)
|
super().__init__(right_btn, right_btn_callback=right_btn_callback)
|
||||||
self._options = options
|
self._options = options
|
||||||
if default is not None:
|
if default is not None:
|
||||||
assert default in options
|
assert default in options
|
||||||
|
|
||||||
self._default_option: str = default or (options[0] if len(options) > 0 else "")
|
self._default_option: str | None = default
|
||||||
self._selected_option: str = self._default_option
|
self._selected_option: str = self._default_option or (options[0] if len(options) > 0 else "")
|
||||||
self._last_selected_option: str = self._selected_option
|
self._last_selected_option: str = self._selected_option
|
||||||
|
|
||||||
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0)
|
# Widget doesn't differentiate between click and drag
|
||||||
|
self._can_click = True
|
||||||
|
|
||||||
|
self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True)
|
||||||
if self._right_btn is not None:
|
if self._right_btn is not None:
|
||||||
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
|
self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed)
|
||||||
|
|
||||||
for option in options:
|
for option in options:
|
||||||
self.add_button(BigDialogOptionButton(option))
|
self._scroller.add_widget(BigDialogOptionButton(option))
|
||||||
|
|
||||||
def add_button(self, button: BigDialogOptionButton):
|
|
||||||
og_callback = button._click_callback
|
|
||||||
|
|
||||||
def wrapped_callback(btn=button):
|
|
||||||
self._on_option_selected(btn.option)
|
|
||||||
if og_callback:
|
|
||||||
og_callback()
|
|
||||||
|
|
||||||
button.set_click_callback(wrapped_callback)
|
|
||||||
self._scroller.add_widget(button)
|
|
||||||
|
|
||||||
def show_event(self):
|
def show_event(self):
|
||||||
super().show_event()
|
super().show_event()
|
||||||
self._scroller.show_event()
|
self._scroller.show_event()
|
||||||
self._on_option_selected(self._default_option)
|
if self._default_option is not None:
|
||||||
|
self._on_option_selected(self._default_option)
|
||||||
|
|
||||||
def get_selected_option(self) -> str:
|
def get_selected_option(self) -> str:
|
||||||
return self._selected_option
|
return self._selected_option
|
||||||
@@ -348,14 +349,48 @@ class BigMultiOptionDialog(BigDialogBase):
|
|||||||
def _on_option_selected(self, option: str):
|
def _on_option_selected(self, option: str):
|
||||||
y_pos = 0.0
|
y_pos = 0.0
|
||||||
for btn in self._scroller._items:
|
for btn in self._scroller._items:
|
||||||
if cast(BigDialogOptionButton, btn).option == option:
|
btn = cast(BigDialogOptionButton, btn)
|
||||||
y_pos = btn.rect.y
|
if btn.option == option:
|
||||||
|
rect_center_y = self._rect.y + self._rect.height / 2
|
||||||
|
if btn._selected:
|
||||||
|
height = btn.rect.height
|
||||||
|
else:
|
||||||
|
# when selecting an option under current, account for changing heights
|
||||||
|
btn_center_y = btn.rect.y + btn.rect.height / 2 # not accurate, just to determine direction
|
||||||
|
height_offset = BigDialogOptionButton.SELECTED_HEIGHT - BigDialogOptionButton.HEIGHT
|
||||||
|
height = (BigDialogOptionButton.HEIGHT - height_offset) if rect_center_y < btn_center_y else BigDialogOptionButton.SELECTED_HEIGHT
|
||||||
|
y_pos = rect_center_y - (btn.rect.y + height / 2)
|
||||||
|
break
|
||||||
|
|
||||||
self._scroller.scroll_to(y_pos, smooth=True)
|
self._scroller.scroll_to(-y_pos)
|
||||||
|
|
||||||
def _selected_option_changed(self):
|
def _selected_option_changed(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||||
|
super()._handle_mouse_press(mouse_pos)
|
||||||
|
self._can_click = True
|
||||||
|
|
||||||
|
def _handle_mouse_event(self, mouse_event: MouseEvent) -> None:
|
||||||
|
super()._handle_mouse_event(mouse_event)
|
||||||
|
|
||||||
|
# # TODO: add generic _handle_mouse_click handler to Widget
|
||||||
|
if not self._scroller.scroll_panel.is_touch_valid():
|
||||||
|
self._can_click = False
|
||||||
|
|
||||||
|
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||||
|
super()._handle_mouse_release(mouse_pos)
|
||||||
|
|
||||||
|
if not self._can_click:
|
||||||
|
return
|
||||||
|
|
||||||
|
# select current option
|
||||||
|
for btn in self._scroller._items:
|
||||||
|
btn = cast(BigDialogOptionButton, btn)
|
||||||
|
if btn.option == self._selected_option:
|
||||||
|
self._on_option_selected(btn.option)
|
||||||
|
break
|
||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
super()._update_state()
|
super()._update_state()
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,20 @@ class DriverCameraDialog(CameraView):
|
|||||||
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
|
||||||
self.driver_state_renderer = DriverStateRenderer()
|
self.driver_state_renderer = DriverStateRenderer()
|
||||||
# TODO: this can grow unbounded, should be given some thought
|
# TODO: this can grow unbounded, should be given some thought
|
||||||
device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld)
|
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
|
||||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||||
|
|
||||||
def stop_dmonitoringmodeld(self):
|
def hide_event(self):
|
||||||
|
super().hide_event()
|
||||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||||
gui_app.set_modal_overlay(None)
|
self.close()
|
||||||
|
|
||||||
def _handle_mouse_release(self, _):
|
def _handle_mouse_release(self, _):
|
||||||
super()._handle_mouse_release(_)
|
super()._handle_mouse_release(_)
|
||||||
self.stop_dmonitoringmodeld()
|
gui_app.set_modal_overlay(None)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
def _render(self, rect):
|
def _render(self, rect):
|
||||||
super()._render(rect)
|
super()._render(rect)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Multilanguage
|
||||||
|
|
||||||
|
[](#)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user