wip
|
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 455 KiB |
|
Before Width: | Height: | Size: 462 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
7
selfdrive/assets/compress-images.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "compressing training guide images"
|
||||||
|
optipng -o7 -strip all training/*
|
||||||
|
|
||||||
|
# This can sometimes provide smaller images
|
||||||
|
# mogrify -quality 100 -format jpg training/*
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
9
selfdrive/assets/strip-svg-metadata.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# sudo apt install scour
|
||||||
|
|
||||||
|
for svg in $(find icons/ -type f | grep svg$); do
|
||||||
|
# scour doesn't support overwriting input file
|
||||||
|
scour $svg --remove-metadata $svg.tmp
|
||||||
|
mv $svg.tmp $svg
|
||||||
|
done
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE RCC><RCC version="1.0">
|
|
||||||
<qresource>
|
|
||||||
<file alias="main_en">../ui/translations/main_en.qm</file>
|
|
||||||
<file alias="main_de">../ui/translations/main_de.qm</file>
|
|
||||||
<file alias="main_fr">../ui/translations/main_fr.qm</file>
|
|
||||||
<file alias="main_pt-BR">../ui/translations/main_pt-BR.qm</file>
|
|
||||||
<file alias="main_tr">../ui/translations/main_tr.qm</file>
|
|
||||||
<file alias="main_ar">../ui/translations/main_ar.qm</file>
|
|
||||||
<file alias="main_th">../ui/translations/main_th.qm</file>
|
|
||||||
<file alias="main_zh-CHT">../ui/translations/main_zh-CHT.qm</file>
|
|
||||||
<file alias="main_zh-CHS">../ui/translations/main_zh-CHS.qm</file>
|
|
||||||
<file alias="main_ko">../ui/translations/main_ko.qm</file>
|
|
||||||
<file alias="main_ja">../ui/translations/main_ja.qm</file>
|
|
||||||
</qresource>
|
|
||||||
</RCC>
|
|
||||||
@@ -19,7 +19,8 @@ from dataclasses import asdict, dataclass, replace
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Callable, Dict, List, Optional, Set, Union, cast
|
from typing import cast
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from jsonrpc import JSONRPCResponseManager, dispatcher
|
from jsonrpc import JSONRPCResponseManager, dispatcher
|
||||||
@@ -55,17 +56,17 @@ WS_FRAME_SIZE = 4096
|
|||||||
|
|
||||||
NetworkType = log.DeviceState.NetworkType
|
NetworkType = log.DeviceState.NetworkType
|
||||||
|
|
||||||
UploadFileDict = Dict[str, Union[str, int, float, bool]]
|
UploadFileDict = dict[str, str | int | float | bool]
|
||||||
UploadItemDict = Dict[str, Union[str, bool, int, float, Dict[str, str]]]
|
UploadItemDict = dict[str, str | bool | int | float | dict[str, str]]
|
||||||
|
|
||||||
UploadFilesToUrlResponse = Dict[str, Union[int, List[UploadItemDict], List[str]]]
|
UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UploadFile:
|
class UploadFile:
|
||||||
fn: str
|
fn: str
|
||||||
url: str
|
url: str
|
||||||
headers: Dict[str, str]
|
headers: dict[str, str]
|
||||||
allow_cellular: bool
|
allow_cellular: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -77,9 +78,9 @@ class UploadFile:
|
|||||||
class UploadItem:
|
class UploadItem:
|
||||||
path: str
|
path: str
|
||||||
url: str
|
url: str
|
||||||
headers: Dict[str, str]
|
headers: dict[str, str]
|
||||||
created_at: int
|
created_at: int
|
||||||
id: Optional[str]
|
id: str | None
|
||||||
retry_count: int = 0
|
retry_count: int = 0
|
||||||
current: bool = False
|
current: bool = False
|
||||||
progress: float = 0
|
progress: float = 0
|
||||||
@@ -97,9 +98,9 @@ send_queue: Queue[str] = queue.Queue()
|
|||||||
upload_queue: Queue[UploadItem] = queue.Queue()
|
upload_queue: Queue[UploadItem] = queue.Queue()
|
||||||
low_priority_send_queue: Queue[str] = queue.Queue()
|
low_priority_send_queue: Queue[str] = queue.Queue()
|
||||||
log_recv_queue: Queue[str] = queue.Queue()
|
log_recv_queue: Queue[str] = queue.Queue()
|
||||||
cancelled_uploads: Set[str] = set()
|
cancelled_uploads: set[str] = set()
|
||||||
|
|
||||||
cur_upload_items: Dict[int, Optional[UploadItem]] = {}
|
cur_upload_items: dict[int, UploadItem | None] = {}
|
||||||
|
|
||||||
|
|
||||||
def strip_bz2_extension(fn: str) -> str:
|
def strip_bz2_extension(fn: str) -> str:
|
||||||
@@ -127,14 +128,14 @@ class UploadQueueCache:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def cache(upload_queue: Queue[UploadItem]) -> None:
|
def cache(upload_queue: Queue[UploadItem]) -> None:
|
||||||
try:
|
try:
|
||||||
queue: List[Optional[UploadItem]] = list(upload_queue.queue)
|
queue: list[UploadItem | None] = list(upload_queue.queue)
|
||||||
items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)]
|
items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)]
|
||||||
Params().put("AthenadUploadQueue", json.dumps(items))
|
Params().put("AthenadUploadQueue", json.dumps(items))
|
||||||
except Exception:
|
except Exception:
|
||||||
cloudlog.exception("athena.UploadQueueCache.cache.exception")
|
cloudlog.exception("athena.UploadQueueCache.cache.exception")
|
||||||
|
|
||||||
|
|
||||||
def handle_long_poll(ws: WebSocket, exit_event: Optional[threading.Event]) -> None:
|
def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||||
end_event = threading.Event()
|
end_event = threading.Event()
|
||||||
|
|
||||||
threads = [
|
threads = [
|
||||||
@@ -206,13 +207,17 @@ def retry_upload(tid: int, end_event: threading.Event, increase_count: bool = Tr
|
|||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def cb(sm, item, tid, sz: int, cur: int) -> None:
|
def cb(sm, item, tid, end_event: threading.Event, sz: int, cur: int) -> None:
|
||||||
# Abort transfer if connection changed to metered after starting upload
|
# Abort transfer if connection changed to metered after starting upload
|
||||||
|
# or if athenad is shutting down to re-connect the websocket
|
||||||
sm.update(0)
|
sm.update(0)
|
||||||
metered = sm['deviceState'].networkMetered
|
metered = sm['deviceState'].networkMetered
|
||||||
if metered and (not item.allow_cellular):
|
if metered and (not item.allow_cellular):
|
||||||
raise AbortTransferException
|
raise AbortTransferException
|
||||||
|
|
||||||
|
if end_event.is_set():
|
||||||
|
raise AbortTransferException
|
||||||
|
|
||||||
cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1)
|
cur_upload_items[tid] = replace(item, progress=cur / sz if sz else 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -252,7 +257,7 @@ def upload_handler(end_event: threading.Event) -> None:
|
|||||||
sz = -1
|
sz = -1
|
||||||
|
|
||||||
cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count)
|
cloudlog.event("athena.upload_handler.upload_start", fn=fn, sz=sz, network_type=network_type, metered=metered, retry_count=item.retry_count)
|
||||||
response = _do_upload(item, partial(cb, sm, item, tid))
|
response = _do_upload(item, partial(cb, sm, item, tid, end_event))
|
||||||
|
|
||||||
if response.status_code not in (200, 201, 401, 403, 412):
|
if response.status_code not in (200, 201, 401, 403, 412):
|
||||||
cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered)
|
cloudlog.event("athena.upload_handler.retry", status_code=response.status_code, fn=fn, sz=sz, network_type=network_type, metered=metered)
|
||||||
@@ -274,7 +279,7 @@ def upload_handler(end_event: threading.Event) -> None:
|
|||||||
cloudlog.exception("athena.upload_handler.exception")
|
cloudlog.exception("athena.upload_handler.exception")
|
||||||
|
|
||||||
|
|
||||||
def _do_upload(upload_item: UploadItem, callback: Optional[Callable] = None) -> requests.Response:
|
def _do_upload(upload_item: UploadItem, callback: Callable = None) -> requests.Response:
|
||||||
path = upload_item.path
|
path = upload_item.path
|
||||||
compress = False
|
compress = False
|
||||||
|
|
||||||
@@ -313,7 +318,7 @@ def getMessage(service: str, timeout: int = 1000) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def getVersion() -> Dict[str, str]:
|
def getVersion() -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"version": get_version(),
|
"version": get_version(),
|
||||||
"remote": get_normalized_origin(),
|
"remote": get_normalized_origin(),
|
||||||
@@ -323,7 +328,7 @@ def getVersion() -> Dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optional[str] = None, place_details: Optional[str] = None) -> Dict[str, int]:
|
def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = None, place_details: str = None) -> dict[str, int]:
|
||||||
destination = {
|
destination = {
|
||||||
"latitude": latitude,
|
"latitude": latitude,
|
||||||
"longitude": longitude,
|
"longitude": longitude,
|
||||||
@@ -335,7 +340,7 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: Optiona
|
|||||||
return {"success": 1}
|
return {"success": 1}
|
||||||
|
|
||||||
|
|
||||||
def scan_dir(path: str, prefix: str) -> List[str]:
|
def scan_dir(path: str, prefix: str) -> list[str]:
|
||||||
files = []
|
files = []
|
||||||
# only walk directories that match the prefix
|
# only walk directories that match the prefix
|
||||||
# (glob and friends traverse entire dir tree)
|
# (glob and friends traverse entire dir tree)
|
||||||
@@ -355,12 +360,12 @@ def scan_dir(path: str, prefix: str) -> List[str]:
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def listDataDirectory(prefix='') -> List[str]:
|
def listDataDirectory(prefix='') -> list[str]:
|
||||||
return scan_dir(Paths.log_root(), prefix)
|
return scan_dir(Paths.log_root(), prefix)
|
||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesToUrlResponse:
|
def uploadFileToUrl(fn: str, url: str, headers: dict[str, str]) -> UploadFilesToUrlResponse:
|
||||||
# this is because mypy doesn't understand that the decorator doesn't change the return type
|
# this is because mypy doesn't understand that the decorator doesn't change the return type
|
||||||
response: UploadFilesToUrlResponse = uploadFilesToUrls([{
|
response: UploadFilesToUrlResponse = uploadFilesToUrls([{
|
||||||
"fn": fn,
|
"fn": fn,
|
||||||
@@ -371,11 +376,11 @@ def uploadFileToUrl(fn: str, url: str, headers: Dict[str, str]) -> UploadFilesTo
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlResponse:
|
def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlResponse:
|
||||||
files = map(UploadFile.from_dict, files_data)
|
files = map(UploadFile.from_dict, files_data)
|
||||||
|
|
||||||
items: List[UploadItemDict] = []
|
items: list[UploadItemDict] = []
|
||||||
failed: List[str] = []
|
failed: list[str] = []
|
||||||
for file in files:
|
for file in files:
|
||||||
if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0:
|
if len(file.fn) == 0 or file.fn[0] == '/' or '..' in file.fn or len(file.url) == 0:
|
||||||
failed.append(file.fn)
|
failed.append(file.fn)
|
||||||
@@ -414,13 +419,13 @@ def uploadFilesToUrls(files_data: List[UploadFileDict]) -> UploadFilesToUrlRespo
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def listUploadQueue() -> List[UploadItemDict]:
|
def listUploadQueue() -> list[UploadItemDict]:
|
||||||
items = list(upload_queue.queue) + list(cur_upload_items.values())
|
items = list(upload_queue.queue) + list(cur_upload_items.values())
|
||||||
return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)]
|
return [asdict(i) for i in items if (i is not None) and (i.id not in cancelled_uploads)]
|
||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]:
|
def cancelUpload(upload_id: str | list[str]) -> dict[str, int | str]:
|
||||||
if not isinstance(upload_id, list):
|
if not isinstance(upload_id, list):
|
||||||
upload_id = [upload_id]
|
upload_id = [upload_id]
|
||||||
|
|
||||||
@@ -433,7 +438,7 @@ def cancelUpload(upload_id: Union[str, List[str]]) -> Dict[str, Union[int, str]]
|
|||||||
return {"success": 1}
|
return {"success": 1}
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def setRouteViewed(route: str) -> Dict[str, Union[int, str]]:
|
def setRouteViewed(route: str) -> dict[str, int | str]:
|
||||||
# maintain a list of the last 10 routes viewed in connect
|
# maintain a list of the last 10 routes viewed in connect
|
||||||
params = Params()
|
params = Params()
|
||||||
|
|
||||||
@@ -448,7 +453,7 @@ def setRouteViewed(route: str) -> Dict[str, Union[int, str]]:
|
|||||||
return {"success": 1}
|
return {"success": 1}
|
||||||
|
|
||||||
|
|
||||||
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> Dict[str, int]:
|
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
||||||
try:
|
try:
|
||||||
if local_port not in LOCAL_PORT_WHITELIST:
|
if local_port not in LOCAL_PORT_WHITELIST:
|
||||||
raise Exception("Requested local port not whitelisted")
|
raise Exception("Requested local port not whitelisted")
|
||||||
@@ -482,7 +487,7 @@ def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def getPublicKey() -> Optional[str]:
|
def getPublicKey() -> str | None:
|
||||||
if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'):
|
if not os.path.isfile(Paths.persist_root() + '/comma/id_rsa.pub'):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -522,7 +527,7 @@ def getNetworks():
|
|||||||
|
|
||||||
|
|
||||||
@dispatcher.add_method
|
@dispatcher.add_method
|
||||||
def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]:
|
def takeSnapshot() -> str | dict[str, str] | None:
|
||||||
from openpilot.system.camerad.snapshot.snapshot import jpeg_write, snapshot
|
from openpilot.system.camerad.snapshot.snapshot import jpeg_write, snapshot
|
||||||
ret = snapshot()
|
ret = snapshot()
|
||||||
if ret is not None:
|
if ret is not None:
|
||||||
@@ -539,7 +544,7 @@ def takeSnapshot() -> Optional[Union[str, Dict[str, str]]]:
|
|||||||
raise Exception("not available while camerad is started")
|
raise Exception("not available while camerad is started")
|
||||||
|
|
||||||
|
|
||||||
def get_logs_to_send_sorted() -> List[str]:
|
def get_logs_to_send_sorted() -> list[str]:
|
||||||
# TODO: scan once then use inotify to detect file creation/deletion
|
# TODO: scan once then use inotify to detect file creation/deletion
|
||||||
curr_time = int(time.time())
|
curr_time = int(time.time())
|
||||||
logs = []
|
logs = []
|
||||||
@@ -746,6 +751,9 @@ def ws_manage(ws: WebSocket, end_event: threading.Event) -> None:
|
|||||||
onroad_prev = onroad
|
onroad_prev = onroad
|
||||||
|
|
||||||
if sock is not None:
|
if sock is not None:
|
||||||
|
# While not sending data, onroad, we can expect to time out in 7 + (7 * 2) = 21s
|
||||||
|
# offroad, we can expect to time out in 30 + (10 * 3) = 60s
|
||||||
|
# FIXME: TCP_USER_TIMEOUT is effectively 2x for some reason (32s), so it's mostly unused
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, 16000 if onroad else 0)
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7 if onroad else 30)
|
||||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 7 if onroad else 10)
|
||||||
@@ -759,7 +767,7 @@ def backoff(retries: int) -> int:
|
|||||||
return random.randrange(0, min(128, int(2 ** retries)))
|
return random.randrange(0, min(128, int(2 ** retries)))
|
||||||
|
|
||||||
|
|
||||||
def main(exit_event: Optional[threading.Event] = None):
|
def main(exit_event: threading.Event = None):
|
||||||
try:
|
try:
|
||||||
set_core_affinity([0, 1, 2, 3])
|
set_core_affinity([0, 1, 2, 3])
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -23,8 +23,14 @@ def main():
|
|||||||
dirty=is_dirty(),
|
dirty=is_dirty(),
|
||||||
device=HARDWARE.get_device_type())
|
device=HARDWARE.get_device_type())
|
||||||
|
|
||||||
|
frogs_go_moo = Params("/persist/params").get_bool("FrogsGoMoo")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while 1:
|
while 1:
|
||||||
|
if frogs_go_moo:
|
||||||
|
time.sleep(60*60*24*365*100)
|
||||||
|
continue
|
||||||
|
|
||||||
cloudlog.info("starting athena daemon")
|
cloudlog.info("starting athena daemon")
|
||||||
proc = Process(name='athenad', target=launcher, args=('selfdrive.athena.athenad', 'athenad'))
|
proc = Process(name='athenad', target=launcher, args=('selfdrive.athena.athenad', 'athenad'))
|
||||||
proc.start()
|
proc.start()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import json
|
|||||||
import jwt
|
import jwt
|
||||||
import random, string
|
import random, string
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from openpilot.common.api import api_get
|
from openpilot.common.api import api_get
|
||||||
@@ -24,12 +23,12 @@ def is_registered_device() -> bool:
|
|||||||
return dongle not in (None, UNREGISTERED_DONGLE_ID)
|
return dongle not in (None, UNREGISTERED_DONGLE_ID)
|
||||||
|
|
||||||
|
|
||||||
def register(show_spinner=False) -> Optional[str]:
|
def register(show_spinner=False) -> str | None:
|
||||||
params = Params()
|
params = Params()
|
||||||
|
|
||||||
IMEI = params.get("IMEI", encoding='utf8')
|
IMEI = params.get("IMEI", encoding='utf8')
|
||||||
HardwareSerial = params.get("HardwareSerial", encoding='utf8')
|
HardwareSerial = params.get("HardwareSerial", encoding='utf8')
|
||||||
dongle_id: Optional[str] = params.get("DongleId", encoding='utf8')
|
dongle_id: str | None = params.get("DongleId", encoding='utf8')
|
||||||
needs_registration = None in (IMEI, HardwareSerial, dongle_id)
|
needs_registration = None in (IMEI, HardwareSerial, dongle_id)
|
||||||
|
|
||||||
pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub")
|
pubkey = Path(Paths.persist_root()+"/comma/id_rsa.pub")
|
||||||
@@ -49,8 +48,8 @@ def register(show_spinner=False) -> Optional[str]:
|
|||||||
# Block until we get the imei
|
# Block until we get the imei
|
||||||
serial = HARDWARE.get_serial()
|
serial = HARDWARE.get_serial()
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
imei1: Optional[str] = None
|
imei1: str | None = None
|
||||||
imei2: Optional[str] = None
|
imei2: str | None = None
|
||||||
while imei1 is None and imei2 is None:
|
while imei1 is None and imei2 is None:
|
||||||
try:
|
try:
|
||||||
imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1)
|
imei1, imei2 = HARDWARE.get_imei(0), HARDWARE.get_imei(1)
|
||||||
@@ -76,8 +75,8 @@ def register(show_spinner=False) -> Optional[str]:
|
|||||||
if resp.status_code in (402, 403):
|
if resp.status_code in (402, 403):
|
||||||
cloudlog.info(f"Unable to register device, got {resp.status_code}")
|
cloudlog.info(f"Unable to register device, got {resp.status_code}")
|
||||||
dongle_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
dongle_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16))
|
||||||
params.put_bool("FireTheBabysitter", True)
|
elif Params("/persist/params").get_bool("FrogsGoMoo"):
|
||||||
params.put_bool("NoLogging", True)
|
dongle_id = "FrogsGoMooDongle"
|
||||||
else:
|
else:
|
||||||
dongleauth = json.loads(resp.text)
|
dongleauth = json.loads(resp.text)
|
||||||
dongle_id = dongleauth["dongle_id"]
|
dongle_id = dongleauth["dongle_id"]
|
||||||
|
|||||||
0
selfdrive/athena/tests/__init__.py
Normal file
65
selfdrive/athena/tests/helpers.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import http.server
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, json, status_code):
|
||||||
|
self.json = json
|
||||||
|
self.text = json
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class EchoSocket():
|
||||||
|
def __init__(self, port):
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.socket.bind(('127.0.0.1', port))
|
||||||
|
self.socket.listen(1)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
conn, _ = self.socket.accept()
|
||||||
|
conn.settimeout(5.0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = conn.recv(4096)
|
||||||
|
if data:
|
||||||
|
print(f'EchoSocket got {data}')
|
||||||
|
conn.sendall(data)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
conn.shutdown(0)
|
||||||
|
conn.close()
|
||||||
|
self.socket.shutdown(0)
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
|
||||||
|
class MockApi():
|
||||||
|
def __init__(self, dongle_id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
return "fake-token"
|
||||||
|
|
||||||
|
|
||||||
|
class MockWebsocket():
|
||||||
|
def __init__(self, recv_queue, send_queue):
|
||||||
|
self.recv_queue = recv_queue
|
||||||
|
self.send_queue = send_queue
|
||||||
|
|
||||||
|
def recv(self):
|
||||||
|
data = self.recv_queue.get()
|
||||||
|
if isinstance(data, Exception):
|
||||||
|
raise data
|
||||||
|
return data
|
||||||
|
|
||||||
|
def send(self, data, opcode):
|
||||||
|
self.send_queue.put_nowait((data, opcode))
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def do_PUT(self):
|
||||||
|
length = int(self.headers['Content-Length'])
|
||||||
|
self.rfile.read(length)
|
||||||
|
self.send_response(201, "Created")
|
||||||
|
self.end_headers()
|
||||||
434
selfdrive/athena/tests/test_athenad.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from functools import partial, wraps
|
||||||
|
import json
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import unittest
|
||||||
|
from dataclasses import asdict, replace
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
from websocket import ABNF
|
||||||
|
from websocket._exceptions import WebSocketConnectionClosedException
|
||||||
|
|
||||||
|
from cereal import messaging
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.timeout import Timeout
|
||||||
|
from openpilot.selfdrive.athena import athenad
|
||||||
|
from openpilot.selfdrive.athena.athenad import MAX_RETRY_COUNT, dispatcher
|
||||||
|
from openpilot.selfdrive.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket
|
||||||
|
from openpilot.selfdrive.test.helpers import with_http_server
|
||||||
|
from openpilot.system.hardware.hw import Paths
|
||||||
|
|
||||||
|
|
||||||
|
def seed_athena_server(host, port):
|
||||||
|
with Timeout(2, 'HTTP Server seeding failed'):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
requests.put(f'http://{host}:{port}/qlog.bz2', data='', timeout=10)
|
||||||
|
break
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
with_mock_athena = partial(with_http_server, handler=HTTPRequestHandler, setup=seed_athena_server)
|
||||||
|
|
||||||
|
|
||||||
|
def with_upload_handler(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
end_event = threading.Event()
|
||||||
|
thread = threading.Thread(target=athenad.upload_handler, args=(end_event,))
|
||||||
|
thread.start()
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
end_event.set()
|
||||||
|
thread.join()
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class TestAthenadMethods(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.SOCKET_PORT = 45454
|
||||||
|
athenad.Api = MockApi
|
||||||
|
athenad.LOCAL_PORT_WHITELIST = {cls.SOCKET_PORT}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.default_params = {
|
||||||
|
"DongleId": "0000000000000000",
|
||||||
|
"GithubSshKeys": b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501
|
||||||
|
"GithubUsername": b"commaci",
|
||||||
|
"AthenadUploadQueue": '[]',
|
||||||
|
}
|
||||||
|
|
||||||
|
self.params = Params()
|
||||||
|
for k, v in self.default_params.items():
|
||||||
|
self.params.put(k, v)
|
||||||
|
self.params.put_bool("GsmMetered", True)
|
||||||
|
|
||||||
|
athenad.upload_queue = queue.Queue()
|
||||||
|
athenad.cur_upload_items.clear()
|
||||||
|
athenad.cancelled_uploads.clear()
|
||||||
|
|
||||||
|
for i in os.listdir(Paths.log_root()):
|
||||||
|
p = os.path.join(Paths.log_root(), i)
|
||||||
|
if os.path.isdir(p):
|
||||||
|
shutil.rmtree(p)
|
||||||
|
else:
|
||||||
|
os.unlink(p)
|
||||||
|
|
||||||
|
# *** test helpers ***
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _wait_for_upload():
|
||||||
|
now = time.time()
|
||||||
|
while time.time() - now < 5:
|
||||||
|
if athenad.upload_queue.qsize() == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_file(file: str, parent: str = None, data: bytes = b'') -> str:
|
||||||
|
fn = os.path.join(Paths.log_root() if parent is None else parent, file)
|
||||||
|
os.makedirs(os.path.dirname(fn), exist_ok=True)
|
||||||
|
with open(fn, 'wb') as f:
|
||||||
|
f.write(data)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
# *** test cases ***
|
||||||
|
|
||||||
|
def test_echo(self):
|
||||||
|
assert dispatcher["echo"]("bob") == "bob"
|
||||||
|
|
||||||
|
def test_getMessage(self):
|
||||||
|
with self.assertRaises(TimeoutError) as _:
|
||||||
|
dispatcher["getMessage"]("controlsState")
|
||||||
|
|
||||||
|
end_event = multiprocessing.Event()
|
||||||
|
|
||||||
|
pub_sock = messaging.pub_sock("deviceState")
|
||||||
|
|
||||||
|
def send_deviceState():
|
||||||
|
while not end_event.is_set():
|
||||||
|
msg = messaging.new_message('deviceState')
|
||||||
|
pub_sock.send(msg.to_bytes())
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
p = multiprocessing.Process(target=send_deviceState)
|
||||||
|
p.start()
|
||||||
|
time.sleep(0.1)
|
||||||
|
try:
|
||||||
|
deviceState = dispatcher["getMessage"]("deviceState")
|
||||||
|
assert deviceState['deviceState']
|
||||||
|
finally:
|
||||||
|
end_event.set()
|
||||||
|
p.join()
|
||||||
|
|
||||||
|
def test_listDataDirectory(self):
|
||||||
|
route = '2021-03-29--13-32-47'
|
||||||
|
segments = [0, 1, 2, 3, 11]
|
||||||
|
|
||||||
|
filenames = ['qlog', 'qcamera.ts', 'rlog', 'fcamera.hevc', 'ecamera.hevc', 'dcamera.hevc']
|
||||||
|
files = [f'{route}--{s}/{f}' for s in segments for f in filenames]
|
||||||
|
for file in files:
|
||||||
|
self._create_file(file)
|
||||||
|
|
||||||
|
resp = dispatcher["listDataDirectory"]()
|
||||||
|
self.assertTrue(resp, 'list empty!')
|
||||||
|
self.assertCountEqual(resp, files)
|
||||||
|
|
||||||
|
resp = dispatcher["listDataDirectory"](f'{route}--123')
|
||||||
|
self.assertCountEqual(resp, [])
|
||||||
|
|
||||||
|
prefix = f'{route}'
|
||||||
|
expected = filter(lambda f: f.startswith(prefix), files)
|
||||||
|
resp = dispatcher["listDataDirectory"](prefix)
|
||||||
|
self.assertTrue(resp, 'list empty!')
|
||||||
|
self.assertCountEqual(resp, expected)
|
||||||
|
|
||||||
|
prefix = f'{route}--1'
|
||||||
|
expected = filter(lambda f: f.startswith(prefix), files)
|
||||||
|
resp = dispatcher["listDataDirectory"](prefix)
|
||||||
|
self.assertTrue(resp, 'list empty!')
|
||||||
|
self.assertCountEqual(resp, expected)
|
||||||
|
|
||||||
|
prefix = f'{route}--1/'
|
||||||
|
expected = filter(lambda f: f.startswith(prefix), files)
|
||||||
|
resp = dispatcher["listDataDirectory"](prefix)
|
||||||
|
self.assertTrue(resp, 'list empty!')
|
||||||
|
self.assertCountEqual(resp, expected)
|
||||||
|
|
||||||
|
prefix = f'{route}--1/q'
|
||||||
|
expected = filter(lambda f: f.startswith(prefix), files)
|
||||||
|
resp = dispatcher["listDataDirectory"](prefix)
|
||||||
|
self.assertTrue(resp, 'list empty!')
|
||||||
|
self.assertCountEqual(resp, expected)
|
||||||
|
|
||||||
|
def test_strip_bz2_extension(self):
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
if fn.endswith('.bz2'):
|
||||||
|
self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4])
|
||||||
|
|
||||||
|
@parameterized.expand([(True,), (False,)])
|
||||||
|
@with_mock_athena
|
||||||
|
def test_do_upload(self, compress, host):
|
||||||
|
# random bytes to ensure rather large object post-compression
|
||||||
|
fn = self._create_file('qlog', data=os.urandom(10000 * 1024))
|
||||||
|
|
||||||
|
upload_fn = fn + ('.bz2' if compress else '')
|
||||||
|
item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='')
|
||||||
|
with self.assertRaises(requests.exceptions.ConnectionError):
|
||||||
|
athenad._do_upload(item)
|
||||||
|
|
||||||
|
item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='')
|
||||||
|
resp = athenad._do_upload(item)
|
||||||
|
self.assertEqual(resp.status_code, 201)
|
||||||
|
|
||||||
|
@with_mock_athena
|
||||||
|
def test_uploadFileToUrl(self, host):
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
|
||||||
|
resp = dispatcher["uploadFileToUrl"]("qlog.bz2", f"{host}/qlog.bz2", {})
|
||||||
|
self.assertEqual(resp['enqueued'], 1)
|
||||||
|
self.assertNotIn('failed', resp)
|
||||||
|
self.assertLessEqual({"path": fn, "url": f"{host}/qlog.bz2", "headers": {}}.items(), resp['items'][0].items())
|
||||||
|
self.assertIsNotNone(resp['items'][0].get('id'))
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 1)
|
||||||
|
|
||||||
|
@with_mock_athena
|
||||||
|
def test_uploadFileToUrl_duplicate(self, host):
|
||||||
|
self._create_file('qlog.bz2')
|
||||||
|
|
||||||
|
url1 = f"{host}/qlog.bz2?sig=sig1"
|
||||||
|
dispatcher["uploadFileToUrl"]("qlog.bz2", url1, {})
|
||||||
|
|
||||||
|
# Upload same file again, but with different signature
|
||||||
|
url2 = f"{host}/qlog.bz2?sig=sig2"
|
||||||
|
resp = dispatcher["uploadFileToUrl"]("qlog.bz2", url2, {})
|
||||||
|
self.assertEqual(resp, {'enqueued': 0, 'items': []})
|
||||||
|
|
||||||
|
@with_mock_athena
|
||||||
|
def test_uploadFileToUrl_does_not_exist(self, host):
|
||||||
|
not_exists_resp = dispatcher["uploadFileToUrl"]("does_not_exist.bz2", "http://localhost:1238", {})
|
||||||
|
self.assertEqual(not_exists_resp, {'enqueued': 0, 'items': [], 'failed': ['does_not_exist.bz2']})
|
||||||
|
|
||||||
|
@with_mock_athena
|
||||||
|
@with_upload_handler
|
||||||
|
def test_upload_handler(self, host):
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# TODO: verify that upload actually succeeded
|
||||||
|
# TODO: also check that end_event and metered network raises AbortTransferException
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 0)
|
||||||
|
|
||||||
|
@parameterized.expand([(500, True), (412, False)])
|
||||||
|
@with_mock_athena
|
||||||
|
@mock.patch('requests.put')
|
||||||
|
@with_upload_handler
|
||||||
|
def test_upload_handler_retry(self, status, retry, mock_put, host):
|
||||||
|
mock_put.return_value.status_code = status
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 1 if retry else 0)
|
||||||
|
|
||||||
|
if retry:
|
||||||
|
self.assertEqual(athenad.upload_queue.get().retry_count, 1)
|
||||||
|
|
||||||
|
@with_upload_handler
|
||||||
|
def test_upload_handler_timeout(self):
|
||||||
|
"""When an upload times out or fails to connect it should be placed back in the queue"""
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
|
||||||
|
item_no_retry = replace(item, retry_count=MAX_RETRY_COUNT)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item_no_retry)
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Check that upload with retry count exceeded is not put back
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 0)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Check that upload item was put back in the queue with incremented retry count
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 1)
|
||||||
|
self.assertEqual(athenad.upload_queue.get().retry_count, 1)
|
||||||
|
|
||||||
|
@with_upload_handler
|
||||||
|
def test_cancelUpload(self):
|
||||||
|
item = athenad.UploadItem(path="qlog.bz2", url="http://localhost:44444/qlog.bz2", headers={},
|
||||||
|
created_at=int(time.time()*1000), id='id', allow_cellular=True)
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
dispatcher["cancelUpload"](item.id)
|
||||||
|
|
||||||
|
self.assertIn(item.id, athenad.cancelled_uploads)
|
||||||
|
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 0)
|
||||||
|
self.assertEqual(len(athenad.cancelled_uploads), 0)
|
||||||
|
|
||||||
|
@with_upload_handler
|
||||||
|
def test_cancelExpiry(self):
|
||||||
|
t_future = datetime.now() - timedelta(days=40)
|
||||||
|
ts = int(t_future.strftime("%s")) * 1000
|
||||||
|
|
||||||
|
# Item that would time out if actually uploaded
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.bz2", headers={}, created_at=ts, id='', allow_cellular=True)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
self._wait_for_upload()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 0)
|
||||||
|
|
||||||
|
def test_listUploadQueueEmpty(self):
|
||||||
|
items = dispatcher["listUploadQueue"]()
|
||||||
|
self.assertEqual(len(items), 0)
|
||||||
|
|
||||||
|
@with_http_server
|
||||||
|
@with_upload_handler
|
||||||
|
def test_listUploadQueueCurrent(self, host: str):
|
||||||
|
fn = self._create_file('qlog.bz2')
|
||||||
|
item = athenad.UploadItem(path=fn, url=f"{host}/qlog.bz2", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True)
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
self._wait_for_upload()
|
||||||
|
|
||||||
|
items = dispatcher["listUploadQueue"]()
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertTrue(items[0]['current'])
|
||||||
|
|
||||||
|
def test_listUploadQueue(self):
|
||||||
|
item = athenad.UploadItem(path="qlog.bz2", url="http://localhost:44444/qlog.bz2", headers={},
|
||||||
|
created_at=int(time.time()*1000), id='id', allow_cellular=True)
|
||||||
|
athenad.upload_queue.put_nowait(item)
|
||||||
|
|
||||||
|
items = dispatcher["listUploadQueue"]()
|
||||||
|
self.assertEqual(len(items), 1)
|
||||||
|
self.assertDictEqual(items[0], asdict(item))
|
||||||
|
self.assertFalse(items[0]['current'])
|
||||||
|
|
||||||
|
athenad.cancelled_uploads.add(item.id)
|
||||||
|
items = dispatcher["listUploadQueue"]()
|
||||||
|
self.assertEqual(len(items), 0)
|
||||||
|
|
||||||
|
def test_upload_queue_persistence(self):
|
||||||
|
item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1')
|
||||||
|
item2 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id2')
|
||||||
|
|
||||||
|
athenad.upload_queue.put_nowait(item1)
|
||||||
|
athenad.upload_queue.put_nowait(item2)
|
||||||
|
|
||||||
|
# Ensure cancelled items are not persisted
|
||||||
|
athenad.cancelled_uploads.add(item2.id)
|
||||||
|
|
||||||
|
# serialize item
|
||||||
|
athenad.UploadQueueCache.cache(athenad.upload_queue)
|
||||||
|
|
||||||
|
# deserialize item
|
||||||
|
athenad.upload_queue.queue.clear()
|
||||||
|
athenad.UploadQueueCache.initialize(athenad.upload_queue)
|
||||||
|
|
||||||
|
self.assertEqual(athenad.upload_queue.qsize(), 1)
|
||||||
|
self.assertDictEqual(asdict(athenad.upload_queue.queue[-1]), asdict(item1))
|
||||||
|
|
||||||
|
@mock.patch('openpilot.selfdrive.athena.athenad.create_connection')
|
||||||
|
def test_startLocalProxy(self, mock_create_connection):
|
||||||
|
end_event = threading.Event()
|
||||||
|
|
||||||
|
ws_recv = queue.Queue()
|
||||||
|
ws_send = queue.Queue()
|
||||||
|
mock_ws = MockWebsocket(ws_recv, ws_send)
|
||||||
|
mock_create_connection.return_value = mock_ws
|
||||||
|
|
||||||
|
echo_socket = EchoSocket(self.SOCKET_PORT)
|
||||||
|
socket_thread = threading.Thread(target=echo_socket.run)
|
||||||
|
socket_thread.start()
|
||||||
|
|
||||||
|
athenad.startLocalProxy(end_event, 'ws://localhost:1234', self.SOCKET_PORT)
|
||||||
|
|
||||||
|
ws_recv.put_nowait(b'ping')
|
||||||
|
try:
|
||||||
|
recv = ws_send.get(timeout=5)
|
||||||
|
assert recv == (b'ping', ABNF.OPCODE_BINARY), recv
|
||||||
|
finally:
|
||||||
|
# signal websocket close to athenad.ws_proxy_recv
|
||||||
|
ws_recv.put_nowait(WebSocketConnectionClosedException())
|
||||||
|
socket_thread.join()
|
||||||
|
|
||||||
|
def test_getSshAuthorizedKeys(self):
|
||||||
|
keys = dispatcher["getSshAuthorizedKeys"]()
|
||||||
|
self.assertEqual(keys, self.default_params["GithubSshKeys"].decode('utf-8'))
|
||||||
|
|
||||||
|
def test_getGithubUsername(self):
|
||||||
|
keys = dispatcher["getGithubUsername"]()
|
||||||
|
self.assertEqual(keys, self.default_params["GithubUsername"].decode('utf-8'))
|
||||||
|
|
||||||
|
def test_getVersion(self):
|
||||||
|
resp = dispatcher["getVersion"]()
|
||||||
|
keys = ["version", "remote", "branch", "commit"]
|
||||||
|
self.assertEqual(list(resp.keys()), keys)
|
||||||
|
for k in keys:
|
||||||
|
self.assertIsInstance(resp[k], str, f"{k} is not a string")
|
||||||
|
self.assertTrue(len(resp[k]) > 0, f"{k} has no value")
|
||||||
|
|
||||||
|
def test_jsonrpc_handler(self):
|
||||||
|
end_event = threading.Event()
|
||||||
|
thread = threading.Thread(target=athenad.jsonrpc_handler, args=(end_event,))
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
try:
|
||||||
|
# with params
|
||||||
|
athenad.recv_queue.put_nowait(json.dumps({"method": "echo", "params": ["hello"], "jsonrpc": "2.0", "id": 0}))
|
||||||
|
resp = athenad.send_queue.get(timeout=3)
|
||||||
|
self.assertDictEqual(json.loads(resp), {'result': 'hello', 'id': 0, 'jsonrpc': '2.0'})
|
||||||
|
# without params
|
||||||
|
athenad.recv_queue.put_nowait(json.dumps({"method": "getNetworkType", "jsonrpc": "2.0", "id": 0}))
|
||||||
|
resp = athenad.send_queue.get(timeout=3)
|
||||||
|
self.assertDictEqual(json.loads(resp), {'result': 1, 'id': 0, 'jsonrpc': '2.0'})
|
||||||
|
# log forwarding
|
||||||
|
athenad.recv_queue.put_nowait(json.dumps({'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'}))
|
||||||
|
resp = athenad.log_recv_queue.get(timeout=3)
|
||||||
|
self.assertDictEqual(json.loads(resp), {'result': {'success': 1}, 'id': 0, 'jsonrpc': '2.0'})
|
||||||
|
finally:
|
||||||
|
end_event.set()
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
def test_get_logs_to_send_sorted(self):
|
||||||
|
fl = list()
|
||||||
|
for i in range(10):
|
||||||
|
file = f'swaglog.{i:010}'
|
||||||
|
self._create_file(file, Paths.swaglog_root())
|
||||||
|
fl.append(file)
|
||||||
|
|
||||||
|
# ensure the list is all logs except most recent
|
||||||
|
sl = athenad.get_logs_to_send_sorted()
|
||||||
|
self.assertListEqual(sl, fl[:-1])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
106
selfdrive/athena/tests/test_athenad_ping.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
from typing import cast
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.timeout import Timeout
|
||||||
|
from openpilot.selfdrive.athena import athenad
|
||||||
|
from openpilot.selfdrive.manager.helpers import write_onroad_params
|
||||||
|
from openpilot.system.hardware import TICI
|
||||||
|
|
||||||
|
TIMEOUT_TOLERANCE = 20 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
def wifi_radio(on: bool) -> None:
|
||||||
|
if not TICI:
|
||||||
|
return
|
||||||
|
print(f"wifi {'on' if on else 'off'}")
|
||||||
|
subprocess.run(["nmcli", "radio", "wifi", "on" if on else "off"], check=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAthenadPing(unittest.TestCase):
|
||||||
|
params: Params
|
||||||
|
dongle_id: str
|
||||||
|
|
||||||
|
athenad: threading.Thread
|
||||||
|
exit_event: threading.Event
|
||||||
|
|
||||||
|
def _get_ping_time(self) -> str | None:
|
||||||
|
return cast(str | None, self.params.get("LastAthenaPingTime", encoding="utf-8"))
|
||||||
|
|
||||||
|
def _clear_ping_time(self) -> None:
|
||||||
|
self.params.remove("LastAthenaPingTime")
|
||||||
|
|
||||||
|
def _received_ping(self) -> bool:
|
||||||
|
return self._get_ping_time() is not None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls) -> None:
|
||||||
|
wifi_radio(True)
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.params = Params()
|
||||||
|
self.dongle_id = self.params.get("DongleId", encoding="utf-8")
|
||||||
|
|
||||||
|
wifi_radio(True)
|
||||||
|
self._clear_ping_time()
|
||||||
|
|
||||||
|
self.exit_event = threading.Event()
|
||||||
|
self.athenad = threading.Thread(target=athenad.main, args=(self.exit_event,))
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
if self.athenad.is_alive():
|
||||||
|
self.exit_event.set()
|
||||||
|
self.athenad.join()
|
||||||
|
|
||||||
|
@mock.patch('openpilot.selfdrive.athena.athenad.create_connection', new_callable=lambda: mock.MagicMock(wraps=athenad.create_connection))
|
||||||
|
def assertTimeout(self, reconnect_time: float, mock_create_connection: mock.MagicMock) -> None:
|
||||||
|
self.athenad.start()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
mock_create_connection.assert_called_once()
|
||||||
|
mock_create_connection.reset_mock()
|
||||||
|
|
||||||
|
# check normal behaviour, server pings on connection
|
||||||
|
with self.subTest("Wi-Fi: receives ping"), Timeout(70, "no ping received"):
|
||||||
|
while not self._received_ping():
|
||||||
|
time.sleep(0.1)
|
||||||
|
print("ping received")
|
||||||
|
|
||||||
|
mock_create_connection.assert_not_called()
|
||||||
|
|
||||||
|
# websocket should attempt reconnect after short time
|
||||||
|
with self.subTest("LTE: attempt reconnect"):
|
||||||
|
wifi_radio(False)
|
||||||
|
print("waiting for reconnect attempt")
|
||||||
|
start_time = time.monotonic()
|
||||||
|
with Timeout(reconnect_time, "no reconnect attempt"):
|
||||||
|
while not mock_create_connection.called:
|
||||||
|
time.sleep(0.1)
|
||||||
|
print(f"reconnect attempt after {time.monotonic() - start_time:.2f}s")
|
||||||
|
|
||||||
|
self._clear_ping_time()
|
||||||
|
|
||||||
|
# check ping received after reconnect
|
||||||
|
with self.subTest("LTE: receives ping"), Timeout(70, "no ping received"):
|
||||||
|
while not self._received_ping():
|
||||||
|
time.sleep(0.1)
|
||||||
|
print("ping received")
|
||||||
|
|
||||||
|
@unittest.skipIf(not TICI, "only run on desk")
|
||||||
|
def test_offroad(self) -> None:
|
||||||
|
write_onroad_params(False, self.params)
|
||||||
|
self.assertTimeout(60 + TIMEOUT_TOLERANCE) # based using TCP keepalive settings
|
||||||
|
|
||||||
|
@unittest.skipIf(not TICI, "only run on desk")
|
||||||
|
def test_onroad(self) -> None:
|
||||||
|
write_onroad_params(True, self.params)
|
||||||
|
self.assertTimeout(21 + TIMEOUT_TOLERANCE)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
81
selfdrive/athena/tests/test_registration.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID
|
||||||
|
from openpilot.selfdrive.athena.tests.helpers import MockResponse
|
||||||
|
from openpilot.system.hardware.hw import Paths
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegistration(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# clear params and setup key paths
|
||||||
|
self.params = Params()
|
||||||
|
self.params.clear_all()
|
||||||
|
|
||||||
|
persist_dir = Path(Paths.persist_root()) / "comma"
|
||||||
|
persist_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.priv_key = persist_dir / "id_rsa"
|
||||||
|
self.pub_key = persist_dir / "id_rsa.pub"
|
||||||
|
|
||||||
|
def _generate_keys(self):
|
||||||
|
self.pub_key.touch()
|
||||||
|
k = RSA.generate(2048)
|
||||||
|
with open(self.priv_key, "wb") as f:
|
||||||
|
f.write(k.export_key())
|
||||||
|
with open(self.pub_key, "wb") as f:
|
||||||
|
f.write(k.publickey().export_key())
|
||||||
|
|
||||||
|
def test_valid_cache(self):
|
||||||
|
# if all params are written, return the cached dongle id
|
||||||
|
self.params.put("IMEI", "imei")
|
||||||
|
self.params.put("HardwareSerial", "serial")
|
||||||
|
self._generate_keys()
|
||||||
|
|
||||||
|
with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
|
||||||
|
dongle = "DONGLE_ID_123"
|
||||||
|
self.params.put("DongleId", dongle)
|
||||||
|
self.assertEqual(register(), dongle)
|
||||||
|
self.assertFalse(m.called)
|
||||||
|
|
||||||
|
def test_no_keys(self):
|
||||||
|
# missing pubkey
|
||||||
|
with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
|
||||||
|
dongle = register()
|
||||||
|
self.assertEqual(m.call_count, 0)
|
||||||
|
self.assertEqual(dongle, UNREGISTERED_DONGLE_ID)
|
||||||
|
self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
|
||||||
|
|
||||||
|
def test_missing_cache(self):
|
||||||
|
# keys exist but no dongle id
|
||||||
|
self._generate_keys()
|
||||||
|
with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
|
||||||
|
dongle = "DONGLE_ID_123"
|
||||||
|
m.return_value = MockResponse(json.dumps({'dongle_id': dongle}), 200)
|
||||||
|
self.assertEqual(register(), dongle)
|
||||||
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
|
# call again, shouldn't hit the API this time
|
||||||
|
self.assertEqual(register(), dongle)
|
||||||
|
self.assertEqual(m.call_count, 1)
|
||||||
|
self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
|
||||||
|
|
||||||
|
def test_unregistered(self):
|
||||||
|
# keys exist, but unregistered
|
||||||
|
self._generate_keys()
|
||||||
|
with mock.patch("openpilot.selfdrive.athena.registration.api_get", autospec=True) as m:
|
||||||
|
m.return_value = MockResponse(None, 402)
|
||||||
|
dongle = register()
|
||||||
|
self.assertEqual(m.call_count, 1)
|
||||||
|
self.assertEqual(dongle, UNREGISTERED_DONGLE_ID)
|
||||||
|
self.assertEqual(self.params.get("DongleId", encoding='utf-8'), dongle)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
73
selfdrive/car/CARS_template.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{% set footnote_tag = '[<sup>{}</sup>](#footnotes)' %}
|
||||||
|
{% set star_icon = '[](##)' %}
|
||||||
|
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>' %}
|
||||||
|
{# Force hardware column wider by using a blank image with max width. #}
|
||||||
|
{% set width_tag = '<a href="##"><img width=2000></a>%s<br> ' %}
|
||||||
|
{% set hardware_col_name = 'Hardware Needed' %}
|
||||||
|
{% set wide_hardware_col_name = width_tag|format(hardware_col_name) -%}
|
||||||
|
|
||||||
|
<!--- AUTOGENERATED FROM selfdrive/car/CARS_template.md, DO NOT EDIT. --->
|
||||||
|
|
||||||
|
# Supported Cars
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
# {{all_car_docs | length}} Supported Cars
|
||||||
|
|
||||||
|
|{{Column | map(attribute='value') | join('|') | replace(hardware_col_name, wide_hardware_col_name)}}|
|
||||||
|
|---|---|---|{% for _ in range((Column | length) - 3) %}{{':---:|'}}{% endfor +%}
|
||||||
|
{% for car_docs in all_car_docs %}
|
||||||
|
|{% for column in Column %}{{car_docs.get_column(column, star_icon, video_icon, footnote_tag)}}|{% endfor %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
### Footnotes
|
||||||
|
{% for footnote in footnotes %}
|
||||||
|
<sup>{{loop.index}}</sup>{{footnote}} <br />
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
## 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/).
|
||||||
|
|
||||||
|
# Don't see your car here?
|
||||||
|
|
||||||
|
**openpilot can support many more cars than it currently does.** There are a few reasons your car may not be supported.
|
||||||
|
If your car doesn't fit into any of the incompatibility criteria here, then there's a good chance it can be supported! We're adding support for new cars all the time. **We don't have a roadmap for car support**, and in fact, most car support comes from users like you!
|
||||||
|
|
||||||
|
### Which cars are able to be supported?
|
||||||
|
|
||||||
|
openpilot uses the existing steering, gas, and brake interfaces in your car. If your car lacks any one of these interfaces, openpilot will not be able to control the car. If your car has [ACC](https://en.wikipedia.org/wiki/Adaptive_cruise_control) and any form of [LKAS](https://en.wikipedia.org/wiki/Automated_Lane_Keeping_Systems)/[LCA](https://en.wikipedia.org/wiki/Lane_centering), then it almost certainly has these interfaces. These features generally started shipping on cars around 2016. Note that manufacturers will often make their own [marketing terms](https://en.wikipedia.org/wiki/Adaptive_cruise_control#Vehicle_models_supporting_adaptive_cruise_control) for these features, such as Hyundai's "Smart Cruise Control" branding of Adaptive Cruise Control.
|
||||||
|
|
||||||
|
If your car has the following packages or features, then it's a good candidate for support.
|
||||||
|
|
||||||
|
| Make | Required Package/Features |
|
||||||
|
| ---- | ------------------------- |
|
||||||
|
| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. |
|
||||||
|
| Ford | Any car with Lane Centering will likely work. |
|
||||||
|
| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. |
|
||||||
|
| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. |
|
||||||
|
| Nissan | Any car with ProPILOT will likely work. |
|
||||||
|
| Toyota & Lexus | Any car that has Toyota/Lexus Safety Sense with "Lane Departure Alert with Steering Assist (LDA w/SA)" and/or "Lane Tracing Assist (LTA)" will work. Note that LDA without Steering Assist will not work. These features come standard on most newer models. |
|
||||||
|
| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA comes standard on most newer models. Any form of SCC will work, such as NSCC. |
|
||||||
|
| Chrysler, Jeep, & Ram | Any car with LaneSense and Adaptive Cruise Control will likely work. These come standard on many newer models. |
|
||||||
|
|
||||||
|
### FlexRay
|
||||||
|
|
||||||
|
All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a CAN bus isn't the only way that the computers in your car can communicate. Most, if not all, vehicles from the following manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars may one day be supported, but we have no immediate plans to support FlexRay.
|
||||||
|
|
||||||
|
### Toyota Security
|
||||||
|
|
||||||
|
openpilot does not yet support these Toyota models due to a new message authentication method.
|
||||||
|
[Vote](https://comma.ai/shop#toyota-security) if you'd like to see openpilot support on these models.
|
||||||
|
|
||||||
|
* Toyota RAV4 Prime 2021+
|
||||||
|
* Toyota Sienna 2021+
|
||||||
|
* Toyota Venza 2021+
|
||||||
|
* Toyota Sequoia 2023+
|
||||||
|
* Toyota Tundra 2022+
|
||||||
|
* Toyota Highlander 2024+
|
||||||
|
* Toyota Corolla Cross 2022+ (only US model)
|
||||||
|
* Lexus NX 2022+
|
||||||
|
* Toyota bZ4x 2023+
|
||||||
|
* Subaru Solterra 2023+
|
||||||
|
|
||||||
19
selfdrive/car/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Car port structure
|
||||||
|
|
||||||
|
### interface.py
|
||||||
|
Generic interface to send and receive messages from CAN (controlsd uses this to communicate with car)
|
||||||
|
|
||||||
|
### fingerprints.py
|
||||||
|
Fingerprints for matching to a specific car
|
||||||
|
|
||||||
|
### carcontroller.py
|
||||||
|
Builds CAN messages to send to car
|
||||||
|
|
||||||
|
##### carstate.py
|
||||||
|
Reads CAN from car and builds openpilot CarState message
|
||||||
|
|
||||||
|
##### values.py
|
||||||
|
Limits for actuation, general constants for cars, and supported car documentation
|
||||||
|
|
||||||
|
##### radar_interface.py
|
||||||
|
Interface for parsing radar points from the car
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# functions common among cars
|
# functions common among cars
|
||||||
from collections import namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
from typing import Dict, List, Optional
|
from dataclasses import dataclass
|
||||||
|
from enum import IntFlag, ReprEnum
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
import capnp
|
import capnp
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.numpy_fast import clip, interp
|
from openpilot.common.numpy_fast import clip, interp
|
||||||
|
from openpilot.common.utils import Freezable
|
||||||
|
from openpilot.selfdrive.car.docs_definitions import CarDocs
|
||||||
|
|
||||||
|
|
||||||
# kg of standard extra cargo to count for drive, gas, etc...
|
# kg of standard extra cargo to count for drive, gas, etc...
|
||||||
@@ -24,9 +28,9 @@ def apply_hysteresis(val: float, val_steady: float, hyst_gap: float) -> float:
|
|||||||
return val_steady
|
return val_steady
|
||||||
|
|
||||||
|
|
||||||
def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: Dict[int, capnp.lib.capnp._EnumModule],
|
def create_button_events(cur_btn: int, prev_btn: int, buttons_dict: dict[int, capnp.lib.capnp._EnumModule],
|
||||||
unpressed_btn: int = 0) -> List[capnp.lib.capnp._DynamicStructBuilder]:
|
unpressed_btn: int = 0) -> list[capnp.lib.capnp._DynamicStructBuilder]:
|
||||||
events: List[capnp.lib.capnp._DynamicStructBuilder] = []
|
events: list[capnp.lib.capnp._DynamicStructBuilder] = []
|
||||||
|
|
||||||
if cur_btn == prev_btn:
|
if cur_btn == prev_btn:
|
||||||
return events
|
return events
|
||||||
@@ -73,7 +77,10 @@ def scale_tire_stiffness(mass, wheelbase, center_to_front, tire_stiffness_factor
|
|||||||
return tire_stiffness_front, tire_stiffness_rear
|
return tire_stiffness_front, tire_stiffness_rear
|
||||||
|
|
||||||
|
|
||||||
def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> Dict[str, str]:
|
DbcDict = dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def dbc_dict(pt_dbc, radar_dbc, chassis_dbc=None, body_dbc=None) -> DbcDict:
|
||||||
return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc}
|
return {'pt': pt_dbc, 'radar': radar_dbc, 'chassis': chassis_dbc, 'body': body_dbc}
|
||||||
|
|
||||||
|
|
||||||
@@ -208,7 +215,7 @@ def get_safety_config(safety_model, safety_param = None):
|
|||||||
class CanBusBase:
|
class CanBusBase:
|
||||||
offset: int
|
offset: int
|
||||||
|
|
||||||
def __init__(self, CP, fingerprint: Optional[Dict[int, Dict[int, int]]]) -> None:
|
def __init__(self, CP, fingerprint: dict[int, dict[int, int]] | None) -> None:
|
||||||
if CP is None:
|
if CP is None:
|
||||||
assert fingerprint is not None
|
assert fingerprint is not None
|
||||||
num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1
|
num = max([k for k, v in fingerprint.items() if len(v)], default=0) // 4 + 1
|
||||||
@@ -236,3 +243,75 @@ class CanSignalRateCalculator:
|
|||||||
self.previous_value = current_value
|
self.previous_value = current_value
|
||||||
|
|
||||||
return self.rate
|
return self.rate
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class CarSpecs:
|
||||||
|
mass: float # kg, curb weight
|
||||||
|
wheelbase: float # meters
|
||||||
|
steerRatio: float
|
||||||
|
centerToFrontRatio: float = 0.5
|
||||||
|
minSteerSpeed: float = 0.0 # m/s
|
||||||
|
minEnableSpeed: float = -1.0 # m/s
|
||||||
|
tireStiffnessFactor: float = 1.0
|
||||||
|
|
||||||
|
def override(self, **kwargs):
|
||||||
|
return replace(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(order=True)
|
||||||
|
class PlatformConfig(Freezable):
|
||||||
|
platform_str: str
|
||||||
|
car_docs: list[CarDocs]
|
||||||
|
specs: CarSpecs
|
||||||
|
|
||||||
|
dbc_dict: DbcDict
|
||||||
|
|
||||||
|
flags: int = 0
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.platform_str)
|
||||||
|
|
||||||
|
def override(self, **kwargs):
|
||||||
|
return replace(self, **kwargs)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.init()
|
||||||
|
self.freeze()
|
||||||
|
|
||||||
|
|
||||||
|
class Platforms(str, ReprEnum):
|
||||||
|
config: PlatformConfig
|
||||||
|
|
||||||
|
def __new__(cls, platform_config: PlatformConfig):
|
||||||
|
member = str.__new__(cls, platform_config.platform_str)
|
||||||
|
member.config = platform_config
|
||||||
|
member._value_ = platform_config.platform_str
|
||||||
|
return member
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_dbc_map(cls) -> dict[str, DbcDict]:
|
||||||
|
return {p: p.config.dbc_dict for p in cls}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def with_flags(cls, flags: IntFlag) -> set['Platforms']:
|
||||||
|
return {p for p in cls if p.config.flags & flags}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def without_flags(cls, flags: IntFlag) -> set['Platforms']:
|
||||||
|
return {p for p in cls if not (p.config.flags & flags)}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def print_debug(cls, flags):
|
||||||
|
platforms_with_flag = defaultdict(list)
|
||||||
|
for flag in flags:
|
||||||
|
for platform in cls:
|
||||||
|
if platform.config.flags & flag:
|
||||||
|
assert flag.name is not None
|
||||||
|
platforms_with_flag[flag.name].append(platform)
|
||||||
|
|
||||||
|
for flag, platforms in platforms_with_flag.items():
|
||||||
|
print(f"{flag:32s}: {', '.join(p.name for p in platforms)}")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from openpilot.common.realtime import DT_CTRL
|
|||||||
from opendbc.can.packer import CANPacker
|
from opendbc.can.packer import CANPacker
|
||||||
from openpilot.selfdrive.car.body import bodycan
|
from openpilot.selfdrive.car.body import bodycan
|
||||||
from openpilot.selfdrive.car.body.values import SPEED_FROM_RPM
|
from openpilot.selfdrive.car.body.values import SPEED_FROM_RPM
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarControllerBase
|
||||||
from openpilot.selfdrive.controls.lib.pid import PIDController
|
from openpilot.selfdrive.controls.lib.pid import PIDController
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ MAX_POS_INTEGRATOR = 0.2 # meters
|
|||||||
MAX_TURN_INTEGRATOR = 0.1 # meters
|
MAX_TURN_INTEGRATOR = 0.1 # meters
|
||||||
|
|
||||||
|
|
||||||
class CarController:
|
class CarController(CarControllerBase):
|
||||||
def __init__(self, dbc_name, CP, VM):
|
def __init__(self, dbc_name, CP, VM):
|
||||||
self.frame = 0
|
self.frame = 0
|
||||||
self.packer = CANPacker(dbc_name)
|
self.packer = CANPacker(dbc_name)
|
||||||
|
|||||||
@@ -7,21 +7,17 @@ from openpilot.selfdrive.car.body.values import SPEED_FROM_RPM
|
|||||||
|
|
||||||
class CarInterface(CarInterfaceBase):
|
class CarInterface(CarInterfaceBase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_params(ret, params, candidate, fingerprint, car_fw, experimental_long, docs):
|
def _get_params(ret, params, candidate, fingerprint, car_fw, disable_openpilot_long, experimental_long, docs):
|
||||||
ret.notCar = True
|
ret.notCar = True
|
||||||
ret.carName = "body"
|
ret.carName = "body"
|
||||||
ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.body)]
|
ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.body)]
|
||||||
|
|
||||||
ret.minSteerSpeed = -math.inf
|
ret.minSteerSpeed = -math.inf
|
||||||
ret.maxLateralAccel = math.inf # TODO: set to a reasonable value
|
ret.maxLateralAccel = math.inf # TODO: set to a reasonable value
|
||||||
ret.steerRatio = 0.5
|
|
||||||
ret.steerLimitTimer = 1.0
|
ret.steerLimitTimer = 1.0
|
||||||
ret.steerActuatorDelay = 0.
|
ret.steerActuatorDelay = 0.
|
||||||
|
|
||||||
ret.mass = 9
|
|
||||||
ret.wheelbase = 0.406
|
|
||||||
ret.wheelSpeedFactor = SPEED_FROM_RPM
|
ret.wheelSpeedFactor = SPEED_FROM_RPM
|
||||||
ret.centerToFront = ret.wheelbase * 0.44
|
|
||||||
|
|
||||||
ret.radarUnavailable = True
|
ret.radarUnavailable = True
|
||||||
ret.openpilotLongitudinalControl = True
|
ret.openpilotLongitudinalControl = True
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from enum import StrEnum
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.selfdrive.car import dbc_dict
|
from openpilot.selfdrive.car import CarSpecs, PlatformConfig, Platforms, dbc_dict
|
||||||
from openpilot.selfdrive.car.docs_definitions import CarInfo
|
from openpilot.selfdrive.car.docs_definitions import CarDocs
|
||||||
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
|
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
|
||||||
|
|
||||||
Ecu = car.CarParams.Ecu
|
Ecu = car.CarParams.Ecu
|
||||||
@@ -22,13 +19,13 @@ class CarControllerParams:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CAR(StrEnum):
|
class CAR(Platforms):
|
||||||
BODY = "COMMA BODY"
|
BODY = PlatformConfig(
|
||||||
|
"COMMA BODY",
|
||||||
|
[CarDocs("comma body", package="All")],
|
||||||
CAR_INFO: Dict[str, CarInfo] = {
|
CarSpecs(mass=9, wheelbase=0.406, steerRatio=0.5, centerToFrontRatio=0.44),
|
||||||
CAR.BODY: CarInfo("comma body", package="All"),
|
dbc_dict('comma_body', None),
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
FW_QUERY_CONFIG = FwQueryConfig(
|
FW_QUERY_CONFIG = FwQueryConfig(
|
||||||
@@ -41,7 +38,4 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DBC = CAR.create_dbc_map()
|
||||||
DBC = {
|
|
||||||
CAR.BODY: dbc_dict('comma_body', None),
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
|
||||||
import sentry_sdk
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Dict, List, Optional, Tuple
|
from collections.abc import Callable
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
@@ -13,6 +11,7 @@ from openpilot.selfdrive.car.interfaces import get_interface_attr
|
|||||||
from openpilot.selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars
|
from openpilot.selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars
|
||||||
from openpilot.selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
|
from openpilot.selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
|
||||||
from openpilot.selfdrive.car.fw_versions import get_fw_versions_ordered, get_present_ecus, match_fw_to_car, set_obd_multiplexing
|
from openpilot.selfdrive.car.fw_versions import get_fw_versions_ordered, get_present_ecus, match_fw_to_car, set_obd_multiplexing
|
||||||
|
from openpilot.selfdrive.car.mock.values import CAR as MOCK
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
import openpilot.selfdrive.sentry as sentry
|
import openpilot.selfdrive.sentry as sentry
|
||||||
@@ -67,7 +66,7 @@ def load_interfaces(brand_names):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def _get_interface_names() -> Dict[str, List[str]]:
|
def _get_interface_names() -> dict[str, list[str]]:
|
||||||
# returns a dict of brand name and its respective models
|
# returns a dict of brand name and its respective models
|
||||||
brand_names = {}
|
brand_names = {}
|
||||||
for brand_name, brand_models in get_interface_attr("CAR").items():
|
for brand_name, brand_models in get_interface_attr("CAR").items():
|
||||||
@@ -81,7 +80,7 @@ interface_names = _get_interface_names()
|
|||||||
interfaces = load_interfaces(interface_names)
|
interfaces = load_interfaces(interface_names)
|
||||||
|
|
||||||
|
|
||||||
def can_fingerprint(next_can: Callable) -> Tuple[Optional[str], Dict[int, dict]]:
|
def can_fingerprint(next_can: Callable) -> tuple[str | None, dict[int, dict]]:
|
||||||
finger = gen_empty_fingerprint()
|
finger = gen_empty_fingerprint()
|
||||||
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
|
candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1
|
||||||
frame = 0
|
frame = 0
|
||||||
@@ -193,75 +192,18 @@ def fingerprint(logcan, sendcan, num_pandas):
|
|||||||
cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, cached=cached,
|
cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, cached=cached,
|
||||||
fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, vin_rx_bus=vin_rx_bus,
|
fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, vin_rx_bus=vin_rx_bus,
|
||||||
fingerprints=repr(finger), fw_query_time=fw_query_time, error=True)
|
fingerprints=repr(finger), fw_query_time=fw_query_time, error=True)
|
||||||
|
|
||||||
return car_fingerprint, finger, vin, car_fw, source, exact_match
|
return car_fingerprint, finger, vin, car_fw, source, exact_match
|
||||||
|
|
||||||
def chunk_data(data, size):
|
|
||||||
return [data[i:i+size] for i in range(0, len(data), size)]
|
|
||||||
|
|
||||||
def format_params(params):
|
def get_car_interface(CP):
|
||||||
return [f"{key}: {value.decode('utf-8') if isinstance(value, bytes) else value}" for key, value in params.items()]
|
CarInterface, CarController, CarState = interfaces[CP.carFingerprint]
|
||||||
|
return CarInterface(CP, CarController, CarState)
|
||||||
|
|
||||||
def get_frogpilot_params(params, keys):
|
|
||||||
return {key: params.get(key) or '0' for key in keys}
|
|
||||||
|
|
||||||
def set_sentry_scope(scope, chunks, label):
|
def get_car(params, logcan, sendcan, disable_openpilot_long, experimental_long_allowed, num_pandas=1):
|
||||||
scope.set_extra(label, '\n'.join(['\n'.join(chunk) for chunk in chunks]))
|
|
||||||
|
|
||||||
def is_connected_to_internet(timeout=5):
|
|
||||||
try:
|
|
||||||
requests.get("https://sentry.io", timeout=timeout)
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def crash_log(params, candidate):
|
|
||||||
serial_id = params.get("HardwareSerial", encoding='utf-8')
|
|
||||||
|
|
||||||
control_keys, vehicle_keys, visual_keys, tracking_keys = [
|
|
||||||
"AdjustablePersonalities", "PersonalitiesViaWheel", "PersonalitiesViaScreen", "AlwaysOnLateral", "AlwaysOnLateralMain",
|
|
||||||
"ConditionalExperimental", "CESpeed", "CESpeedLead", "CECurves", "CECurvesLead", "CENavigation", "CENavigationIntersections",
|
|
||||||
"CENavigationLead", "CENavigationTurns", "CESlowerLead", "CEStopLights", "CEStopLightsLead", "CESignal", "CustomPersonalities",
|
|
||||||
"AggressiveFollow", "AggressiveJerk", "StandardFollow", "StandardJerk", "RelaxedFollow", "RelaxedJerk", "DeviceShutdown",
|
|
||||||
"ExperimentalModeActivation", "ExperimentalModeViaLKAS", "ExperimentalModeViaScreen", "FireTheBabysitter", "NoLogging", "MuteOverheated",
|
|
||||||
"OfflineMode", "LateralTune", "ForceAutoTune", "NNFF", "SteerRatio", "UseLateralJerk", "LongitudinalTune", "AccelerationProfile",
|
|
||||||
"DecelerationProfile", "AggressiveAcceleration", "StoppingDistance", "LeadDetectionThreshold", "SmoothBraking", "Model", "MTSCEnabled",
|
|
||||||
"DisableMTSCSmoothing", "MTSCAggressiveness", "MTSCCurvatureCheck", "MTSCLimit", "NudgelessLaneChange", "LaneChangeTime", "LaneDetection",
|
|
||||||
"LaneDetectionWidth", "OneLaneChange", "QOLControls", "DisableOnroadUploads", "HigherBitrate", "NavChill", "PauseLateralOnSignal", "ReverseCruise",
|
|
||||||
"ReverseCruiseUI", "SetSpeedLimit", "SetSpeedOffset", "SpeedLimitController", "Offset1", "Offset2", "Offset3", "Offset4", "SLCConfirmation",
|
|
||||||
"SLCFallback", "SLCPriority1", "SLCPriority2", "SLCPriority3", "SLCOverride", "TurnDesires", "VisionTurnControl", "DisableVTSCSmoothing",
|
|
||||||
"CurveSensitivity", "TurnAggressiveness"
|
|
||||||
], [
|
|
||||||
"ForceFingerprint", "DisableOpenpilotLongitudinal", "EVTable", "GasRegenCmd", "LongPitch", "LowerVolt", "CrosstrekTorque", "CydiaTune",
|
|
||||||
"DragonPilotTune", "FrogsGoMooTune", "LockDoors", "SNGHack"
|
|
||||||
], [
|
|
||||||
"CustomTheme", "HolidayThemes", "CustomColors", "CustomIcons", "CustomSignals", "CustomSounds", "GoatScream", "AlertVolumeControl", "DisengageVolume",
|
|
||||||
"EngageVolume", "PromptVolume", "PromptDistractedVolume", "RefuseVolume", "WarningSoftVolume", "WarningImmediateVolume", "CameraView",
|
|
||||||
"Compass", "CustomAlerts", "GreenLightAlert", "LeadDepartingAlert", "LoudBlindspotAlert", "SpeedLimitChangedAlert", "CustomUI", "AccelerationPath",
|
|
||||||
"AdjacentPath", "AdjacentPathMetrics", "BlindSpotPath", "FPSCounter", "LeadInfo", "UseSI", "PedalsOnUI", "RoadNameUI", "UseVienna", "DriverCamera",
|
|
||||||
"ModelUI", "DynamicPathWidth", "LaneLinesWidth", "PathEdgeWidth", "PathWidth", "RoadEdgesWidth", "UnlimitedLength", "QOLVisuals", "DriveStats",
|
|
||||||
"FullMap", "HideSpeed", "HideSpeedUI", "ShowSLCOffset", "SpeedLimitChangedAlert", "WheelSpeed", "RandomEvents", "ScreenBrightness", "WheelIcon",
|
|
||||||
"RotatingWheel", "NumericalTemp", "Fahrenheit", "ShowCPU", "ShowGPU", "ShowIP", "ShowMemoryUsage", "ShowStorageLeft", "ShowStorageUsed", "Sidebar"
|
|
||||||
], [
|
|
||||||
"FrogPilotDrives", "FrogPilotKilometers", "FrogPilotMinutes"
|
|
||||||
]
|
|
||||||
|
|
||||||
control_params, vehicle_params, visual_params, tracking_params = map(lambda keys: get_frogpilot_params(params, keys), [control_keys, vehicle_keys, visual_keys, tracking_keys])
|
|
||||||
control_values, vehicle_values, visual_values, tracking_values = map(format_params, [control_params, vehicle_params, visual_params, tracking_params])
|
|
||||||
control_chunks, vehicle_chunks, visual_chunks, tracking_chunks = map(lambda data: chunk_data(data, 50), [control_values, vehicle_values, visual_values, tracking_values])
|
|
||||||
|
|
||||||
while not is_connected_to_internet():
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
with sentry_sdk.configure_scope() as scope:
|
|
||||||
for chunks, label in zip([control_chunks, vehicle_chunks, visual_chunks, tracking_chunks], ["FrogPilot Controls", "FrogPilot Vehicles", "FrogPilot Visuals", "FrogPilot Tracking"]):
|
|
||||||
set_sentry_scope(scope, chunks, label)
|
|
||||||
sentry.capture_warning(f"Fingerprinted: {candidate}", serial_id)
|
|
||||||
|
|
||||||
def get_car(logcan, sendcan, experimental_long_allowed, num_pandas=1):
|
|
||||||
params = Params()
|
|
||||||
car_brand = params.get("CarMake", encoding='utf-8')
|
car_brand = params.get("CarMake", encoding='utf-8')
|
||||||
car_model = params.get("CarModel", encoding='utf-8')
|
car_model = params.get("CarModel", encoding='utf-8')
|
||||||
dongle_id = params.get("DongleId", encoding='utf-8')
|
|
||||||
|
|
||||||
force_fingerprint = params.get_bool("ForceFingerprint")
|
force_fingerprint = params.get_bool("ForceFingerprint")
|
||||||
|
|
||||||
@@ -273,33 +215,37 @@ def get_car(logcan, sendcan, experimental_long_allowed, num_pandas=1):
|
|||||||
else:
|
else:
|
||||||
cloudlog.event("car doesn't match any fingerprints", fingerprints=repr(fingerprints), error=True)
|
cloudlog.event("car doesn't match any fingerprints", fingerprints=repr(fingerprints), error=True)
|
||||||
candidate = "mock"
|
candidate = "mock"
|
||||||
elif car_model is None:
|
|
||||||
|
if car_model is None and candidate != "mock":
|
||||||
params.put("CarMake", candidate.split(' ')[0].title())
|
params.put("CarMake", candidate.split(' ')[0].title())
|
||||||
params.put("CarModel", candidate)
|
params.put("CarModel", candidate)
|
||||||
|
|
||||||
if get_short_branch() == "FrogPilot-Development" and not Params("/persist/comma/params").get_bool("FrogsGoMoo"):
|
if get_short_branch() == "FrogPilot-Development" and not Params("/persist/params").get_bool("FrogsGoMoo"):
|
||||||
|
cloudlog.event("Blocked user from using the 'FrogPilot-Development' branch", fingerprints=repr(fingerprints), error=True)
|
||||||
candidate = "mock"
|
candidate = "mock"
|
||||||
|
fingerprint_log = threading.Thread(target=sentry.capture_fingerprint, args=(params, candidate, True,))
|
||||||
|
fingerprint_log.start()
|
||||||
|
elif not params.get_bool("FingerprintLogged"):
|
||||||
|
fingerprint_log = threading.Thread(target=sentry.capture_fingerprint, args=(params, candidate,))
|
||||||
|
fingerprint_log.start()
|
||||||
|
|
||||||
setFingerprintLog = threading.Thread(target=crash_log, args=(params, candidate,))
|
CarInterface, _, _ = interfaces[candidate]
|
||||||
setFingerprintLog.start()
|
CP = CarInterface.get_params(params, candidate, fingerprints, car_fw, disable_openpilot_long, experimental_long_allowed, docs=False)
|
||||||
|
|
||||||
CarInterface, CarController, CarState = interfaces[candidate]
|
|
||||||
CP = CarInterface.get_params(params, candidate, fingerprints, car_fw, experimental_long_allowed, docs=False)
|
|
||||||
CP.carVin = vin
|
CP.carVin = vin
|
||||||
CP.carFw = car_fw
|
CP.carFw = car_fw
|
||||||
CP.fingerprintSource = source
|
CP.fingerprintSource = source
|
||||||
CP.fuzzyFingerprint = not exact_match
|
CP.fuzzyFingerprint = not exact_match
|
||||||
|
|
||||||
return CarInterface(CP, CarController, CarState), CP
|
return get_car_interface(CP), CP
|
||||||
|
|
||||||
def write_car_param(fingerprint="mock"):
|
def write_car_param(platform=MOCK.MOCK):
|
||||||
params = Params()
|
params = Params()
|
||||||
CarInterface, _, _ = interfaces[fingerprint]
|
CarInterface, _, _ = interfaces[platform]
|
||||||
CP = CarInterface.get_non_essential_params(fingerprint)
|
CP = CarInterface.get_non_essential_params(platform)
|
||||||
params.put("CarParams", CP.to_bytes())
|
params.put("CarParams", CP.to_bytes())
|
||||||
|
|
||||||
def get_demo_car_params():
|
def get_demo_car_params():
|
||||||
fingerprint="mock"
|
platform = MOCK.MOCK
|
||||||
CarInterface, _, _ = interfaces[fingerprint]
|
CarInterface, _, _ = interfaces[platform]
|
||||||
CP = CarInterface.get_non_essential_params(fingerprint)
|
CP = CarInterface.get_non_essential_params(platform)
|
||||||
return CP
|
return CP
|
||||||
|
|||||||
148
selfdrive/car/card.py
Executable file
@@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import cereal.messaging as messaging
|
||||||
|
|
||||||
|
from cereal import car
|
||||||
|
|
||||||
|
from panda import ALTERNATIVE_EXPERIENCE
|
||||||
|
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.realtime import DT_CTRL
|
||||||
|
|
||||||
|
from openpilot.selfdrive.boardd.boardd import can_list_to_can_capnp
|
||||||
|
from openpilot.selfdrive.car.car_helpers import get_car, get_one_can
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
||||||
|
|
||||||
|
|
||||||
|
REPLAY = "REPLAY" in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
class CarD:
|
||||||
|
CI: CarInterfaceBase
|
||||||
|
CS: car.CarState
|
||||||
|
|
||||||
|
def __init__(self, CI=None):
|
||||||
|
self.can_sock = messaging.sub_sock('can', timeout=20)
|
||||||
|
self.sm = messaging.SubMaster(['pandaStates'])
|
||||||
|
self.pm = messaging.PubMaster(['sendcan', 'carState', 'carParams', 'carOutput'])
|
||||||
|
|
||||||
|
self.can_rcv_timeout_counter = 0 # conseuctive timeout count
|
||||||
|
self.can_rcv_cum_timeout_counter = 0 # cumulative timeout count
|
||||||
|
|
||||||
|
self.CC_prev = car.CarControl.new_message()
|
||||||
|
|
||||||
|
self.last_actuators = None
|
||||||
|
|
||||||
|
self.params = Params()
|
||||||
|
|
||||||
|
if CI is None:
|
||||||
|
# wait for one pandaState and one CAN packet
|
||||||
|
print("Waiting for CAN messages...")
|
||||||
|
get_one_can(self.can_sock)
|
||||||
|
|
||||||
|
num_pandas = len(messaging.recv_one_retry(self.sm.sock['pandaStates']).pandaStates)
|
||||||
|
disable_openpilot_long = self.params.get_bool("DisableOpenpilotLongitudinal")
|
||||||
|
experimental_long_allowed = not disable_openpilot_long and self.params.get_bool("ExperimentalLongitudinalEnabled")
|
||||||
|
self.CI, self.CP = get_car(self.params, self.can_sock, self.pm.sock['sendcan'], disable_openpilot_long, experimental_long_allowed, num_pandas)
|
||||||
|
else:
|
||||||
|
self.CI, self.CP = CI, CI.CP
|
||||||
|
|
||||||
|
# set alternative experiences from parameters
|
||||||
|
disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
|
self.CP.alternativeExperience = 0
|
||||||
|
if not disengage_on_accelerator:
|
||||||
|
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS
|
||||||
|
|
||||||
|
always_on_lateral = self.params.get_bool("AlwaysOnLateral")
|
||||||
|
if always_on_lateral:
|
||||||
|
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL
|
||||||
|
|
||||||
|
self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX
|
||||||
|
|
||||||
|
car_recognized = self.CP.carName != 'mock'
|
||||||
|
openpilot_enabled_toggle = self.params.get_bool("OpenpilotEnabledToggle")
|
||||||
|
|
||||||
|
controller_available = self.CI.CC is not None and openpilot_enabled_toggle and not self.CP.dashcamOnly
|
||||||
|
|
||||||
|
self.CP.passive = not car_recognized or not controller_available or self.CP.dashcamOnly
|
||||||
|
if self.CP.passive:
|
||||||
|
safety_config = car.CarParams.SafetyConfig.new_message()
|
||||||
|
safety_config.safetyModel = car.CarParams.SafetyModel.noOutput
|
||||||
|
self.CP.safetyConfigs = [safety_config]
|
||||||
|
|
||||||
|
# Write previous route's CarParams
|
||||||
|
prev_cp = self.params.get("CarParamsPersistent")
|
||||||
|
if prev_cp is not None:
|
||||||
|
self.params.put("CarParamsPrevRoute", prev_cp)
|
||||||
|
|
||||||
|
# Write CarParams for controls and radard
|
||||||
|
cp_bytes = self.CP.to_bytes()
|
||||||
|
self.params.put("CarParams", cp_bytes)
|
||||||
|
self.params.put_nonblocking("CarParamsCache", cp_bytes)
|
||||||
|
self.params.put_nonblocking("CarParamsPersistent", cp_bytes)
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Initialize CarInterface, once controls are ready"""
|
||||||
|
self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan'])
|
||||||
|
|
||||||
|
def state_update(self, frogpilot_variables):
|
||||||
|
"""carState update loop, driven by can"""
|
||||||
|
|
||||||
|
# Update carState from CAN
|
||||||
|
can_strs = messaging.drain_sock_raw(self.can_sock, wait_for_one=True)
|
||||||
|
self.CS = self.CI.update(self.CC_prev, can_strs, frogpilot_variables)
|
||||||
|
|
||||||
|
self.sm.update(0)
|
||||||
|
|
||||||
|
can_rcv_valid = len(can_strs) > 0
|
||||||
|
|
||||||
|
# Check for CAN timeout
|
||||||
|
if not can_rcv_valid:
|
||||||
|
self.can_rcv_timeout_counter += 1
|
||||||
|
self.can_rcv_cum_timeout_counter += 1
|
||||||
|
else:
|
||||||
|
self.can_rcv_timeout_counter = 0
|
||||||
|
|
||||||
|
self.can_rcv_timeout = self.can_rcv_timeout_counter >= 5
|
||||||
|
|
||||||
|
if can_rcv_valid and REPLAY:
|
||||||
|
self.can_log_mono_time = messaging.log_from_bytes(can_strs[0]).logMonoTime
|
||||||
|
|
||||||
|
self.state_publish()
|
||||||
|
|
||||||
|
return self.CS
|
||||||
|
|
||||||
|
def state_publish(self):
|
||||||
|
"""carState and carParams publish loop"""
|
||||||
|
|
||||||
|
# carState
|
||||||
|
cs_send = messaging.new_message('carState')
|
||||||
|
cs_send.valid = self.CS.canValid
|
||||||
|
cs_send.carState = self.CS
|
||||||
|
self.pm.send('carState', cs_send)
|
||||||
|
|
||||||
|
# carParams - logged every 50 seconds (> 1 per segment)
|
||||||
|
if (self.sm.frame % int(50. / DT_CTRL) == 0):
|
||||||
|
cp_send = messaging.new_message('carParams')
|
||||||
|
cp_send.valid = True
|
||||||
|
cp_send.carParams = self.CP
|
||||||
|
self.pm.send('carParams', cp_send)
|
||||||
|
|
||||||
|
# publish new carOutput
|
||||||
|
co_send = messaging.new_message('carOutput')
|
||||||
|
co_send.valid = True
|
||||||
|
if self.last_actuators is not None:
|
||||||
|
co_send.carOutput.actuatorsOutput = self.last_actuators
|
||||||
|
self.pm.send('carOutput', co_send)
|
||||||
|
|
||||||
|
def controls_update(self, CC: car.CarControl, frogpilot_variables):
|
||||||
|
"""control update loop, driven by carControl"""
|
||||||
|
|
||||||
|
# send car controls over can
|
||||||
|
now_nanos = self.can_log_mono_time if REPLAY else int(time.monotonic() * 1e9)
|
||||||
|
self.last_actuators, can_sends = self.CI.apply(CC, now_nanos, frogpilot_variables)
|
||||||
|
self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=self.CS.canValid))
|
||||||
|
|
||||||
|
self.CC_prev = CC
|
||||||
@@ -3,9 +3,10 @@ from openpilot.common.realtime import DT_CTRL
|
|||||||
from openpilot.selfdrive.car import apply_meas_steer_torque_limits
|
from openpilot.selfdrive.car import apply_meas_steer_torque_limits
|
||||||
from openpilot.selfdrive.car.chrysler import chryslercan
|
from openpilot.selfdrive.car.chrysler import chryslercan
|
||||||
from openpilot.selfdrive.car.chrysler.values import RAM_CARS, CarControllerParams, ChryslerFlags
|
from openpilot.selfdrive.car.chrysler.values import RAM_CARS, CarControllerParams, ChryslerFlags
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarControllerBase
|
||||||
|
|
||||||
|
|
||||||
class CarController:
|
class CarController(CarControllerBase):
|
||||||
def __init__(self, dbc_name, CP, VM):
|
def __init__(self, dbc_name, CP, VM):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
self.apply_steer_last = 0
|
self.apply_steer_last = 0
|
||||||
|
|||||||
@@ -21,10 +21,16 @@ class CarState(CarStateBase):
|
|||||||
else:
|
else:
|
||||||
self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
|
self.shifter_values = can_define.dv["GEAR"]["PRNDL"]
|
||||||
|
|
||||||
|
self.prev_distance_button = 0
|
||||||
|
self.distance_button = 0
|
||||||
|
|
||||||
def update(self, cp, cp_cam, frogpilot_variables):
|
def update(self, cp, cp_cam, frogpilot_variables):
|
||||||
|
|
||||||
ret = car.CarState.new_message()
|
ret = car.CarState.new_message()
|
||||||
|
|
||||||
|
self.prev_distance_button = self.distance_button
|
||||||
|
self.distance_button = cp.vl["CRUISE_BUTTONS"]["ACC_Distance_Dec"]
|
||||||
|
|
||||||
# lock info
|
# lock info
|
||||||
ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"],
|
ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"],
|
||||||
cp.vl["BCM_1"]["DOOR_OPEN_FR"],
|
cp.vl["BCM_1"]["DOOR_OPEN_FR"],
|
||||||
@@ -95,6 +101,12 @@ class CarState(CarStateBase):
|
|||||||
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
|
self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"]
|
||||||
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
|
self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"]
|
||||||
|
|
||||||
|
self.lkas_previously_enabled = self.lkas_enabled
|
||||||
|
if self.CP.carFingerprint in RAM_CARS:
|
||||||
|
self.lkas_enabled = cp.vl["Center_Stack_2"]["LKAS_Button"] or cp.vl["Center_Stack_1"]["LKAS_Button"]
|
||||||
|
else:
|
||||||
|
self.lkas_enabled = cp.vl["TRACTION_BUTTON"]["TOGGLE_LKAS"] == 1
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ FW_VERSIONS = {
|
|||||||
b'68227902AF',
|
b'68227902AF',
|
||||||
b'68227902AG',
|
b'68227902AG',
|
||||||
b'68227902AH',
|
b'68227902AH',
|
||||||
|
b'68227905AG',
|
||||||
b'68360252AC',
|
b'68360252AC',
|
||||||
],
|
],
|
||||||
(Ecu.srs, 0x744, None): [
|
(Ecu.srs, 0x744, None): [
|
||||||
@@ -71,6 +72,7 @@ FW_VERSIONS = {
|
|||||||
b'68340762AD ',
|
b'68340762AD ',
|
||||||
b'68340764AD ',
|
b'68340764AD ',
|
||||||
b'68352652AE ',
|
b'68352652AE ',
|
||||||
|
b'68352654AE ',
|
||||||
b'68366851AH ',
|
b'68366851AH ',
|
||||||
b'68366853AE ',
|
b'68366853AE ',
|
||||||
b'68372861AF ',
|
b'68372861AF ',
|
||||||
@@ -93,6 +95,7 @@ FW_VERSIONS = {
|
|||||||
b'68405327AC',
|
b'68405327AC',
|
||||||
b'68436233AB',
|
b'68436233AB',
|
||||||
b'68436233AC',
|
b'68436233AC',
|
||||||
|
b'68436234AB',
|
||||||
b'68436250AE',
|
b'68436250AE',
|
||||||
b'68529067AA',
|
b'68529067AA',
|
||||||
b'68594993AB',
|
b'68594993AB',
|
||||||
@@ -304,6 +307,7 @@ FW_VERSIONS = {
|
|||||||
b'68402708AB',
|
b'68402708AB',
|
||||||
b'68402971AD',
|
b'68402971AD',
|
||||||
b'68454144AD',
|
b'68454144AD',
|
||||||
|
b'68454145AB',
|
||||||
b'68454152AB',
|
b'68454152AB',
|
||||||
b'68454156AB',
|
b'68454156AB',
|
||||||
b'68516650AB',
|
b'68516650AB',
|
||||||
@@ -376,6 +380,7 @@ FW_VERSIONS = {
|
|||||||
b'68434859AC',
|
b'68434859AC',
|
||||||
b'68434860AC',
|
b'68434860AC',
|
||||||
b'68453483AC',
|
b'68453483AC',
|
||||||
|
b'68453483AD',
|
||||||
b'68453487AD',
|
b'68453487AD',
|
||||||
b'68453491AC',
|
b'68453491AC',
|
||||||
b'68453499AD',
|
b'68453499AD',
|
||||||
@@ -401,6 +406,7 @@ FW_VERSIONS = {
|
|||||||
b'68527383AD',
|
b'68527383AD',
|
||||||
b'68527387AE',
|
b'68527387AE',
|
||||||
b'68527403AC',
|
b'68527403AC',
|
||||||
|
b'68527403AD',
|
||||||
b'68546047AF',
|
b'68546047AF',
|
||||||
b'68631938AA',
|
b'68631938AA',
|
||||||
b'68631942AA',
|
b'68631942AA',
|
||||||
@@ -474,6 +480,7 @@ FW_VERSIONS = {
|
|||||||
],
|
],
|
||||||
(Ecu.engine, 0x7e0, None): [
|
(Ecu.engine, 0x7e0, None): [
|
||||||
b'05035699AG ',
|
b'05035699AG ',
|
||||||
|
b'05035841AC ',
|
||||||
b'05036026AB ',
|
b'05036026AB ',
|
||||||
b'05036065AE ',
|
b'05036065AE ',
|
||||||
b'05036066AE ',
|
b'05036066AE ',
|
||||||
@@ -506,11 +513,13 @@ FW_VERSIONS = {
|
|||||||
b'68455145AE ',
|
b'68455145AE ',
|
||||||
b'68455146AC ',
|
b'68455146AC ',
|
||||||
b'68467915AC ',
|
b'68467915AC ',
|
||||||
|
b'68467916AC ',
|
||||||
b'68467936AC ',
|
b'68467936AC ',
|
||||||
b'68500630AD',
|
b'68500630AD',
|
||||||
b'68500630AE',
|
b'68500630AE',
|
||||||
b'68502719AC ',
|
b'68502719AC ',
|
||||||
b'68502722AC ',
|
b'68502722AC ',
|
||||||
|
b'68502733AC ',
|
||||||
b'68502734AF ',
|
b'68502734AF ',
|
||||||
b'68502740AF ',
|
b'68502740AF ',
|
||||||
b'68502741AF ',
|
b'68502741AF ',
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from cereal import car
|
from cereal import car, custom
|
||||||
from panda import Panda
|
from panda import Panda
|
||||||
from openpilot.selfdrive.car import get_safety_config
|
from openpilot.selfdrive.car import create_button_events, get_safety_config
|
||||||
from openpilot.selfdrive.car.chrysler.values import CAR, RAM_HD, RAM_DT, RAM_CARS, ChryslerFlags
|
from openpilot.selfdrive.car.chrysler.values import CAR, RAM_HD, RAM_DT, RAM_CARS, ChryslerFlags
|
||||||
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
||||||
|
|
||||||
|
ButtonType = car.CarState.ButtonEvent.Type
|
||||||
|
FrogPilotButtonType = custom.FrogPilotCarState.ButtonEvent.Type
|
||||||
|
|
||||||
|
|
||||||
class CarInterface(CarInterfaceBase):
|
class CarInterface(CarInterfaceBase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_params(ret, params, candidate, fingerprint, car_fw, experimental_long, docs):
|
def _get_params(ret, params, candidate, fingerprint, car_fw, disable_openpilot_long, experimental_long, docs):
|
||||||
ret.carName = "chrysler"
|
ret.carName = "chrysler"
|
||||||
ret.dashcamOnly = candidate in RAM_HD
|
ret.dashcamOnly = candidate in RAM_HD
|
||||||
|
|
||||||
@@ -24,7 +27,6 @@ class CarInterface(CarInterfaceBase):
|
|||||||
elif candidate in RAM_DT:
|
elif candidate in RAM_DT:
|
||||||
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_CHRYSLER_RAM_DT
|
ret.safetyConfigs[0].safetyParam |= Panda.FLAG_CHRYSLER_RAM_DT
|
||||||
|
|
||||||
ret.minSteerSpeed = 3.8 # m/s
|
|
||||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning)
|
||||||
if candidate not in RAM_CARS:
|
if candidate not in RAM_CARS:
|
||||||
# Newer FW versions standard on the following platforms, or flashed by a dealer onto older platforms have a higher minimum steering speed.
|
# Newer FW versions standard on the following platforms, or flashed by a dealer onto older platforms have a higher minimum steering speed.
|
||||||
@@ -35,10 +37,6 @@ class CarInterface(CarInterfaceBase):
|
|||||||
|
|
||||||
# Chrysler
|
# Chrysler
|
||||||
if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.DODGE_DURANGO):
|
if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.DODGE_DURANGO):
|
||||||
ret.mass = 2242.
|
|
||||||
ret.wheelbase = 3.089
|
|
||||||
ret.steerRatio = 16.2 # Pacifica Hybrid 2017
|
|
||||||
|
|
||||||
ret.lateralTuning.init('pid')
|
ret.lateralTuning.init('pid')
|
||||||
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
|
ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]]
|
||||||
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]]
|
ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]]
|
||||||
@@ -46,9 +44,6 @@ class CarInterface(CarInterfaceBase):
|
|||||||
|
|
||||||
# Jeep
|
# Jeep
|
||||||
elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
|
elif candidate in (CAR.JEEP_GRAND_CHEROKEE, CAR.JEEP_GRAND_CHEROKEE_2019):
|
||||||
ret.mass = 1778
|
|
||||||
ret.wheelbase = 2.71
|
|
||||||
ret.steerRatio = 16.7
|
|
||||||
ret.steerActuatorDelay = 0.2
|
ret.steerActuatorDelay = 0.2
|
||||||
|
|
||||||
ret.lateralTuning.init('pid')
|
ret.lateralTuning.init('pid')
|
||||||
@@ -60,19 +55,12 @@ class CarInterface(CarInterfaceBase):
|
|||||||
elif candidate == CAR.RAM_1500:
|
elif candidate == CAR.RAM_1500:
|
||||||
ret.steerActuatorDelay = 0.2
|
ret.steerActuatorDelay = 0.2
|
||||||
ret.wheelbase = 3.88
|
ret.wheelbase = 3.88
|
||||||
ret.steerRatio = 16.3
|
|
||||||
ret.mass = 2493.
|
|
||||||
ret.minSteerSpeed = 14.5
|
|
||||||
# Older EPS FW allow steer to zero
|
# Older EPS FW allow steer to zero
|
||||||
if any(fw.ecu == 'eps' and b"68" < fw.fwVersion[:4] <= b"6831" for fw in car_fw):
|
if any(fw.ecu == 'eps' and b"68" < fw.fwVersion[:4] <= b"6831" for fw in car_fw):
|
||||||
ret.minSteerSpeed = 0.
|
ret.minSteerSpeed = 0.
|
||||||
|
|
||||||
elif candidate == CAR.RAM_HD:
|
elif candidate == CAR.RAM_HD:
|
||||||
ret.steerActuatorDelay = 0.2
|
ret.steerActuatorDelay = 0.2
|
||||||
ret.wheelbase = 3.785
|
|
||||||
ret.steerRatio = 15.61
|
|
||||||
ret.mass = 3405.
|
|
||||||
ret.minSteerSpeed = 16
|
|
||||||
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, 1.0, False)
|
CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, 1.0, False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -91,8 +79,13 @@ class CarInterface(CarInterfaceBase):
|
|||||||
def _update(self, c, frogpilot_variables):
|
def _update(self, c, frogpilot_variables):
|
||||||
ret = self.CS.update(self.cp, self.cp_cam, frogpilot_variables)
|
ret = self.CS.update(self.cp, self.cp_cam, frogpilot_variables)
|
||||||
|
|
||||||
|
ret.buttonEvents = [
|
||||||
|
*create_button_events(self.CS.distance_button, self.CS.prev_distance_button, {1: ButtonType.gapAdjustCruise}),
|
||||||
|
*create_button_events(self.CS.lkas_enabled, self.CS.lkas_previously_enabled, {1: FrogPilotButtonType.lkas}),
|
||||||
|
]
|
||||||
|
|
||||||
# events
|
# events
|
||||||
events = self.create_common_events(ret, frogpilot_variables, extra_gears=[car.CarState.GearShifter.low])
|
events = self.create_common_events(ret, extra_gears=[car.CarState.GearShifter.low])
|
||||||
|
|
||||||
# Low speed steer alert hysteresis logic
|
# Low speed steer alert hysteresis logic
|
||||||
if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 0.5):
|
if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 0.5):
|
||||||
|
|||||||
@@ -1,38 +1,102 @@
|
|||||||
from enum import IntFlag, StrEnum
|
from enum import IntFlag
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List, Optional, Union
|
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from panda.python import uds
|
from panda.python import uds
|
||||||
from openpilot.selfdrive.car import dbc_dict
|
from openpilot.selfdrive.car import CarSpecs, DbcDict, PlatformConfig, Platforms, dbc_dict
|
||||||
from openpilot.selfdrive.car.docs_definitions import CarHarness, CarInfo, CarParts
|
from openpilot.selfdrive.car.docs_definitions import CarHarness, CarDocs, CarParts
|
||||||
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16
|
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, p16
|
||||||
|
|
||||||
Ecu = car.CarParams.Ecu
|
Ecu = car.CarParams.Ecu
|
||||||
|
|
||||||
|
|
||||||
class ChryslerFlags(IntFlag):
|
class ChryslerFlags(IntFlag):
|
||||||
|
# Detected flags
|
||||||
HIGHER_MIN_STEERING_SPEED = 1
|
HIGHER_MIN_STEERING_SPEED = 1
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChryslerCarDocs(CarDocs):
|
||||||
|
package: str = "Adaptive Cruise Control (ACC)"
|
||||||
|
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca]))
|
||||||
|
|
||||||
class CAR(StrEnum):
|
|
||||||
|
@dataclass
|
||||||
|
class ChryslerPlatformConfig(PlatformConfig):
|
||||||
|
dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ChryslerCarSpecs(CarSpecs):
|
||||||
|
minSteerSpeed: float = 3.8 # m/s
|
||||||
|
|
||||||
|
|
||||||
|
class CAR(Platforms):
|
||||||
# Chrysler
|
# Chrysler
|
||||||
PACIFICA_2017_HYBRID = "CHRYSLER PACIFICA HYBRID 2017"
|
PACIFICA_2017_HYBRID = ChryslerPlatformConfig(
|
||||||
PACIFICA_2018_HYBRID = "CHRYSLER PACIFICA HYBRID 2018"
|
"CHRYSLER PACIFICA HYBRID 2017",
|
||||||
PACIFICA_2019_HYBRID = "CHRYSLER PACIFICA HYBRID 2019"
|
[ChryslerCarDocs("Chrysler Pacifica Hybrid 2017")],
|
||||||
PACIFICA_2018 = "CHRYSLER PACIFICA 2018"
|
ChryslerCarSpecs(mass=2242., wheelbase=3.089, steerRatio=16.2),
|
||||||
PACIFICA_2020 = "CHRYSLER PACIFICA 2020"
|
)
|
||||||
|
PACIFICA_2018_HYBRID = ChryslerPlatformConfig(
|
||||||
|
"CHRYSLER PACIFICA HYBRID 2018",
|
||||||
|
[ChryslerCarDocs("Chrysler Pacifica Hybrid 2018")],
|
||||||
|
PACIFICA_2017_HYBRID.specs,
|
||||||
|
)
|
||||||
|
PACIFICA_2019_HYBRID = ChryslerPlatformConfig(
|
||||||
|
"CHRYSLER PACIFICA HYBRID 2019",
|
||||||
|
[ChryslerCarDocs("Chrysler Pacifica Hybrid 2019-23")],
|
||||||
|
PACIFICA_2017_HYBRID.specs,
|
||||||
|
)
|
||||||
|
PACIFICA_2018 = ChryslerPlatformConfig(
|
||||||
|
"CHRYSLER PACIFICA 2018",
|
||||||
|
[ChryslerCarDocs("Chrysler Pacifica 2017-18")],
|
||||||
|
PACIFICA_2017_HYBRID.specs,
|
||||||
|
)
|
||||||
|
PACIFICA_2020 = ChryslerPlatformConfig(
|
||||||
|
"CHRYSLER PACIFICA 2020",
|
||||||
|
[
|
||||||
|
ChryslerCarDocs("Chrysler Pacifica 2019-20"),
|
||||||
|
ChryslerCarDocs("Chrysler Pacifica 2021-23", package="All"),
|
||||||
|
],
|
||||||
|
PACIFICA_2017_HYBRID.specs,
|
||||||
|
)
|
||||||
|
|
||||||
# Dodge
|
# Dodge
|
||||||
DODGE_DURANGO = "DODGE DURANGO 2021"
|
DODGE_DURANGO = ChryslerPlatformConfig(
|
||||||
|
"DODGE DURANGO 2021",
|
||||||
|
[ChryslerCarDocs("Dodge Durango 2020-21")],
|
||||||
|
PACIFICA_2017_HYBRID.specs,
|
||||||
|
)
|
||||||
|
|
||||||
# Jeep
|
# Jeep
|
||||||
JEEP_GRAND_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk
|
JEEP_GRAND_CHEROKEE = ChryslerPlatformConfig( # includes 2017 Trailhawk
|
||||||
JEEP_GRAND_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk
|
"JEEP GRAND CHEROKEE V6 2018",
|
||||||
|
[ChryslerCarDocs("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk")],
|
||||||
|
ChryslerCarSpecs(mass=1778., wheelbase=2.71, steerRatio=16.7),
|
||||||
|
)
|
||||||
|
|
||||||
|
JEEP_GRAND_CHEROKEE_2019 = ChryslerPlatformConfig( # includes 2020 Trailhawk
|
||||||
|
"JEEP GRAND CHEROKEE 2019",
|
||||||
|
[ChryslerCarDocs("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4")],
|
||||||
|
JEEP_GRAND_CHEROKEE.specs,
|
||||||
|
)
|
||||||
|
|
||||||
# Ram
|
# Ram
|
||||||
RAM_1500 = "RAM 1500 5TH GEN"
|
RAM_1500 = ChryslerPlatformConfig(
|
||||||
RAM_HD = "RAM HD 5TH GEN"
|
"RAM 1500 5TH GEN",
|
||||||
|
[ChryslerCarDocs("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram]))],
|
||||||
|
ChryslerCarSpecs(mass=2493., wheelbase=3.88, steerRatio=16.3, minSteerSpeed=14.5),
|
||||||
|
dbc_dict('chrysler_ram_dt_generated', None),
|
||||||
|
)
|
||||||
|
RAM_HD = ChryslerPlatformConfig(
|
||||||
|
"RAM HD 5TH GEN",
|
||||||
|
[
|
||||||
|
ChryslerCarDocs("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])),
|
||||||
|
ChryslerCarDocs("Ram 3500 2019-22", car_parts=CarParts.common([CarHarness.ram])),
|
||||||
|
],
|
||||||
|
ChryslerCarSpecs(mass=3405., wheelbase=3.785, steerRatio=15.61, minSteerSpeed=16.),
|
||||||
|
dbc_dict('chrysler_ram_hd_generated', None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CarControllerParams:
|
class CarControllerParams:
|
||||||
@@ -60,32 +124,6 @@ RAM_HD = {CAR.RAM_HD, }
|
|||||||
RAM_CARS = RAM_DT | RAM_HD
|
RAM_CARS = RAM_DT | RAM_HD
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ChryslerCarInfo(CarInfo):
|
|
||||||
package: str = "Adaptive Cruise Control (ACC)"
|
|
||||||
car_parts: CarParts = field(default_factory=CarParts.common([CarHarness.fca]))
|
|
||||||
|
|
||||||
|
|
||||||
CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = {
|
|
||||||
CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017"),
|
|
||||||
CAR.PACIFICA_2018_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2018"),
|
|
||||||
CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-23"),
|
|
||||||
CAR.PACIFICA_2018: ChryslerCarInfo("Chrysler Pacifica 2017-18"),
|
|
||||||
CAR.PACIFICA_2020: [
|
|
||||||
ChryslerCarInfo("Chrysler Pacifica 2019-20"),
|
|
||||||
ChryslerCarInfo("Chrysler Pacifica 2021-23", package="All"),
|
|
||||||
],
|
|
||||||
CAR.JEEP_GRAND_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"),
|
|
||||||
CAR.JEEP_GRAND_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"),
|
|
||||||
CAR.DODGE_DURANGO: ChryslerCarInfo("Dodge Durango 2020-21"),
|
|
||||||
CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-24", car_parts=CarParts.common([CarHarness.ram])),
|
|
||||||
CAR.RAM_HD: [
|
|
||||||
ChryslerCarInfo("Ram 2500 2020-24", car_parts=CarParts.common([CarHarness.ram])),
|
|
||||||
ChryslerCarInfo("Ram 3500 2019-22", car_parts=CarParts.common([CarHarness.ram])),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CHRYSLER_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
|
CHRYSLER_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
|
||||||
p16(0xf132)
|
p16(0xf132)
|
||||||
CHRYSLER_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
CHRYSLER_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
||||||
@@ -125,16 +163,4 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DBC = CAR.create_dbc_map()
|
||||||
DBC = {
|
|
||||||
CAR.PACIFICA_2017_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.PACIFICA_2018: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.DODGE_DURANGO: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.JEEP_GRAND_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.JEEP_GRAND_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'),
|
|
||||||
CAR.RAM_1500: dbc_dict('chrysler_ram_dt_generated', None),
|
|
||||||
CAR.RAM_HD: dbc_dict('chrysler_ram_hd_generated', None),
|
|
||||||
}
|
|
||||||
|
|||||||
80
selfdrive/car/docs.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
|
import jinja2
|
||||||
|
import os
|
||||||
|
from enum import Enum
|
||||||
|
from natsort import natsorted
|
||||||
|
|
||||||
|
from cereal import car
|
||||||
|
from openpilot.common.basedir import BASEDIR
|
||||||
|
from openpilot.selfdrive.car import gen_empty_fingerprint
|
||||||
|
from openpilot.selfdrive.car.docs_definitions import CarDocs, Column, CommonFootnote, PartType
|
||||||
|
from openpilot.selfdrive.car.car_helpers import interfaces, get_interface_attr
|
||||||
|
from openpilot.selfdrive.car.values import PLATFORMS
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_footnotes() -> dict[Enum, int]:
|
||||||
|
all_footnotes = list(CommonFootnote)
|
||||||
|
for footnotes in get_interface_attr("Footnote", ignore_none=True).values():
|
||||||
|
all_footnotes.extend(footnotes)
|
||||||
|
return {fn: idx + 1 for idx, fn in enumerate(all_footnotes)}
|
||||||
|
|
||||||
|
|
||||||
|
CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md")
|
||||||
|
CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md")
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_car_docs() -> list[CarDocs]:
|
||||||
|
all_car_docs: list[CarDocs] = []
|
||||||
|
footnotes = get_all_footnotes()
|
||||||
|
for model, platform in PLATFORMS.items():
|
||||||
|
car_docs = platform.config.car_docs
|
||||||
|
# If available, uses experimental longitudinal limits for the docs
|
||||||
|
CP = interfaces[model][0].get_params(platform, fingerprint=gen_empty_fingerprint(),
|
||||||
|
car_fw=[car.CarParams.CarFw(ecu="unknown")], experimental_long=True, docs=True)
|
||||||
|
|
||||||
|
if CP.dashcamOnly or not len(car_docs):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# A platform can include multiple car models
|
||||||
|
for _car_docs in car_docs:
|
||||||
|
if not hasattr(_car_docs, "row"):
|
||||||
|
_car_docs.init_make(CP)
|
||||||
|
_car_docs.init(CP, footnotes)
|
||||||
|
all_car_docs.append(_car_docs)
|
||||||
|
|
||||||
|
# Sort cars by make and model + year
|
||||||
|
sorted_cars: list[CarDocs] = natsorted(all_car_docs, key=lambda car: car.name.lower())
|
||||||
|
return sorted_cars
|
||||||
|
|
||||||
|
|
||||||
|
def group_by_make(all_car_docs: list[CarDocs]) -> dict[str, list[CarDocs]]:
|
||||||
|
sorted_car_docs = defaultdict(list)
|
||||||
|
for car_docs in all_car_docs:
|
||||||
|
sorted_car_docs[car_docs.make].append(car_docs)
|
||||||
|
return dict(sorted_car_docs)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cars_md(all_car_docs: list[CarDocs], template_fn: str) -> str:
|
||||||
|
with open(template_fn) as f:
|
||||||
|
template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True)
|
||||||
|
|
||||||
|
footnotes = [fn.value.text for fn in get_all_footnotes()]
|
||||||
|
cars_md: str = template.render(all_car_docs=all_car_docs, PartType=PartType,
|
||||||
|
group_by_make=group_by_make, footnotes=footnotes,
|
||||||
|
Column=Column)
|
||||||
|
return cars_md
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Auto generates supported cars documentation",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
|
||||||
|
parser.add_argument("--template", default=CARS_MD_TEMPLATE, help="Override default template filename")
|
||||||
|
parser.add_argument("--out", default=CARS_MD_OUT, help="Override default generated filename")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.out, 'w') as f:
|
||||||
|
f.write(generate_cars_md(get_all_car_docs(), args.template))
|
||||||
|
print(f"Generated and written to {args.out}")
|
||||||
@@ -3,7 +3,6 @@ from collections import namedtuple
|
|||||||
import copy
|
import copy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
@@ -35,7 +34,7 @@ class Star(Enum):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class BasePart:
|
class BasePart:
|
||||||
name: str
|
name: str
|
||||||
parts: List[Enum] = field(default_factory=list)
|
parts: list[Enum] = field(default_factory=list)
|
||||||
|
|
||||||
def all_parts(self):
|
def all_parts(self):
|
||||||
# Recursively get all parts
|
# Recursively get all parts
|
||||||
@@ -76,7 +75,7 @@ class Accessory(EnumBase):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BaseCarHarness(BasePart):
|
class BaseCarHarness(BasePart):
|
||||||
parts: List[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft])
|
parts: list[Enum] = field(default_factory=lambda: [Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft])
|
||||||
has_connector: bool = True # without are hidden on the harness connector page
|
has_connector: bool = True # without are hidden on the harness connector page
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +118,8 @@ class CarHarness(EnumBase):
|
|||||||
nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
|
nissan_b = BaseCarHarness("Nissan B connector", parts=[Accessory.harness_box, Cable.rj45_cable_7ft, Cable.long_obdc_cable, Cable.usbc_coupler])
|
||||||
mazda = BaseCarHarness("Mazda connector")
|
mazda = BaseCarHarness("Mazda connector")
|
||||||
ford_q3 = BaseCarHarness("Ford Q3 connector")
|
ford_q3 = BaseCarHarness("Ford Q3 connector")
|
||||||
ford_q4 = BaseCarHarness("Ford Q4 connector")
|
ford_q4 = BaseCarHarness("Ford Q4 connector", parts=[Accessory.harness_box, Accessory.comma_power_v2, Cable.rj45_cable_7ft, Cable.long_obdc_cable,
|
||||||
|
Cable.usbc_coupler])
|
||||||
|
|
||||||
|
|
||||||
class Device(EnumBase):
|
class Device(EnumBase):
|
||||||
@@ -149,18 +149,18 @@ class PartType(Enum):
|
|||||||
tool = Tool
|
tool = Tool
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CAR_PARTS: List[EnumBase] = [Device.threex]
|
DEFAULT_CAR_PARTS: list[EnumBase] = [Device.threex]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CarParts:
|
class CarParts:
|
||||||
parts: List[EnumBase] = field(default_factory=list)
|
parts: list[EnumBase] = field(default_factory=list)
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
return copy.deepcopy(self)
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def common(cls, add: Optional[List[EnumBase]] = None, remove: Optional[List[EnumBase]] = None):
|
def common(cls, add: list[EnumBase] = None, remove: list[EnumBase] = None):
|
||||||
p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])]
|
p = [part for part in (add or []) + DEFAULT_CAR_PARTS if part not in (remove or [])]
|
||||||
return cls(p)
|
return cls(p)
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ class CommonFootnote(Enum):
|
|||||||
Column.LONGITUDINAL)
|
Column.LONGITUDINAL)
|
||||||
|
|
||||||
|
|
||||||
def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]:
|
def get_footnotes(footnotes: list[Enum], column: Column) -> list[Enum]:
|
||||||
# Returns applicable footnotes given current column
|
# Returns applicable footnotes given current column
|
||||||
return [fn for fn in footnotes if fn.value.column == column]
|
return [fn for fn in footnotes if fn.value.column == column]
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ def get_year_list(years):
|
|||||||
return years_list
|
return years_list
|
||||||
|
|
||||||
|
|
||||||
def split_name(name: str) -> Tuple[str, str, str]:
|
def split_name(name: str) -> tuple[str, str, str]:
|
||||||
make, model = name.split(" ", 1)
|
make, model = name.split(" ", 1)
|
||||||
years = ""
|
years = ""
|
||||||
match = re.search(MODEL_YEARS_RE, model)
|
match = re.search(MODEL_YEARS_RE, model)
|
||||||
@@ -220,7 +220,7 @@ def split_name(name: str) -> Tuple[str, str, str]:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CarInfo:
|
class CarDocs:
|
||||||
# make + model + model years
|
# make + model + model years
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
@@ -233,13 +233,13 @@ class CarInfo:
|
|||||||
|
|
||||||
# the minimum compatibility requirements for this model, regardless
|
# the minimum compatibility requirements for this model, regardless
|
||||||
# of market. can be a package, trim, or list of features
|
# of market. can be a package, trim, or list of features
|
||||||
requirements: Optional[str] = None
|
requirements: str | None = None
|
||||||
|
|
||||||
video_link: Optional[str] = None
|
video_link: str | None = None
|
||||||
footnotes: List[Enum] = field(default_factory=list)
|
footnotes: list[Enum] = field(default_factory=list)
|
||||||
min_steer_speed: Optional[float] = None
|
min_steer_speed: float | None = None
|
||||||
min_enable_speed: Optional[float] = None
|
min_enable_speed: float | None = None
|
||||||
auto_resume: Optional[bool] = None
|
auto_resume: bool | None = None
|
||||||
|
|
||||||
# all the parts needed for the supported car
|
# all the parts needed for the supported car
|
||||||
car_parts: CarParts = field(default_factory=CarParts)
|
car_parts: CarParts = field(default_factory=CarParts)
|
||||||
@@ -248,7 +248,7 @@ class CarInfo:
|
|||||||
self.make, self.model, self.years = split_name(self.name)
|
self.make, self.model, self.years = split_name(self.name)
|
||||||
self.year_list = get_year_list(self.years)
|
self.year_list = get_year_list(self.years)
|
||||||
|
|
||||||
def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]):
|
def init(self, CP: car.CarParams, all_footnotes: dict[Enum, int]):
|
||||||
self.car_name = CP.carName
|
self.car_name = CP.carName
|
||||||
self.car_fingerprint = CP.carFingerprint
|
self.car_fingerprint = CP.carFingerprint
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ class CarInfo:
|
|||||||
# min steer & enable speed columns
|
# min steer & enable speed columns
|
||||||
# TODO: set all the min steer speeds in carParams and remove this
|
# TODO: set all the min steer speeds in carParams and remove this
|
||||||
if self.min_steer_speed is not None:
|
if self.min_steer_speed is not None:
|
||||||
assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarInfo and CarParams"
|
assert CP.minSteerSpeed == 0, f"{CP.carFingerprint}: Minimum steer speed set in both CarDocs and CarParams"
|
||||||
else:
|
else:
|
||||||
self.min_steer_speed = CP.minSteerSpeed
|
self.min_steer_speed = CP.minSteerSpeed
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ class CarInfo:
|
|||||||
if len(tools_docs):
|
if len(tools_docs):
|
||||||
hardware_col += f'<details><summary>Tools</summary><sub>{display_func(tools_docs)}</sub></details>'
|
hardware_col += f'<details><summary>Tools</summary><sub>{display_func(tools_docs)}</sub></details>'
|
||||||
|
|
||||||
self.row: Dict[Enum, Union[str, Star]] = {
|
self.row: dict[Enum, str | Star] = {
|
||||||
Column.MAKE: self.make,
|
Column.MAKE: self.make,
|
||||||
Column.MODEL: self.model,
|
Column.MODEL: self.model,
|
||||||
Column.PACKAGE: self.package,
|
Column.PACKAGE: self.package,
|
||||||
@@ -317,7 +317,7 @@ class CarInfo:
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def init_make(self, CP: car.CarParams):
|
def init_make(self, CP: car.CarParams):
|
||||||
"""CarInfo subclasses can add make-specific logic for harness selection, footnotes, etc."""
|
"""CarDocs subclasses can add make-specific logic for harness selection, footnotes, etc."""
|
||||||
|
|
||||||
def get_detail_sentence(self, CP):
|
def get_detail_sentence(self, CP):
|
||||||
if not CP.notCar:
|
if not CP.notCar:
|
||||||
@@ -352,7 +352,7 @@ class CarInfo:
|
|||||||
raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}")
|
raise Exception(f"This notCar does not have a detail sentence: {CP.carFingerprint}")
|
||||||
|
|
||||||
def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str:
|
def get_column(self, column: Column, star_icon: str, video_icon: str, footnote_tag: str) -> str:
|
||||||
item: Union[str, Star] = self.row[column]
|
item: str | Star = self.row[column]
|
||||||
if isinstance(item, Star):
|
if isinstance(item, Star):
|
||||||
item = star_icon.format(item.value)
|
item = star_icon.format(item.value)
|
||||||
elif column == Column.MODEL and len(self.years):
|
elif column == Column.MODEL and len(self.years):
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import capnp
|
import capnp
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Set
|
|
||||||
|
|
||||||
import cereal.messaging as messaging
|
import cereal.messaging as messaging
|
||||||
from panda.python.uds import SERVICE_TYPE
|
from panda.python.uds import SERVICE_TYPE
|
||||||
@@ -20,7 +19,7 @@ def make_tester_present_msg(addr, bus, subaddr=None):
|
|||||||
return make_can_msg(addr, bytes(dat), bus)
|
return make_can_msg(addr, bytes(dat), bus)
|
||||||
|
|
||||||
|
|
||||||
def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: Optional[int] = None) -> bool:
|
def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: int = None) -> bool:
|
||||||
# ISO-TP messages are always padded to 8 bytes
|
# ISO-TP messages are always padded to 8 bytes
|
||||||
# tester present response is always a single frame
|
# tester present response is always a single frame
|
||||||
dat_offset = 1 if subaddr is not None else 0
|
dat_offset = 1 if subaddr is not None else 0
|
||||||
@@ -34,16 +33,16 @@ def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subadd
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[EcuAddrBusType]:
|
def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> set[EcuAddrBusType]:
|
||||||
addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)]
|
addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)]
|
||||||
queries: Set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list}
|
queries: set[EcuAddrBusType] = {(addr, None, bus) for addr in addr_list}
|
||||||
responses = queries
|
responses = queries
|
||||||
return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug)
|
return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug)
|
||||||
|
|
||||||
|
|
||||||
def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[EcuAddrBusType],
|
def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: set[EcuAddrBusType],
|
||||||
responses: Set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> Set[EcuAddrBusType]:
|
responses: set[EcuAddrBusType], timeout: float = 1, debug: bool = False) -> set[EcuAddrBusType]:
|
||||||
ecu_responses: Set[EcuAddrBusType] = set() # set((addr, subaddr, bus),)
|
ecu_responses: set[EcuAddrBusType] = set() # set((addr, subaddr, bus),)
|
||||||
try:
|
try:
|
||||||
msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries]
|
msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from openpilot.selfdrive.car.interfaces import get_interface_attr
|
from openpilot.selfdrive.car.interfaces import get_interface_attr
|
||||||
|
from openpilot.selfdrive.car.honda.values import CAR as HONDA
|
||||||
|
from openpilot.selfdrive.car.hyundai.values import CAR as HYUNDAI
|
||||||
|
from openpilot.selfdrive.car.toyota.values import CAR as TOYOTA
|
||||||
|
from openpilot.selfdrive.car.volkswagen.values import CAR as VW
|
||||||
|
|
||||||
FW_VERSIONS = get_interface_attr('FW_VERSIONS', combine_brands=True, ignore_none=True)
|
FW_VERSIONS = get_interface_attr('FW_VERSIONS', combine_brands=True, ignore_none=True)
|
||||||
_FINGERPRINTS = get_interface_attr('FINGERPRINTS', combine_brands=True, ignore_none=True)
|
_FINGERPRINTS = get_interface_attr('FINGERPRINTS', combine_brands=True, ignore_none=True)
|
||||||
@@ -44,3 +48,73 @@ def all_known_cars():
|
|||||||
def all_legacy_fingerprint_cars():
|
def all_legacy_fingerprint_cars():
|
||||||
"""Returns a list of all known car strings, FPv1 only."""
|
"""Returns a list of all known car strings, FPv1 only."""
|
||||||
return list(_FINGERPRINTS.keys())
|
return list(_FINGERPRINTS.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# A dict that maps old platform strings to their latest representations
|
||||||
|
MIGRATION = {
|
||||||
|
"ACURA ILX 2016 ACURAWATCH PLUS": HONDA.ACURA_ILX,
|
||||||
|
"ACURA RDX 2018 ACURAWATCH PLUS": HONDA.ACURA_RDX,
|
||||||
|
"ACURA RDX 2020 TECH": HONDA.ACURA_RDX_3G,
|
||||||
|
"AUDI A3": VW.AUDI_A3_MK3,
|
||||||
|
"HONDA ACCORD 2018 HYBRID TOURING": HONDA.ACCORD,
|
||||||
|
"HONDA ACCORD 1.5T 2018": HONDA.ACCORD,
|
||||||
|
"HONDA ACCORD 2018 LX 1.5T": HONDA.ACCORD,
|
||||||
|
"HONDA ACCORD 2018 SPORT 2T": HONDA.ACCORD,
|
||||||
|
"HONDA ACCORD 2T 2018": HONDA.ACCORD,
|
||||||
|
"HONDA ACCORD HYBRID 2018": HONDA.ACCORD,
|
||||||
|
"HONDA CIVIC 2016 TOURING": HONDA.CIVIC,
|
||||||
|
"HONDA CIVIC HATCHBACK 2017 SEDAN/COUPE 2019": HONDA.CIVIC_BOSCH,
|
||||||
|
"HONDA CIVIC SEDAN 1.6 DIESEL": HONDA.CIVIC_BOSCH_DIESEL,
|
||||||
|
"HONDA CR-V 2016 EXECUTIVE": HONDA.CRV_EU,
|
||||||
|
"HONDA CR-V 2016 TOURING": HONDA.CRV,
|
||||||
|
"HONDA CR-V 2017 EX": HONDA.CRV_5G,
|
||||||
|
"HONDA CR-V 2019 HYBRID": HONDA.CRV_HYBRID,
|
||||||
|
"HONDA FIT 2018 EX": HONDA.FIT,
|
||||||
|
"HONDA HRV 2019 TOURING": HONDA.HRV,
|
||||||
|
"HONDA INSIGHT 2019 TOURING": HONDA.INSIGHT,
|
||||||
|
"HONDA ODYSSEY 2018 EX-L": HONDA.ODYSSEY,
|
||||||
|
"HONDA ODYSSEY 2019 EXCLUSIVE CHN": HONDA.ODYSSEY_CHN,
|
||||||
|
"HONDA PILOT 2017 TOURING": HONDA.PILOT,
|
||||||
|
"HONDA PILOT 2019 ELITE": HONDA.PILOT,
|
||||||
|
"HONDA PILOT 2019": HONDA.PILOT,
|
||||||
|
"HONDA PASSPORT 2021": HONDA.PILOT,
|
||||||
|
"HONDA RIDGELINE 2017 BLACK EDITION": HONDA.RIDGELINE,
|
||||||
|
"HYUNDAI ELANTRA LIMITED ULTIMATE 2017": HYUNDAI.ELANTRA,
|
||||||
|
"HYUNDAI SANTA FE LIMITED 2019": HYUNDAI.SANTA_FE,
|
||||||
|
"HYUNDAI TUCSON DIESEL 2019": HYUNDAI.TUCSON,
|
||||||
|
"KIA OPTIMA 2016": HYUNDAI.KIA_OPTIMA_G4,
|
||||||
|
"KIA OPTIMA 2019": HYUNDAI.KIA_OPTIMA_G4_FL,
|
||||||
|
"KIA OPTIMA SX 2019 & 2016": HYUNDAI.KIA_OPTIMA_G4_FL,
|
||||||
|
"LEXUS CT 200H 2018": TOYOTA.LEXUS_CTH,
|
||||||
|
"LEXUS ES 300H 2018": TOYOTA.LEXUS_ES,
|
||||||
|
"LEXUS ES 300H 2019": TOYOTA.LEXUS_ES_TSS2,
|
||||||
|
"LEXUS IS300 2018": TOYOTA.LEXUS_IS,
|
||||||
|
"LEXUS NX300 2018": TOYOTA.LEXUS_NX,
|
||||||
|
"LEXUS NX300H 2018": TOYOTA.LEXUS_NX,
|
||||||
|
"LEXUS RX 350 2016": TOYOTA.LEXUS_RX,
|
||||||
|
"LEXUS RX350 2020": TOYOTA.LEXUS_RX_TSS2,
|
||||||
|
"LEXUS RX450 HYBRID 2020": TOYOTA.LEXUS_RX_TSS2,
|
||||||
|
"TOYOTA SIENNA XLE 2018": TOYOTA.SIENNA,
|
||||||
|
"TOYOTA C-HR HYBRID 2018": TOYOTA.CHR,
|
||||||
|
"TOYOTA COROLLA HYBRID TSS2 2019": TOYOTA.COROLLA_TSS2,
|
||||||
|
"TOYOTA RAV4 HYBRID 2019": TOYOTA.RAV4_TSS2,
|
||||||
|
"LEXUS ES HYBRID 2019": TOYOTA.LEXUS_ES_TSS2,
|
||||||
|
"LEXUS NX HYBRID 2018": TOYOTA.LEXUS_NX,
|
||||||
|
"LEXUS NX HYBRID 2020": TOYOTA.LEXUS_NX_TSS2,
|
||||||
|
"LEXUS RX HYBRID 2020": TOYOTA.LEXUS_RX_TSS2,
|
||||||
|
"TOYOTA ALPHARD HYBRID 2021": TOYOTA.ALPHARD_TSS2,
|
||||||
|
"TOYOTA AVALON HYBRID 2019": TOYOTA.AVALON_2019,
|
||||||
|
"TOYOTA AVALON HYBRID 2022": TOYOTA.AVALON_TSS2,
|
||||||
|
"TOYOTA CAMRY HYBRID 2018": TOYOTA.CAMRY,
|
||||||
|
"TOYOTA CAMRY HYBRID 2021": TOYOTA.CAMRY_TSS2,
|
||||||
|
"TOYOTA C-HR HYBRID 2022": TOYOTA.CHR_TSS2,
|
||||||
|
"TOYOTA HIGHLANDER HYBRID 2020": TOYOTA.HIGHLANDER_TSS2,
|
||||||
|
"TOYOTA RAV4 HYBRID 2022": TOYOTA.RAV4_TSS2_2022,
|
||||||
|
"TOYOTA RAV4 HYBRID 2023": TOYOTA.RAV4_TSS2_2023,
|
||||||
|
"TOYOTA HIGHLANDER HYBRID 2018": TOYOTA.HIGHLANDER,
|
||||||
|
"LEXUS ES HYBRID 2018": TOYOTA.LEXUS_ES,
|
||||||
|
"LEXUS RX HYBRID 2017": TOYOTA.LEXUS_RX,
|
||||||
|
"HYUNDAI TUCSON HYBRID 4TH GEN": HYUNDAI.TUCSON_4TH_GEN,
|
||||||
|
"KIA SPORTAGE HYBRID 5TH GEN": HYUNDAI.KIA_SPORTAGE_5TH_GEN,
|
||||||
|
"KIA SORENTO PLUG-IN HYBRID 4TH GEN": HYUNDAI.KIA_SORENTO_HEV_4TH_GEN,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.numpy_fast import clip
|
|
||||||
from opendbc.can.packer import CANPacker
|
from opendbc.can.packer import CANPacker
|
||||||
|
from openpilot.common.numpy_fast import clip
|
||||||
from openpilot.selfdrive.car import apply_std_steer_angle_limits
|
from openpilot.selfdrive.car import apply_std_steer_angle_limits
|
||||||
from openpilot.selfdrive.car.ford import fordcan
|
from openpilot.selfdrive.car.ford import fordcan
|
||||||
from openpilot.selfdrive.car.ford.values import CANFD_CAR, CarControllerParams
|
from openpilot.selfdrive.car.ford.values import CarControllerParams, FordFlags
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarControllerBase
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
||||||
|
|
||||||
LongCtrlState = car.CarControl.Actuators.LongControlState
|
LongCtrlState = car.CarControl.Actuators.LongControlState
|
||||||
@@ -22,7 +23,7 @@ def apply_ford_curvature_limits(apply_curvature, apply_curvature_last, current_c
|
|||||||
return clip(apply_curvature, -CarControllerParams.CURVATURE_MAX, CarControllerParams.CURVATURE_MAX)
|
return clip(apply_curvature, -CarControllerParams.CURVATURE_MAX, CarControllerParams.CURVATURE_MAX)
|
||||||
|
|
||||||
|
|
||||||
class CarController:
|
class CarController(CarControllerBase):
|
||||||
def __init__(self, dbc_name, CP, VM):
|
def __init__(self, dbc_name, CP, VM):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
self.VM = VM
|
self.VM = VM
|
||||||
@@ -34,6 +35,7 @@ class CarController:
|
|||||||
self.main_on_last = False
|
self.main_on_last = False
|
||||||
self.lkas_enabled_last = False
|
self.lkas_enabled_last = False
|
||||||
self.steer_alert_last = False
|
self.steer_alert_last = False
|
||||||
|
self.lead_distance_bars_last = None
|
||||||
|
|
||||||
def update(self, CC, CS, now_nanos, frogpilot_variables):
|
def update(self, CC, CS, now_nanos, frogpilot_variables):
|
||||||
can_sends = []
|
can_sends = []
|
||||||
@@ -69,10 +71,10 @@ class CarController:
|
|||||||
|
|
||||||
self.apply_curvature_last = apply_curvature
|
self.apply_curvature_last = apply_curvature
|
||||||
|
|
||||||
if self.CP.carFingerprint in CANFD_CAR:
|
if self.CP.flags & FordFlags.CANFD:
|
||||||
# TODO: extended mode
|
# TODO: extended mode
|
||||||
mode = 1 if CC.latActive else 0
|
mode = 1 if CC.latActive else 0
|
||||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0xF
|
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||||
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -apply_curvature, 0., counter))
|
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -apply_curvature, 0., counter))
|
||||||
else:
|
else:
|
||||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -apply_curvature, 0.))
|
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -apply_curvature, 0.))
|
||||||
@@ -100,15 +102,19 @@ class CarController:
|
|||||||
# send lkas ui msg at 1Hz or if ui state changes
|
# send lkas ui msg at 1Hz or if ui state changes
|
||||||
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
|
if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui:
|
||||||
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
|
can_sends.append(fordcan.create_lkas_ui_msg(self.packer, self.CAN, main_on, CC.latActive, steer_alert, hud_control, CS.lkas_status_stock_values))
|
||||||
|
|
||||||
# send acc ui msg at 5Hz or if ui state changes
|
# send acc ui msg at 5Hz or if ui state changes
|
||||||
|
if hud_control.leadDistanceBars != self.lead_distance_bars_last:
|
||||||
|
send_ui = True
|
||||||
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
|
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
|
||||||
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
|
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, CC.latActive,
|
||||||
fcw_alert, CS.out.cruiseState.standstill, hud_control,
|
fcw_alert, CS.out.cruiseState.standstill, hud_control,
|
||||||
CS.acc_tja_status_stock_values))
|
CS.acc_tja_status_stock_values))
|
||||||
|
|
||||||
self.main_on_last = main_on
|
self.main_on_last = main_on
|
||||||
self.lkas_enabled_last = CC.latActive
|
self.lkas_enabled_last = CC.latActive
|
||||||
self.steer_alert_last = steer_alert
|
self.steer_alert_last = steer_alert
|
||||||
|
self.lead_distance_bars_last = hud_control.leadDistanceBars
|
||||||
|
|
||||||
new_actuators = actuators.copy()
|
new_actuators = actuators.copy()
|
||||||
new_actuators.curvature = self.apply_curvature_last
|
new_actuators.curvature = self.apply_curvature_last
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.conversions import Conversions as CV
|
|
||||||
from opendbc.can.can_define import CANDefine
|
from opendbc.can.can_define import CANDefine
|
||||||
from opendbc.can.parser import CANParser
|
from opendbc.can.parser import CANParser
|
||||||
from openpilot.selfdrive.car.interfaces import CarStateBase
|
from openpilot.common.conversions import Conversions as CV
|
||||||
from openpilot.selfdrive.car.ford.fordcan import CanBus
|
from openpilot.selfdrive.car.ford.fordcan import CanBus
|
||||||
from openpilot.selfdrive.car.ford.values import CANFD_CAR, CarControllerParams, DBC
|
from openpilot.selfdrive.car.ford.values import DBC, CarControllerParams, FordFlags
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarStateBase
|
||||||
|
|
||||||
GearShifter = car.CarState.GearShifter
|
GearShifter = car.CarState.GearShifter
|
||||||
TransmissionType = car.CarParams.TransmissionType
|
TransmissionType = car.CarParams.TransmissionType
|
||||||
@@ -18,17 +18,13 @@ class CarState(CarStateBase):
|
|||||||
self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"]
|
self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"]
|
||||||
|
|
||||||
self.vehicle_sensors_valid = False
|
self.vehicle_sensors_valid = False
|
||||||
self.unsupported_platform = False
|
|
||||||
|
self.prev_distance_button = 0
|
||||||
|
self.distance_button = 0
|
||||||
|
|
||||||
def update(self, cp, cp_cam, frogpilot_variables):
|
def update(self, cp, cp_cam, frogpilot_variables):
|
||||||
ret = car.CarState.new_message()
|
ret = car.CarState.new_message()
|
||||||
|
|
||||||
# Ford Q3 hybrid variants experience a bug where a message from the PCM sends invalid checksums,
|
|
||||||
# this must be root-caused before enabling support. Ford Q4 hybrids do not have this problem.
|
|
||||||
# TrnAin_Tq_Actl and its quality flag are only set on ICE platform variants
|
|
||||||
self.unsupported_platform = (cp.vl["VehicleOperatingModes"]["TrnAinTq_D_Qf"] == 0 and
|
|
||||||
self.CP.carFingerprint not in CANFD_CAR)
|
|
||||||
|
|
||||||
# Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement
|
# Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement
|
||||||
# The vehicle usually recovers out of this state within a minute of normal driving
|
# The vehicle usually recovers out of this state within a minute of normal driving
|
||||||
self.vehicle_sensors_valid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] == 3
|
self.vehicle_sensors_valid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] == 3
|
||||||
@@ -56,7 +52,7 @@ class CarState(CarStateBase):
|
|||||||
ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3)
|
ret.steerFaultPermanent = cp.vl["EPAS_INFO"]["EPAS_Failure"] in (2, 3)
|
||||||
ret.espDisabled = cp.vl["Cluster_Info1_FD1"]["DrvSlipCtlMde_D_Rq"] != 0 # 0 is default mode
|
ret.espDisabled = cp.vl["Cluster_Info1_FD1"]["DrvSlipCtlMde_D_Rq"] != 0 # 0 is default mode
|
||||||
|
|
||||||
if self.CP.carFingerprint in CANFD_CAR:
|
if self.CP.flags & FordFlags.CANFD:
|
||||||
# this signal is always 0 on non-CAN FD cars
|
# this signal is always 0 on non-CAN FD cars
|
||||||
ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3)
|
ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3)
|
||||||
|
|
||||||
@@ -90,6 +86,8 @@ class CarState(CarStateBase):
|
|||||||
ret.rightBlinker = cp.vl["Steering_Data_FD1"]["TurnLghtSwtch_D_Stat"] == 2
|
ret.rightBlinker = cp.vl["Steering_Data_FD1"]["TurnLghtSwtch_D_Stat"] == 2
|
||||||
# TODO: block this going to the camera otherwise it will enable stock TJA
|
# TODO: block this going to the camera otherwise it will enable stock TJA
|
||||||
ret.genericToggle = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
|
ret.genericToggle = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
|
||||||
|
self.prev_distance_button = self.distance_button
|
||||||
|
self.distance_button = cp.vl["Steering_Data_FD1"]["AccButtnGapTogglePress"]
|
||||||
|
|
||||||
# lock info
|
# lock info
|
||||||
ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"],
|
ret.doorOpen = any([cp.vl["BodyInfo_3_FD1"]["DrStatDrv_B_Actl"], cp.vl["BodyInfo_3_FD1"]["DrStatPsngr_B_Actl"],
|
||||||
@@ -98,7 +96,7 @@ class CarState(CarStateBase):
|
|||||||
|
|
||||||
# blindspot sensors
|
# blindspot sensors
|
||||||
if self.CP.enableBsm:
|
if self.CP.enableBsm:
|
||||||
cp_bsm = cp_cam if self.CP.carFingerprint in CANFD_CAR else cp
|
cp_bsm = cp_cam if self.CP.flags & FordFlags.CANFD else cp
|
||||||
ret.leftBlindspot = cp_bsm.vl["Side_Detect_L_Stat"]["SodDetctLeft_D_Stat"] != 0
|
ret.leftBlindspot = cp_bsm.vl["Side_Detect_L_Stat"]["SodDetctLeft_D_Stat"] != 0
|
||||||
ret.rightBlindspot = cp_bsm.vl["Side_Detect_R_Stat"]["SodDetctRight_D_Stat"] != 0
|
ret.rightBlindspot = cp_bsm.vl["Side_Detect_R_Stat"]["SodDetctRight_D_Stat"] != 0
|
||||||
|
|
||||||
@@ -108,6 +106,9 @@ class CarState(CarStateBase):
|
|||||||
self.acc_tja_status_stock_values = cp_cam.vl["ACCDATA_3"]
|
self.acc_tja_status_stock_values = cp_cam.vl["ACCDATA_3"]
|
||||||
self.lkas_status_stock_values = cp_cam.vl["IPMA_Data"]
|
self.lkas_status_stock_values = cp_cam.vl["IPMA_Data"]
|
||||||
|
|
||||||
|
self.lkas_previously_enabled = self.lkas_enabled
|
||||||
|
self.lkas_enabled = bool(cp.vl["Steering_Data_FD1"]["TjaButtnOnOffPress"])
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -129,7 +130,7 @@ class CarState(CarStateBase):
|
|||||||
("RCMStatusMessage2_FD1", 10),
|
("RCMStatusMessage2_FD1", 10),
|
||||||
]
|
]
|
||||||
|
|
||||||
if CP.carFingerprint in CANFD_CAR:
|
if CP.flags & FordFlags.CANFD:
|
||||||
messages += [
|
messages += [
|
||||||
("Lane_Assist_Data3_FD1", 33),
|
("Lane_Assist_Data3_FD1", 33),
|
||||||
]
|
]
|
||||||
@@ -144,7 +145,7 @@ class CarState(CarStateBase):
|
|||||||
("BCM_Lamp_Stat_FD1", 1),
|
("BCM_Lamp_Stat_FD1", 1),
|
||||||
]
|
]
|
||||||
|
|
||||||
if CP.enableBsm and CP.carFingerprint not in CANFD_CAR:
|
if CP.enableBsm and not (CP.flags & FordFlags.CANFD):
|
||||||
messages += [
|
messages += [
|
||||||
("Side_Detect_L_Stat", 5),
|
("Side_Detect_L_Stat", 5),
|
||||||
("Side_Detect_R_Stat", 5),
|
("Side_Detect_R_Stat", 5),
|
||||||
@@ -162,7 +163,7 @@ class CarState(CarStateBase):
|
|||||||
("IPMA_Data", 1),
|
("IPMA_Data", 1),
|
||||||
]
|
]
|
||||||
|
|
||||||
if CP.enableBsm and CP.carFingerprint in CANFD_CAR:
|
if CP.enableBsm and CP.flags & FordFlags.CANFD:
|
||||||
messages += [
|
messages += [
|
||||||
("Side_Detect_L_Stat", 5),
|
("Side_Detect_L_Stat", 5),
|
||||||
("Side_Detect_R_Stat", 5),
|
("Side_Detect_R_Stat", 5),
|
||||||
|
|||||||
@@ -8,16 +8,19 @@ FW_VERSIONS = {
|
|||||||
(Ecu.eps, 0x730, None): [
|
(Ecu.eps, 0x730, None): [
|
||||||
b'LX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'LX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
b'LX6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'LX6C-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
|
b'LX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
(Ecu.abs, 0x760, None): [
|
(Ecu.abs, 0x760, None): [
|
||||||
b'LX6C-2D053-RD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'LX6C-2D053-RD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
b'LX6C-2D053-RE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'LX6C-2D053-RE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
|
b'LX6C-2D053-RF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
(Ecu.fwdRadar, 0x764, None): [
|
(Ecu.fwdRadar, 0x764, None): [
|
||||||
b'LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
(Ecu.fwdCamera, 0x706, None): [
|
(Ecu.fwdCamera, 0x706, None): [
|
||||||
b'M1PT-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'M1PT-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
|
b'M1PT-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
CAR.ESCAPE_MK4: {
|
CAR.ESCAPE_MK4: {
|
||||||
@@ -82,6 +85,7 @@ FW_VERSIONS = {
|
|||||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
(Ecu.fwdCamera, 0x706, None): [
|
(Ecu.fwdCamera, 0x706, None): [
|
||||||
|
b'ML3T-14H102-ABR\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
b'PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -133,6 +137,7 @@ FW_VERSIONS = {
|
|||||||
b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'NZ6C-2D053-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'PZ6C-2D053-ED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
b'PZ6C-2D053-EE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'PZ6C-2D053-EE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
|
b'PZ6C-2D053-EF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
],
|
],
|
||||||
(Ecu.fwdRadar, 0x764, None): [
|
(Ecu.fwdRadar, 0x764, None): [
|
||||||
b'NZ6T-14D049-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
b'NZ6T-14D049-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ def create_acc_ui_msg(packer, CAN: CanBus, CP, main_on: bool, enabled: bool, fcw
|
|||||||
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
|
"AccFllwMde_B_Dsply": 1 if hud_control.leadVisible else 0, # Lead indicator
|
||||||
"AccStopMde_B_Dsply": 1 if standstill else 0,
|
"AccStopMde_B_Dsply": 1 if standstill else 0,
|
||||||
"AccWarn_D_Dsply": 0, # ACC warning
|
"AccWarn_D_Dsply": 0, # ACC warning
|
||||||
"AccTGap_D_Dsply": 4, # Fixed time gap in UI
|
"AccTGap_D_Dsply": hud_control.leadDistanceBars, # Time gap
|
||||||
})
|
})
|
||||||
|
|
||||||
# Forwards FCW alert from IPMA
|
# Forwards FCW alert from IPMA
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
from cereal import car
|
from cereal import car, custom
|
||||||
from panda import Panda
|
from panda import Panda
|
||||||
from openpilot.common.conversions import Conversions as CV
|
from openpilot.common.conversions import Conversions as CV
|
||||||
from openpilot.selfdrive.car import get_safety_config
|
from openpilot.selfdrive.car import create_button_events, get_safety_config
|
||||||
from openpilot.selfdrive.car.ford.fordcan import CanBus
|
from openpilot.selfdrive.car.ford.fordcan import CanBus
|
||||||
from openpilot.selfdrive.car.ford.values import CANFD_CAR, CAR, Ecu
|
from openpilot.selfdrive.car.ford.values import Ecu, FordFlags
|
||||||
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
from openpilot.selfdrive.car.interfaces import CarInterfaceBase
|
||||||
|
|
||||||
|
ButtonType = car.CarState.ButtonEvent.Type
|
||||||
TransmissionType = car.CarParams.TransmissionType
|
TransmissionType = car.CarParams.TransmissionType
|
||||||
GearShifter = car.CarState.GearShifter
|
GearShifter = car.CarState.GearShifter
|
||||||
|
FrogPilotButtonType = custom.FrogPilotCarState.ButtonEvent.Type
|
||||||
|
|
||||||
|
|
||||||
class CarInterface(CarInterfaceBase):
|
class CarInterface(CarInterfaceBase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_params(ret, params, candidate, fingerprint, car_fw, experimental_long, docs):
|
def _get_params(ret, params, candidate, fingerprint, car_fw, disable_openpilot_long, experimental_long, docs):
|
||||||
ret.carName = "ford"
|
ret.carName = "ford"
|
||||||
ret.dashcamOnly = False
|
ret.dashcamOnly = False
|
||||||
|
|
||||||
@@ -34,56 +36,11 @@ class CarInterface(CarInterfaceBase):
|
|||||||
ret.experimentalLongitudinalAvailable = True
|
ret.experimentalLongitudinalAvailable = True
|
||||||
if experimental_long:
|
if experimental_long:
|
||||||
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_LONG_CONTROL
|
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_LONG_CONTROL
|
||||||
ret.openpilotLongitudinalControl = True and not params.get_bool("DisableOpenpilotLongitudinal")
|
ret.openpilotLongitudinalControl = True
|
||||||
|
|
||||||
if candidate in CANFD_CAR:
|
if ret.flags & FordFlags.CANFD:
|
||||||
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_CANFD
|
ret.safetyConfigs[-1].safetyParam |= Panda.FLAG_FORD_CANFD
|
||||||
|
|
||||||
if candidate == CAR.BRONCO_SPORT_MK1:
|
|
||||||
ret.wheelbase = 2.67
|
|
||||||
ret.steerRatio = 17.7
|
|
||||||
ret.mass = 1625
|
|
||||||
|
|
||||||
elif candidate == CAR.ESCAPE_MK4:
|
|
||||||
ret.wheelbase = 2.71
|
|
||||||
ret.steerRatio = 16.7
|
|
||||||
ret.mass = 1750
|
|
||||||
|
|
||||||
elif candidate == CAR.EXPLORER_MK6:
|
|
||||||
ret.wheelbase = 3.025
|
|
||||||
ret.steerRatio = 16.8
|
|
||||||
ret.mass = 2050
|
|
||||||
|
|
||||||
elif candidate == CAR.F_150_MK14:
|
|
||||||
# required trim only on SuperCrew
|
|
||||||
ret.wheelbase = 3.69
|
|
||||||
ret.steerRatio = 17.0
|
|
||||||
ret.mass = 2000
|
|
||||||
|
|
||||||
elif candidate == CAR.F_150_LIGHTNING_MK1:
|
|
||||||
# required trim only on SuperCrew
|
|
||||||
ret.wheelbase = 3.70
|
|
||||||
ret.steerRatio = 16.9
|
|
||||||
ret.mass = 2948
|
|
||||||
|
|
||||||
elif candidate == CAR.MUSTANG_MACH_E_MK1:
|
|
||||||
ret.wheelbase = 2.984
|
|
||||||
ret.steerRatio = 17.0 # guess
|
|
||||||
ret.mass = 2200
|
|
||||||
|
|
||||||
elif candidate == CAR.FOCUS_MK4:
|
|
||||||
ret.wheelbase = 2.7
|
|
||||||
ret.steerRatio = 15.0
|
|
||||||
ret.mass = 1350
|
|
||||||
|
|
||||||
elif candidate == CAR.MAVERICK_MK1:
|
|
||||||
ret.wheelbase = 3.076
|
|
||||||
ret.steerRatio = 17.0
|
|
||||||
ret.mass = 1650
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported car: {candidate}")
|
|
||||||
|
|
||||||
# Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1
|
# Auto Transmission: 0x732 ECU or Gear_Shift_by_Wire_FD1
|
||||||
found_ecus = [fw.ecu for fw in car_fw]
|
found_ecus = [fw.ecu for fw in car_fw]
|
||||||
if Ecu.shiftByWire in found_ecus or 0x5A in fingerprint[CAN.main] or docs:
|
if Ecu.shiftByWire in found_ecus or 0x5A in fingerprint[CAN.main] or docs:
|
||||||
@@ -106,11 +63,14 @@ class CarInterface(CarInterfaceBase):
|
|||||||
def _update(self, c, frogpilot_variables):
|
def _update(self, c, frogpilot_variables):
|
||||||
ret = self.CS.update(self.cp, self.cp_cam, frogpilot_variables)
|
ret = self.CS.update(self.cp, self.cp_cam, frogpilot_variables)
|
||||||
|
|
||||||
events = self.create_common_events(ret, frogpilot_variables, extra_gears=[GearShifter.manumatic])
|
ret.buttonEvents = [
|
||||||
|
*create_button_events(self.CS.distance_button, self.CS.prev_distance_button, {1: ButtonType.gapAdjustCruise}),
|
||||||
|
*create_button_events(self.CS.lkas_enabled, self.CS.lkas_previously_enabled, {1: FrogPilotButtonType.lkas}),
|
||||||
|
]
|
||||||
|
|
||||||
|
events = self.create_common_events(ret, extra_gears=[GearShifter.manumatic])
|
||||||
if not self.CS.vehicle_sensors_valid:
|
if not self.CS.vehicle_sensors_valid:
|
||||||
events.add(car.CarEvent.EventName.vehicleSensorsInvalid)
|
events.add(car.CarEvent.EventName.vehicleSensorsInvalid)
|
||||||
if self.CS.unsupported_platform:
|
|
||||||
events.add(car.CarEvent.EventName.startupNoControl)
|
|
||||||
|
|
||||||
ret.events = events.to_msg()
|
ret.events = events.to_msg()
|
||||||
|
|
||||||
|
|||||||
0
selfdrive/car/ford/tests/__init__.py
Normal file
82
selfdrive/car/ford/tests/test_ford.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import unittest
|
||||||
|
from parameterized import parameterized
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
import capnp
|
||||||
|
|
||||||
|
from cereal import car
|
||||||
|
from openpilot.selfdrive.car.ford.values import FW_QUERY_CONFIG
|
||||||
|
from openpilot.selfdrive.car.ford.fingerprints import FW_VERSIONS
|
||||||
|
|
||||||
|
Ecu = car.CarParams.Ecu
|
||||||
|
|
||||||
|
|
||||||
|
ECU_ADDRESSES = {
|
||||||
|
Ecu.eps: 0x730, # Power Steering Control Module (PSCM)
|
||||||
|
Ecu.abs: 0x760, # Anti-Lock Brake System (ABS)
|
||||||
|
Ecu.fwdRadar: 0x764, # Cruise Control Module (CCM)
|
||||||
|
Ecu.fwdCamera: 0x706, # Image Processing Module A (IPMA)
|
||||||
|
Ecu.engine: 0x7E0, # Powertrain Control Module (PCM)
|
||||||
|
Ecu.shiftByWire: 0x732, # Gear Shift Module (GSM)
|
||||||
|
Ecu.debug: 0x7D0, # Accessory Protocol Interface Module (APIM)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ECU_FW_CORE = {
|
||||||
|
Ecu.eps: [
|
||||||
|
b"14D003",
|
||||||
|
],
|
||||||
|
Ecu.abs: [
|
||||||
|
b"2D053",
|
||||||
|
],
|
||||||
|
Ecu.fwdRadar: [
|
||||||
|
b"14D049",
|
||||||
|
],
|
||||||
|
Ecu.fwdCamera: [
|
||||||
|
b"14F397", # Ford Q3
|
||||||
|
b"14H102", # Ford Q4
|
||||||
|
],
|
||||||
|
Ecu.engine: [
|
||||||
|
b"14C204",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestFordFW(unittest.TestCase):
|
||||||
|
def test_fw_query_config(self):
|
||||||
|
for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
|
||||||
|
self.assertIn(ecu, ECU_ADDRESSES, "Unknown ECU")
|
||||||
|
self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")
|
||||||
|
self.assertIsNone(subaddr, "Unexpected ECU subaddress")
|
||||||
|
|
||||||
|
@parameterized.expand(FW_VERSIONS.items())
|
||||||
|
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]):
|
||||||
|
for (ecu, addr, subaddr), fws in fw_versions.items():
|
||||||
|
self.assertIn(ecu, ECU_FW_CORE, "Unexpected ECU")
|
||||||
|
self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch")
|
||||||
|
self.assertIsNone(subaddr, "Unexpected ECU subaddress")
|
||||||
|
|
||||||
|
# Software part number takes the form: PREFIX-CORE-SUFFIX
|
||||||
|
# Prefix changes based on the family of part. It includes the model year
|
||||||
|
# and likely the platform.
|
||||||
|
# Core identifies the type of the item (e.g. 14D003 = PSCM, 14C204 = PCM).
|
||||||
|
# Suffix specifies the version of the part. -AA would be followed by -AB.
|
||||||
|
# Small increments in the suffix are usually compatible.
|
||||||
|
# Details: https://forscan.org/forum/viewtopic.php?p=70008#p70008
|
||||||
|
for fw in fws:
|
||||||
|
self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes")
|
||||||
|
|
||||||
|
# TODO: parse with regex, don't need detailed error message
|
||||||
|
fw_parts = fw.rstrip(b'\x00').split(b'-')
|
||||||
|
self.assertEqual(len(fw_parts), 3, "Expected FW to be in format: prefix-core-suffix")
|
||||||
|
|
||||||
|
prefix, core, suffix = fw_parts
|
||||||
|
self.assertEqual(len(prefix), 4, "Expected FW prefix to be 4 characters")
|
||||||
|
self.assertIn(len(core), (5, 6), "Expected FW core to be 5-6 characters")
|
||||||
|
self.assertIn(core, ECU_FW_CORE[ecu], f"Unexpected FW core for {ecu}")
|
||||||
|
self.assertIn(len(suffix), (2, 3), "Expected FW suffix to be 2-3 characters")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
from collections import defaultdict
|
import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field, replace
|
||||||
from enum import Enum, StrEnum
|
from enum import Enum, IntFlag
|
||||||
from typing import Dict, List, Union
|
|
||||||
|
|
||||||
|
import panda.python.uds as uds
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.selfdrive.car import AngleRateLimit, dbc_dict
|
from openpilot.selfdrive.car import AngleRateLimit, CarSpecs, dbc_dict, DbcDict, PlatformConfig, Platforms
|
||||||
from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarInfo, CarParts, Column, \
|
from openpilot.selfdrive.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \
|
||||||
Device
|
Device
|
||||||
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries
|
from openpilot.selfdrive.car.fw_query_definitions import FwQueryConfig, Request, StdQueries, p16
|
||||||
|
|
||||||
Ecu = car.CarParams.Ecu
|
Ecu = car.CarParams.Ecu
|
||||||
|
|
||||||
@@ -41,18 +41,9 @@ class CarControllerParams:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CAR(StrEnum):
|
class FordFlags(IntFlag):
|
||||||
BRONCO_SPORT_MK1 = "FORD BRONCO SPORT 1ST GEN"
|
# Static flags
|
||||||
ESCAPE_MK4 = "FORD ESCAPE 4TH GEN"
|
CANFD = 1
|
||||||
EXPLORER_MK6 = "FORD EXPLORER 6TH GEN"
|
|
||||||
F_150_MK14 = "FORD F-150 14TH GEN"
|
|
||||||
FOCUS_MK4 = "FORD FOCUS 4TH GEN"
|
|
||||||
MAVERICK_MK1 = "FORD MAVERICK 1ST GEN"
|
|
||||||
F_150_LIGHTNING_MK1 = "FORD F-150 LIGHTNING 1ST GEN"
|
|
||||||
MUSTANG_MACH_E_MK1 = "FORD MUSTANG MACH-E 1ST GEN"
|
|
||||||
|
|
||||||
|
|
||||||
CANFD_CAR = {CAR.F_150_MK14, CAR.F_150_LIGHTNING_MK1, CAR.MUSTANG_MACH_E_MK1}
|
|
||||||
|
|
||||||
|
|
||||||
class RADAR:
|
class RADAR:
|
||||||
@@ -60,14 +51,6 @@ class RADAR:
|
|||||||
DELPHI_MRR = 'FORD_CADS'
|
DELPHI_MRR = 'FORD_CADS'
|
||||||
|
|
||||||
|
|
||||||
DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict("ford_lincoln_base_pt", RADAR.DELPHI_MRR))
|
|
||||||
|
|
||||||
# F-150 radar is not yet supported
|
|
||||||
DBC[CAR.F_150_MK14] = dbc_dict("ford_lincoln_base_pt", None)
|
|
||||||
DBC[CAR.F_150_LIGHTNING_MK1] = dbc_dict("ford_lincoln_base_pt", None)
|
|
||||||
DBC[CAR.MUSTANG_MACH_E_MK1] = dbc_dict("ford_lincoln_base_pt", None)
|
|
||||||
|
|
||||||
|
|
||||||
class Footnote(Enum):
|
class Footnote(Enum):
|
||||||
FOCUS = CarFootnote(
|
FOCUS = CarFootnote(
|
||||||
"Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in " +
|
"Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in " +
|
||||||
@@ -77,36 +60,120 @@ class Footnote(Enum):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FordCarInfo(CarInfo):
|
class FordCarDocs(CarDocs):
|
||||||
package: str = "Co-Pilot360 Assist+"
|
package: str = "Co-Pilot360 Assist+"
|
||||||
|
hybrid: bool = False
|
||||||
|
plug_in_hybrid: bool = False
|
||||||
|
|
||||||
def init_make(self, CP: car.CarParams):
|
def init_make(self, CP: car.CarParams):
|
||||||
harness = CarHarness.ford_q4 if CP.carFingerprint in CANFD_CAR else CarHarness.ford_q3
|
harness = CarHarness.ford_q4 if CP.flags & FordFlags.CANFD else CarHarness.ford_q3
|
||||||
if CP.carFingerprint in (CAR.BRONCO_SPORT_MK1, CAR.MAVERICK_MK1, CAR.F_150_MK14):
|
if CP.carFingerprint in (CAR.BRONCO_SPORT_MK1, CAR.MAVERICK_MK1, CAR.F_150_MK14, CAR.F_150_LIGHTNING_MK1):
|
||||||
self.car_parts = CarParts([Device.threex_angled_mount, harness])
|
self.car_parts = CarParts([Device.threex_angled_mount, harness])
|
||||||
else:
|
else:
|
||||||
self.car_parts = CarParts([Device.threex, harness])
|
self.car_parts = CarParts([Device.threex, harness])
|
||||||
|
|
||||||
|
|
||||||
CAR_INFO: Dict[str, Union[CarInfo, List[CarInfo]]] = {
|
@dataclass
|
||||||
CAR.BRONCO_SPORT_MK1: FordCarInfo("Ford Bronco Sport 2021-22"),
|
class FordPlatformConfig(PlatformConfig):
|
||||||
CAR.ESCAPE_MK4: [
|
dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('ford_lincoln_base_pt', RADAR.DELPHI_MRR))
|
||||||
FordCarInfo("Ford Escape 2020-22"),
|
|
||||||
FordCarInfo("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering"),
|
def init(self):
|
||||||
],
|
for car_docs in list(self.car_docs):
|
||||||
CAR.EXPLORER_MK6: [
|
if car_docs.hybrid:
|
||||||
FordCarInfo("Ford Explorer 2020-23"),
|
name = f"{car_docs.make} {car_docs.model} Hybrid {car_docs.years}"
|
||||||
FordCarInfo("Lincoln Aviator 2020-21", "Co-Pilot360 Plus"),
|
self.car_docs.append(replace(copy.deepcopy(car_docs), name=name))
|
||||||
],
|
if car_docs.plug_in_hybrid:
|
||||||
CAR.F_150_MK14: FordCarInfo("Ford F-150 2023", "Co-Pilot360 Active 2.0"),
|
name = f"{car_docs.make} {car_docs.model} Plug-in Hybrid {car_docs.years}"
|
||||||
CAR.F_150_LIGHTNING_MK1: FordCarInfo("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0"),
|
self.car_docs.append(replace(copy.deepcopy(car_docs), name=name))
|
||||||
CAR.MUSTANG_MACH_E_MK1: FordCarInfo("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0"),
|
|
||||||
CAR.FOCUS_MK4: FordCarInfo("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS]),
|
|
||||||
CAR.MAVERICK_MK1: [
|
@dataclass
|
||||||
FordCarInfo("Ford Maverick 2022", "LARIAT Luxury"),
|
class FordCANFDPlatformConfig(FordPlatformConfig):
|
||||||
FordCarInfo("Ford Maverick 2023", "Co-Pilot360 Assist"),
|
dbc_dict: DbcDict = field(default_factory=lambda: dbc_dict('ford_lincoln_base_pt', None))
|
||||||
],
|
|
||||||
}
|
def init(self):
|
||||||
|
super().init()
|
||||||
|
self.flags |= FordFlags.CANFD
|
||||||
|
|
||||||
|
|
||||||
|
class CAR(Platforms):
|
||||||
|
BRONCO_SPORT_MK1 = FordPlatformConfig(
|
||||||
|
"FORD BRONCO SPORT 1ST GEN",
|
||||||
|
[FordCarDocs("Ford Bronco Sport 2021-23")],
|
||||||
|
CarSpecs(mass=1625, wheelbase=2.67, steerRatio=17.7),
|
||||||
|
)
|
||||||
|
ESCAPE_MK4 = FordPlatformConfig(
|
||||||
|
"FORD ESCAPE 4TH GEN",
|
||||||
|
[
|
||||||
|
FordCarDocs("Ford Escape 2020-22", hybrid=True, plug_in_hybrid=True),
|
||||||
|
FordCarDocs("Ford Kuga 2020-22", "Adaptive Cruise Control with Lane Centering", hybrid=True, plug_in_hybrid=True),
|
||||||
|
],
|
||||||
|
CarSpecs(mass=1750, wheelbase=2.71, steerRatio=16.7),
|
||||||
|
)
|
||||||
|
EXPLORER_MK6 = FordPlatformConfig(
|
||||||
|
"FORD EXPLORER 6TH GEN",
|
||||||
|
[
|
||||||
|
FordCarDocs("Ford Explorer 2020-23", hybrid=True), # Hybrid: Limited and Platinum only
|
||||||
|
FordCarDocs("Lincoln Aviator 2020-23", "Co-Pilot360 Plus", plug_in_hybrid=True), # Hybrid: Grand Touring only
|
||||||
|
],
|
||||||
|
CarSpecs(mass=2050, wheelbase=3.025, steerRatio=16.8),
|
||||||
|
)
|
||||||
|
F_150_MK14 = FordCANFDPlatformConfig(
|
||||||
|
"FORD F-150 14TH GEN",
|
||||||
|
[FordCarDocs("Ford F-150 2022-23", "Co-Pilot360 Active 2.0", hybrid=True)],
|
||||||
|
CarSpecs(mass=2000, wheelbase=3.69, steerRatio=17.0),
|
||||||
|
)
|
||||||
|
F_150_LIGHTNING_MK1 = FordCANFDPlatformConfig(
|
||||||
|
"FORD F-150 LIGHTNING 1ST GEN",
|
||||||
|
[FordCarDocs("Ford F-150 Lightning 2021-23", "Co-Pilot360 Active 2.0")],
|
||||||
|
CarSpecs(mass=2948, wheelbase=3.70, steerRatio=16.9),
|
||||||
|
)
|
||||||
|
FOCUS_MK4 = FordPlatformConfig(
|
||||||
|
"FORD FOCUS 4TH GEN",
|
||||||
|
[FordCarDocs("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS], hybrid=True)], # mHEV only
|
||||||
|
CarSpecs(mass=1350, wheelbase=2.7, steerRatio=15.0),
|
||||||
|
)
|
||||||
|
MAVERICK_MK1 = FordPlatformConfig(
|
||||||
|
"FORD MAVERICK 1ST GEN",
|
||||||
|
[
|
||||||
|
FordCarDocs("Ford Maverick 2022", "LARIAT Luxury", hybrid=True),
|
||||||
|
FordCarDocs("Ford Maverick 2023-24", "Co-Pilot360 Assist", hybrid=True),
|
||||||
|
],
|
||||||
|
CarSpecs(mass=1650, wheelbase=3.076, steerRatio=17.0),
|
||||||
|
)
|
||||||
|
MUSTANG_MACH_E_MK1 = FordCANFDPlatformConfig(
|
||||||
|
"FORD MUSTANG MACH-E 1ST GEN",
|
||||||
|
[FordCarDocs("Ford Mustang Mach-E 2021-23", "Co-Pilot360 Active 2.0")],
|
||||||
|
CarSpecs(mass=2200, wheelbase=2.984, steerRatio=17.0), # TODO: check steer ratio
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_IDENTIFIER_FORD_ASBUILT = 0xDE00
|
||||||
|
|
||||||
|
ASBUILT_BLOCKS: list[tuple[int, list]] = [
|
||||||
|
(1, [Ecu.debug, Ecu.fwdCamera, Ecu.eps]),
|
||||||
|
(2, [Ecu.abs, Ecu.debug, Ecu.eps]),
|
||||||
|
(3, [Ecu.abs, Ecu.debug, Ecu.eps]),
|
||||||
|
(4, [Ecu.debug, Ecu.fwdCamera]),
|
||||||
|
(5, [Ecu.debug]),
|
||||||
|
(6, [Ecu.debug]),
|
||||||
|
(7, [Ecu.debug]),
|
||||||
|
(8, [Ecu.debug]),
|
||||||
|
(9, [Ecu.debug]),
|
||||||
|
(16, [Ecu.debug, Ecu.fwdCamera]),
|
||||||
|
(18, [Ecu.fwdCamera]),
|
||||||
|
(20, [Ecu.fwdCamera]),
|
||||||
|
(21, [Ecu.fwdCamera]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def ford_asbuilt_block_request(block_id: int):
|
||||||
|
return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def ford_asbuilt_block_response(block_id: int):
|
||||||
|
return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1)
|
||||||
|
|
||||||
|
|
||||||
FW_QUERY_CONFIG = FwQueryConfig(
|
FW_QUERY_CONFIG = FwQueryConfig(
|
||||||
requests=[
|
requests=[
|
||||||
@@ -115,13 +182,30 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
|||||||
Request(
|
Request(
|
||||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
||||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
||||||
|
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||||
|
logging=True,
|
||||||
|
),
|
||||||
|
Request(
|
||||||
|
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
||||||
|
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
||||||
|
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||||
bus=0,
|
bus=0,
|
||||||
auxiliary=True,
|
auxiliary=True,
|
||||||
),
|
),
|
||||||
|
*[Request(
|
||||||
|
[StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
|
||||||
|
[StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
|
||||||
|
whitelist_ecus=ecus,
|
||||||
|
bus=0,
|
||||||
|
logging=True,
|
||||||
|
) for block_id, ecus in ASBUILT_BLOCKS],
|
||||||
],
|
],
|
||||||
extra_ecus=[
|
extra_ecus=[
|
||||||
# We are unlikely to get a response from the PCM from behind the gateway
|
(Ecu.engine, 0x7e0, None), # Powertrain Control Module
|
||||||
(Ecu.engine, 0x7e0, None),
|
# Note: We are unlikely to get a response from behind the gateway
|
||||||
(Ecu.shiftByWire, 0x732, None),
|
(Ecu.shiftByWire, 0x732, None), # Gear Shift Module
|
||||||
|
(Ecu.debug, 0x7d0, None), # Accessory Protocol Interface Module
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DBC = CAR.create_dbc_map()
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import capnp
|
|||||||
import copy
|
import copy
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import struct
|
import struct
|
||||||
from typing import Callable, Dict, List, Optional, Set, Tuple
|
from collections.abc import Callable
|
||||||
|
|
||||||
import panda.python.uds as uds
|
import panda.python.uds as uds
|
||||||
|
|
||||||
AddrType = Tuple[int, Optional[int]]
|
AddrType = tuple[int, int | None]
|
||||||
EcuAddrBusType = Tuple[int, Optional[int], int]
|
EcuAddrBusType = tuple[int, int | None, int]
|
||||||
EcuAddrSubAddr = Tuple[int, int, Optional[int]]
|
EcuAddrSubAddr = tuple[int, int, int | None]
|
||||||
|
|
||||||
LiveFwVersions = Dict[AddrType, Set[bytes]]
|
LiveFwVersions = dict[AddrType, set[bytes]]
|
||||||
OfflineFwVersions = Dict[str, Dict[EcuAddrSubAddr, List[bytes]]]
|
OfflineFwVersions = dict[str, dict[EcuAddrSubAddr, list[bytes]]]
|
||||||
|
|
||||||
# A global list of addresses we will only ever consider for VIN responses
|
# A global list of addresses we will only ever consider for VIN responses
|
||||||
# engine, hybrid controller, Ford abs, Hyundai CAN FD cluster, 29-bit engine, PGM-FI
|
# engine, hybrid controller, Ford abs, Hyundai CAN FD cluster, 29-bit engine, PGM-FI
|
||||||
@@ -47,6 +47,11 @@ class StdQueries:
|
|||||||
MANUFACTURER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
MANUFACTURER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
||||||
p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER)
|
p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER)
|
||||||
|
|
||||||
|
SUPPLIER_SOFTWARE_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
|
||||||
|
p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER)
|
||||||
|
SUPPLIER_SOFTWARE_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
||||||
|
p16(uds.DATA_IDENTIFIER_TYPE.SYSTEM_SUPPLIER_ECU_SOFTWARE_VERSION_NUMBER)
|
||||||
|
|
||||||
UDS_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
|
UDS_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \
|
||||||
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
|
p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_SOFTWARE_IDENTIFICATION)
|
||||||
UDS_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
UDS_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \
|
||||||
@@ -71,9 +76,9 @@ class StdQueries:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Request:
|
class Request:
|
||||||
request: List[bytes]
|
request: list[bytes]
|
||||||
response: List[bytes]
|
response: list[bytes]
|
||||||
whitelist_ecus: List[int] = field(default_factory=list)
|
whitelist_ecus: list[int] = field(default_factory=list)
|
||||||
rx_offset: int = 0x8
|
rx_offset: int = 0x8
|
||||||
bus: int = 1
|
bus: int = 1
|
||||||
# Whether this query should be run on the first auxiliary panda (CAN FD cars for example)
|
# Whether this query should be run on the first auxiliary panda (CAN FD cars for example)
|
||||||
@@ -86,15 +91,15 @@ class Request:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FwQueryConfig:
|
class FwQueryConfig:
|
||||||
requests: List[Request]
|
requests: list[Request]
|
||||||
# TODO: make this automatic and remove hardcoded lists, or do fingerprinting with ecus
|
# TODO: make this automatic and remove hardcoded lists, or do fingerprinting with ecus
|
||||||
# Overrides and removes from essential ecus for specific models and ecus (exact matching)
|
# Overrides and removes from essential ecus for specific models and ecus (exact matching)
|
||||||
non_essential_ecus: Dict[capnp.lib.capnp._EnumModule, List[str]] = field(default_factory=dict)
|
non_essential_ecus: dict[capnp.lib.capnp._EnumModule, list[str]] = field(default_factory=dict)
|
||||||
# Ecus added for data collection, not to be fingerprinted on
|
# Ecus added for data collection, not to be fingerprinted on
|
||||||
extra_ecus: List[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]]] = field(default_factory=list)
|
extra_ecus: list[tuple[capnp.lib.capnp._EnumModule, int, int | None]] = field(default_factory=list)
|
||||||
# Function a brand can implement to provide better fuzzy matching. Takes in FW versions,
|
# Function a brand can implement to provide better fuzzy matching. Takes in FW versions,
|
||||||
# returns set of candidates. Only will match if one candidate is returned
|
# returns set of candidates. Only will match if one candidate is returned
|
||||||
match_fw_to_car_fuzzy: Optional[Callable[[LiveFwVersions, OfflineFwVersions], Set[str]]] = None
|
match_fw_to_car_fuzzy: Callable[[LiveFwVersions, OfflineFwVersions], set[str]] | None = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
for i in range(len(self.requests)):
|
for i in range(len(self.requests)):
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, DefaultDict, Dict, Iterator, List, Optional, Set, TypeVar
|
from collections.abc import Iterator
|
||||||
|
from typing import Any, Protocol, TypeVar
|
||||||
|
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
import capnp
|
import capnp
|
||||||
|
|
||||||
import panda.python.uds as uds
|
import panda.python.uds as uds
|
||||||
from cereal import car
|
from cereal import car
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.selfdrive.car.ecu_addrs import get_ecu_addrs
|
|
||||||
from openpilot.selfdrive.car.fw_query_definitions import AddrType, EcuAddrBusType, FwQueryConfig
|
|
||||||
from openpilot.selfdrive.car.interfaces import get_interface_attr
|
|
||||||
from openpilot.selfdrive.car.fingerprints import FW_VERSIONS
|
|
||||||
from openpilot.selfdrive.car.isotp_parallel_query import IsoTpParallelQuery
|
|
||||||
from openpilot.common.swaglog import cloudlog
|
from openpilot.common.swaglog import cloudlog
|
||||||
|
from openpilot.selfdrive.car.ecu_addrs import get_ecu_addrs
|
||||||
|
from openpilot.selfdrive.car.fingerprints import FW_VERSIONS
|
||||||
|
from openpilot.selfdrive.car.fw_query_definitions import AddrType, EcuAddrBusType, FwQueryConfig, LiveFwVersions, OfflineFwVersions
|
||||||
|
from openpilot.selfdrive.car.interfaces import get_interface_attr
|
||||||
|
from openpilot.selfdrive.car.isotp_parallel_query import IsoTpParallelQuery
|
||||||
|
|
||||||
Ecu = car.CarParams.Ecu
|
Ecu = car.CarParams.Ecu
|
||||||
ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa]
|
ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.abs, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa]
|
||||||
@@ -27,19 +29,18 @@ REQUESTS = [(brand, config, r) for brand, config in FW_QUERY_CONFIGS.items() for
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
def chunks(l: List[T], n: int = 128) -> Iterator[List[T]]:
|
def chunks(l: list[T], n: int = 128) -> Iterator[list[T]]:
|
||||||
for i in range(0, len(l), n):
|
for i in range(0, len(l), n):
|
||||||
yield l[i:i + n]
|
yield l[i:i + n]
|
||||||
|
|
||||||
|
|
||||||
def is_brand(brand: str, filter_brand: Optional[str]) -> bool:
|
def is_brand(brand: str, filter_brand: str | None) -> bool:
|
||||||
"""Returns if brand matches filter_brand or no brand filter is specified"""
|
"""Returns if brand matches filter_brand or no brand filter is specified"""
|
||||||
return filter_brand is None or brand == filter_brand
|
return filter_brand is None or brand == filter_brand
|
||||||
|
|
||||||
|
|
||||||
def build_fw_dict(fw_versions: List[capnp.lib.capnp._DynamicStructBuilder],
|
def build_fw_dict(fw_versions: list[capnp.lib.capnp._DynamicStructBuilder], filter_brand: str = None) -> dict[AddrType, set[bytes]]:
|
||||||
filter_brand: Optional[str] = None) -> Dict[AddrType, Set[bytes]]:
|
fw_versions_dict: defaultdict[AddrType, set[bytes]] = defaultdict(set)
|
||||||
fw_versions_dict: DefaultDict[AddrType, Set[bytes]] = defaultdict(set)
|
|
||||||
for fw in fw_versions:
|
for fw in fw_versions:
|
||||||
if is_brand(fw.brand, filter_brand) and not fw.logging:
|
if is_brand(fw.brand, filter_brand) and not fw.logging:
|
||||||
sub_addr = fw.subAddress if fw.subAddress != 0 else None
|
sub_addr = fw.subAddress if fw.subAddress != 0 else None
|
||||||
@@ -47,7 +48,12 @@ def build_fw_dict(fw_versions: List[capnp.lib.capnp._DynamicStructBuilder],
|
|||||||
return dict(fw_versions_dict)
|
return dict(fw_versions_dict)
|
||||||
|
|
||||||
|
|
||||||
def match_fw_to_car_fuzzy(live_fw_versions, match_brand=None, log=True, exclude=None):
|
class MatchFwToCar(Protocol):
|
||||||
|
def __call__(self, live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True) -> set[str]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True, exclude: str = None) -> set[str]:
|
||||||
"""Do a fuzzy FW match. This function will return a match, and the number of firmware version
|
"""Do a fuzzy FW match. This function will return a match, and the number of firmware version
|
||||||
that were matched uniquely to that specific car. If multiple ECUs uniquely match to different cars
|
that were matched uniquely to that specific car. If multiple ECUs uniquely match to different cars
|
||||||
the match is rejected."""
|
the match is rejected."""
|
||||||
@@ -72,7 +78,7 @@ def match_fw_to_car_fuzzy(live_fw_versions, match_brand=None, log=True, exclude=
|
|||||||
all_fw_versions[(addr[1], addr[2], f)].append(candidate)
|
all_fw_versions[(addr[1], addr[2], f)].append(candidate)
|
||||||
|
|
||||||
matched_ecus = set()
|
matched_ecus = set()
|
||||||
candidate = None
|
match: str | None = None
|
||||||
for addr, versions in live_fw_versions.items():
|
for addr, versions in live_fw_versions.items():
|
||||||
ecu_key = (addr[0], addr[1])
|
ecu_key = (addr[0], addr[1])
|
||||||
for version in versions:
|
for version in versions:
|
||||||
@@ -81,23 +87,23 @@ def match_fw_to_car_fuzzy(live_fw_versions, match_brand=None, log=True, exclude=
|
|||||||
|
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
matched_ecus.add(ecu_key)
|
matched_ecus.add(ecu_key)
|
||||||
if candidate is None:
|
if match is None:
|
||||||
candidate = candidates[0]
|
match = candidates[0]
|
||||||
# We uniquely matched two different cars. No fuzzy match possible
|
# We uniquely matched two different cars. No fuzzy match possible
|
||||||
elif candidate != candidates[0]:
|
elif match != candidates[0]:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
# Note that it is possible to match to a candidate without all its ECUs being present
|
# Note that it is possible to match to a candidate without all its ECUs being present
|
||||||
# if there are enough matches. FIXME: parameterize this or require all ECUs to exist like exact matching
|
# if there are enough matches. FIXME: parameterize this or require all ECUs to exist like exact matching
|
||||||
if len(matched_ecus) >= 2:
|
if match and len(matched_ecus) >= 2:
|
||||||
if log:
|
if log:
|
||||||
cloudlog.error(f"Fingerprinted {candidate} using fuzzy match. {len(matched_ecus)} matching ECUs")
|
cloudlog.error(f"Fingerprinted {match} using fuzzy match. {len(matched_ecus)} matching ECUs")
|
||||||
return {candidate}
|
return {match}
|
||||||
else:
|
else:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw_versions=None) -> Set[str]:
|
def match_fw_to_car_exact(live_fw_versions: LiveFwVersions, match_brand: str = None, log: bool = True, extra_fw_versions: dict = None) -> set[str]:
|
||||||
"""Do an exact FW match. Returns all cars that match the given
|
"""Do an exact FW match. Returns all cars that match the given
|
||||||
FW versions for a list of "essential" ECUs. If an ECU is not considered
|
FW versions for a list of "essential" ECUs. If an ECU is not considered
|
||||||
essential the FW version can be missing to get a fingerprint, but if it's present it
|
essential the FW version can be missing to get a fingerprint, but if it's present it
|
||||||
@@ -138,9 +144,10 @@ def match_fw_to_car_exact(live_fw_versions, match_brand=None, log=True, extra_fw
|
|||||||
return set(candidates.keys()) - invalid
|
return set(candidates.keys()) - invalid
|
||||||
|
|
||||||
|
|
||||||
def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True):
|
def match_fw_to_car(fw_versions: list[capnp.lib.capnp._DynamicStructBuilder], allow_exact: bool = True, allow_fuzzy: bool = True,
|
||||||
|
log: bool = True) -> tuple[bool, set[str]]:
|
||||||
# Try exact matching first
|
# Try exact matching first
|
||||||
exact_matches = []
|
exact_matches: list[tuple[bool, MatchFwToCar]] = []
|
||||||
if allow_exact:
|
if allow_exact:
|
||||||
exact_matches = [(True, match_fw_to_car_exact)]
|
exact_matches = [(True, match_fw_to_car_exact)]
|
||||||
if allow_fuzzy:
|
if allow_fuzzy:
|
||||||
@@ -148,7 +155,7 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True):
|
|||||||
|
|
||||||
for exact_match, match_func in exact_matches:
|
for exact_match, match_func in exact_matches:
|
||||||
# For each brand, attempt to fingerprint using all FW returned from its queries
|
# For each brand, attempt to fingerprint using all FW returned from its queries
|
||||||
matches = set()
|
matches: set[str] = set()
|
||||||
for brand in VERSIONS.keys():
|
for brand in VERSIONS.keys():
|
||||||
fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand)
|
fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand)
|
||||||
matches |= match_func(fw_versions_dict, match_brand=brand, log=log)
|
matches |= match_func(fw_versions_dict, match_brand=brand, log=log)
|
||||||
@@ -164,12 +171,12 @@ def match_fw_to_car(fw_versions, allow_exact=True, allow_fuzzy=True, log=True):
|
|||||||
return True, set()
|
return True, set()
|
||||||
|
|
||||||
|
|
||||||
def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]:
|
def get_present_ecus(logcan, sendcan, num_pandas: int = 1) -> set[EcuAddrBusType]:
|
||||||
params = Params()
|
params = Params()
|
||||||
# queries are split by OBD multiplexing mode
|
# queries are split by OBD multiplexing mode
|
||||||
queries: Dict[bool, List[List[EcuAddrBusType]]] = {True: [], False: []}
|
queries: dict[bool, list[list[EcuAddrBusType]]] = {True: [], False: []}
|
||||||
parallel_queries: Dict[bool, List[EcuAddrBusType]] = {True: [], False: []}
|
parallel_queries: dict[bool, list[EcuAddrBusType]] = {True: [], False: []}
|
||||||
responses = set()
|
responses: set[EcuAddrBusType] = set()
|
||||||
|
|
||||||
for brand, config, r in REQUESTS:
|
for brand, config, r in REQUESTS:
|
||||||
# Skip query if no panda available
|
# Skip query if no panda available
|
||||||
@@ -203,7 +210,7 @@ def get_present_ecus(logcan, sendcan, num_pandas=1) -> Set[EcuAddrBusType]:
|
|||||||
return ecu_responses
|
return ecu_responses
|
||||||
|
|
||||||
|
|
||||||
def get_brand_ecu_matches(ecu_rx_addrs: Set[EcuAddrBusType]) -> dict[str, set[AddrType]]:
|
def get_brand_ecu_matches(ecu_rx_addrs: set[EcuAddrBusType]) -> dict[str, set[AddrType]]:
|
||||||
"""Returns dictionary of brands and matches with ECUs in their FW versions"""
|
"""Returns dictionary of brands and matches with ECUs in their FW versions"""
|
||||||
|
|
||||||
brand_addrs = {brand: {(addr, subaddr) for _, addr, subaddr in config.get_all_ecus(VERSIONS[brand])} for
|
brand_addrs = {brand: {(addr, subaddr) for _, addr, subaddr in config.get_all_ecus(VERSIONS[brand])} for
|
||||||
@@ -230,8 +237,8 @@ def set_obd_multiplexing(params: Params, obd_multiplexing: bool):
|
|||||||
cloudlog.warning("OBD multiplexing set successfully")
|
cloudlog.warning("OBD multiplexing set successfully")
|
||||||
|
|
||||||
|
|
||||||
def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \
|
def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs: set[EcuAddrBusType], timeout: float = 0.1, num_pandas: int = 1,
|
||||||
List[capnp.lib.capnp._DynamicStructBuilder]:
|
debug: bool = False, progress: bool = False) -> list[capnp.lib.capnp._DynamicStructBuilder]:
|
||||||
"""Queries for FW versions ordering brands by likelihood, breaks when exact match is found"""
|
"""Queries for FW versions ordering brands by likelihood, breaks when exact match is found"""
|
||||||
|
|
||||||
all_car_fw = []
|
all_car_fw = []
|
||||||
@@ -253,8 +260,8 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, num_pand
|
|||||||
return all_car_fw
|
return all_car_fw
|
||||||
|
|
||||||
|
|
||||||
def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, num_pandas=1, debug=False, progress=False) -> \
|
def get_fw_versions(logcan, sendcan, query_brand: str = None, extra: OfflineFwVersions = None, timeout: float = 0.1, num_pandas: int = 1,
|
||||||
List[capnp.lib.capnp._DynamicStructBuilder]:
|
debug: bool = False, progress: bool = False) -> list[capnp.lib.capnp._DynamicStructBuilder]:
|
||||||
versions = VERSIONS.copy()
|
versions = VERSIONS.copy()
|
||||||
params = Params()
|
params = Params()
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from openpilot.common.realtime import DT_CTRL
|
|||||||
from opendbc.can.packer import CANPacker
|
from opendbc.can.packer import CANPacker
|
||||||
from openpilot.selfdrive.car import apply_driver_steer_torque_limits, create_gas_interceptor_command
|
from openpilot.selfdrive.car import apply_driver_steer_torque_limits, create_gas_interceptor_command
|
||||||
from openpilot.selfdrive.car.gm import gmcan
|
from openpilot.selfdrive.car.gm import gmcan
|
||||||
from openpilot.selfdrive.car.gm.values import DBC, CanBus, CarControllerParams, CruiseButtons, GMFlags, CC_ONLY_CAR, EV_CAR, SDGM_CAR
|
from openpilot.selfdrive.car.gm.values import DBC, CanBus, CarControllerParams, CruiseButtons, GMFlags, CC_ONLY_CAR, SDGM_CAR, EV_CAR
|
||||||
|
from openpilot.selfdrive.car.interfaces import CarControllerBase
|
||||||
from openpilot.selfdrive.controls.lib.drive_helpers import apply_deadzone
|
from openpilot.selfdrive.controls.lib.drive_helpers import apply_deadzone
|
||||||
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
|
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ PITCH_DEADZONE = 0.01 # [radians] 0.01 ≈ 1% grade
|
|||||||
BRAKE_PITCH_FACTOR_BP = [5., 10.] # [m/s] smoothly revert to planned accel at low speeds
|
BRAKE_PITCH_FACTOR_BP = [5., 10.] # [m/s] smoothly revert to planned accel at low speeds
|
||||||
BRAKE_PITCH_FACTOR_V = [0., 1.] # [unitless in [0,1]]; don't touch
|
BRAKE_PITCH_FACTOR_V = [0., 1.] # [unitless in [0,1]]; don't touch
|
||||||
|
|
||||||
class CarController:
|
class CarController(CarControllerBase):
|
||||||
def __init__(self, dbc_name, CP, VM):
|
def __init__(self, dbc_name, CP, VM):
|
||||||
self.CP = CP
|
self.CP = CP
|
||||||
self.start_time = 0.
|
self.start_time = 0.
|
||||||
@@ -136,7 +137,6 @@ class CarController:
|
|||||||
self.apply_gas = self.params.INACTIVE_REGEN
|
self.apply_gas = self.params.INACTIVE_REGEN
|
||||||
self.apply_brake = int(min(-100 * self.CP.stopAccel, self.params.MAX_BRAKE))
|
self.apply_brake = int(min(-100 * self.CP.stopAccel, self.params.MAX_BRAKE))
|
||||||
else:
|
else:
|
||||||
# Normal operation
|
|
||||||
brake_accel = actuators.accel + self.accel_g * interp(CS.out.vEgo, BRAKE_PITCH_FACTOR_BP, BRAKE_PITCH_FACTOR_V)
|
brake_accel = actuators.accel + self.accel_g * interp(CS.out.vEgo, BRAKE_PITCH_FACTOR_BP, BRAKE_PITCH_FACTOR_V)
|
||||||
if self.CP.carFingerprint in EV_CAR and frogpilot_variables.use_ev_tables:
|
if self.CP.carFingerprint in EV_CAR and frogpilot_variables.use_ev_tables:
|
||||||
self.params.update_ev_gas_brake_threshold(CS.out.vEgo)
|
self.params.update_ev_gas_brake_threshold(CS.out.vEgo)
|
||||||
@@ -188,12 +188,12 @@ class CarController:
|
|||||||
# GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation
|
# GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation
|
||||||
can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, CC.enabled, at_full_stop))
|
can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, CC.enabled, at_full_stop))
|
||||||
can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, friction_brake_bus, self.apply_brake,
|
can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, friction_brake_bus, self.apply_brake,
|
||||||
idx, CC.enabled, near_stop, at_full_stop, self.CP))
|
idx, CC.enabled, near_stop, at_full_stop, self.CP))
|
||||||
|
|
||||||
# Send dashboard UI commands (ACC status)
|
# Send dashboard UI commands (ACC status)
|
||||||
send_fcw = hud_alert == VisualAlert.fcw
|
send_fcw = hud_alert == VisualAlert.fcw
|
||||||
can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled,
|
can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled,
|
||||||
hud_v_cruise * CV.MS_TO_KPH, hud_control.leadVisible, send_fcw, CS.display_menu, CS.personality_profile))
|
hud_v_cruise * CV.MS_TO_KPH, hud_control, send_fcw))
|
||||||
else:
|
else:
|
||||||
# to keep accel steady for logs when not sending gas
|
# to keep accel steady for logs when not sending gas
|
||||||
accel += self.accel_g
|
accel += self.accel_g
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from openpilot.common.numpy_fast import mean
|
|||||||
from opendbc.can.can_define import CANDefine
|
from opendbc.can.can_define import CANDefine
|
||||||
from opendbc.can.parser import CANParser
|
from opendbc.can.parser import CANParser
|
||||||
from openpilot.selfdrive.car.interfaces import CarStateBase
|
from openpilot.selfdrive.car.interfaces import CarStateBase
|
||||||
from openpilot.selfdrive.car.gm.values import DBC, AccState, CanBus, STEER_THRESHOLD, GMFlags, CC_ONLY_CAR, CAMERA_ACC_CAR, SDGM_CAR
|
from openpilot.selfdrive.car.gm.values import DBC, AccState, CanBus, STEER_THRESHOLD, GMFlags, CAMERA_ACC_CAR, CC_ONLY_CAR, SDGM_CAR
|
||||||
|
|
||||||
TransmissionType = car.CarParams.TransmissionType
|
TransmissionType = car.CarParams.TransmissionType
|
||||||
NetworkLocation = car.CarParams.NetworkLocation
|
NetworkLocation = car.CarParams.NetworkLocation
|
||||||
@@ -27,23 +27,24 @@ class CarState(CarStateBase):
|
|||||||
self.cam_lka_steering_cmd_counter = 0
|
self.cam_lka_steering_cmd_counter = 0
|
||||||
self.buttons_counter = 0
|
self.buttons_counter = 0
|
||||||
|
|
||||||
|
self.prev_distance_button = 0
|
||||||
|
self.distance_button = 0
|
||||||
|
|
||||||
# FrogPilot variables
|
# FrogPilot variables
|
||||||
self.single_pedal_mode = False
|
self.single_pedal_mode = False
|
||||||
|
|
||||||
# FrogPilot variables
|
|
||||||
self.display_menu = False
|
|
||||||
|
|
||||||
self.display_timer = 0
|
|
||||||
|
|
||||||
def update(self, pt_cp, cam_cp, loopback_cp, frogpilot_variables):
|
def update(self, pt_cp, cam_cp, loopback_cp, frogpilot_variables):
|
||||||
ret = car.CarState.new_message()
|
ret = car.CarState.new_message()
|
||||||
|
|
||||||
self.prev_cruise_buttons = self.cruise_buttons
|
self.prev_cruise_buttons = self.cruise_buttons
|
||||||
|
self.prev_distance_button = self.distance_button
|
||||||
if self.CP.carFingerprint not in SDGM_CAR:
|
if self.CP.carFingerprint not in SDGM_CAR:
|
||||||
self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"]
|
self.cruise_buttons = pt_cp.vl["ASCMSteeringButton"]["ACCButtons"]
|
||||||
|
self.distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"]
|
||||||
self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"]
|
self.buttons_counter = pt_cp.vl["ASCMSteeringButton"]["RollingCounter"]
|
||||||
else:
|
else:
|
||||||
self.cruise_buttons = cam_cp.vl["ASCMSteeringButton"]["ACCButtons"]
|
self.cruise_buttons = cam_cp.vl["ASCMSteeringButton"]["ACCButtons"]
|
||||||
|
self.distance_button = cam_cp.vl["ASCMSteeringButton"]["DistanceButton"]
|
||||||
self.buttons_counter = cam_cp.vl["ASCMSteeringButton"]["RollingCounter"]
|
self.buttons_counter = cam_cp.vl["ASCMSteeringButton"]["RollingCounter"]
|
||||||
self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"])
|
self.pscm_status = copy.copy(pt_cp.vl["PSCMStatus"])
|
||||||
# This is to avoid a fault where you engage while still moving backwards after shifting to D.
|
# This is to avoid a fault where you engage while still moving backwards after shifting to D.
|
||||||
@@ -167,58 +168,11 @@ class CarState(CarStateBase):
|
|||||||
ret.leftBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
|
ret.leftBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["LeftBSM"] == 1
|
||||||
ret.rightBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
|
ret.rightBlindspot = cam_cp.vl["BCMBlindSpotMonitor"]["RightBSM"] == 1
|
||||||
|
|
||||||
# Driving personalities function - Credit goes to Mangomoose!
|
self.lkas_previously_enabled = self.lkas_enabled
|
||||||
if frogpilot_variables.personalities_via_wheel and ret.cruiseState.available:
|
if self.CP.carFingerprint in SDGM_CAR:
|
||||||
# Sync with the onroad UI button
|
self.lkas_enabled = cam_cp.vl["ASCMSteeringButton"]["LKAButton"]
|
||||||
if self.fpf.personality_changed_via_ui:
|
else:
|
||||||
self.personality_profile = self.fpf.current_personality
|
self.lkas_enabled = pt_cp.vl["ASCMSteeringButton"]["LKAButton"]
|
||||||
self.previous_personality_profile = self.personality_profile
|
|
||||||
self.fpf.reset_personality_changed_param()
|
|
||||||
|
|
||||||
# Check if the car has a camera
|
|
||||||
has_camera = self.CP.networkLocation == NetworkLocation.fwdCamera
|
|
||||||
has_camera &= not self.CP.flags & GMFlags.NO_CAMERA.value
|
|
||||||
has_camera &= not self.CP.carFingerprint in (CC_ONLY_CAR)
|
|
||||||
|
|
||||||
if has_camera:
|
|
||||||
# Need to subtract by 1 to comply with the personality profiles of "0", "1", and "2"
|
|
||||||
self.personality_profile = cam_cp.vl["ASCMActiveCruiseControlStatus"]["ACCGapLevel"] - 1
|
|
||||||
else:
|
|
||||||
if self.CP.carFingerprint in SDGM_CAR:
|
|
||||||
distance_button = cam_cp.vl["ASCMSteeringButton"]["DistanceButton"]
|
|
||||||
else:
|
|
||||||
distance_button = pt_cp.vl["ASCMSteeringButton"]["DistanceButton"]
|
|
||||||
|
|
||||||
if distance_button and not self.distance_previously_pressed:
|
|
||||||
if self.display_menu:
|
|
||||||
self.personality_profile = (self.previous_personality_profile + 2) % 3
|
|
||||||
self.display_timer = 350
|
|
||||||
self.distance_previously_pressed = distance_button
|
|
||||||
|
|
||||||
# Check if the display is open
|
|
||||||
if self.display_timer > 0:
|
|
||||||
self.display_timer -= 1
|
|
||||||
self.display_menu = True
|
|
||||||
else:
|
|
||||||
self.display_menu = False
|
|
||||||
|
|
||||||
if self.personality_profile != self.previous_personality_profile and self.personality_profile >= 0:
|
|
||||||
self.fpf.distance_button_function(self.personality_profile)
|
|
||||||
self.previous_personality_profile = self.personality_profile
|
|
||||||
|
|
||||||
# Toggle Experimental Mode from steering wheel function
|
|
||||||
if frogpilot_variables.experimental_mode_via_lkas and ret.cruiseState.available:
|
|
||||||
if self.CP.carFingerprint in SDGM_CAR:
|
|
||||||
lkas_pressed = cam_cp.vl["ASCMSteeringButton"]["LKAButton"]
|
|
||||||
else:
|
|
||||||
lkas_pressed = pt_cp.vl["ASCMSteeringButton"]["LKAButton"]
|
|
||||||
|
|
||||||
if lkas_pressed and not self.lkas_previously_pressed:
|
|
||||||
if frogpilot_variables.conditional_experimental_mode:
|
|
||||||
self.fpf.update_cestatus_lkas()
|
|
||||||
else:
|
|
||||||
self.fpf.update_experimental_mode()
|
|
||||||
self.lkas_previously_pressed = lkas_pressed
|
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -285,6 +239,7 @@ class CarState(CarStateBase):
|
|||||||
messages += [
|
messages += [
|
||||||
("ASCMLKASteeringCmd", 0),
|
("ASCMLKASteeringCmd", 0),
|
||||||
]
|
]
|
||||||
|
|
||||||
if CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG.value:
|
if CP.flags & GMFlags.NO_ACCELERATOR_POS_MSG.value:
|
||||||
messages.remove(("ECMAcceleratorPos", 80))
|
messages.remove(("ECMAcceleratorPos", 80))
|
||||||
messages.append(("EBCMBrakePedalPosition", 100))
|
messages.append(("EBCMBrakePedalPosition", 100))
|
||||||
|
|||||||