Compare commits

...

37 Commits

Author SHA1 Message Date
Jason Wen 21b89d237e lint 2026-07-22 19:42:09 -04:00
Jason Wen b46f967003 Merge commaai/openpilot fdd1df79 into sync-20260721
Merge commaai/opendbc d4c6f68c into sync-20260721
2026-07-22 18:35:26 -04:00
Adeeb Shihadeh fdd1df79fb longitudinal: remove per-car stopping tunes (#38394)
* remove per-car longitudinal stopping tunes

* bump opendbc

* lil more

* lil more

* lil more

* lil more

* revert modeld for now
2026-07-20 19:52:23 -07:00
Adeeb Shihadeh 031b1ad0a3 longcontrol: remove starting state (#38340) 2026-07-20 16:30:42 -07:00
Daniel Koepping b1f2e638de fix branch cleanup burning GH API requests (#38392)
* ci: stop branch cleanup from burning API rate limit

* simpler
2026-07-20 16:00:20 -07:00
Daniel Koepping c20263d985 speedup chunk reading (#38389)
read chunk directly into caller buffer
2026-07-20 14:46:33 -07:00
Daniel Koepping 5472e69e35 New sounds (#38154)
* new sounds

* soundd: fix sound cutoff

* update sounds

* add max warning_immediate variant

* mici: play sounds button in developer settings

* rename sounds

* add complete sound

* update sounds

* raise soundfloor by 5%

* update sounds

* play unused sounds

* update sounds

* bump opendbc

* Revert "play unused sounds"

This reverts commit 47e538da2200e700804714576baa5f8c0f28cdb3.

* Revert "mici: play sounds button in developer settings"

This reverts commit 149141bb8a21bec7a04dc8f231d0b6b9120cc7c2.

* remove unused sounds

* raise soundfloor by 5%

* opendbc

* add space
2026-07-20 14:31:55 -07:00
Daniel Koepping 78909dac73 soundd: add complete sound (#38390) 2026-07-20 10:50:18 -07:00
Daniel Koepping 24a9b6dae5 profile usb gpu link stability (#38368)
* profile usb gpu link before using

* ci
2026-07-20 10:36:09 -07:00
Adeeb Shihadeh d9596fa998 gc old athena and uploader migration code 2026-07-19 14:55:42 -07:00
Adeeb Shihadeh e124d6df9b more dead code gc 2026-07-19 14:52:04 -07:00
Adeeb Shihadeh 3dbf02f803 cereal: gc dead ZMQ branches in tests 2026-07-19 14:45:26 -07:00
Adeeb Shihadeh fef29ad225 start porting tests to unittest style (#38384) 2026-07-19 14:33:26 -07:00
Adeeb Shihadeh 157c7080ce bump opendbc (#38370) 2026-07-19 12:21:04 -07:00
Adeeb Shihadeh caa9e770cc manager: remove preimport stage (#38381) 2026-07-19 12:20:25 -07:00
Adeeb Shihadeh 24893ebadb rm pre-commit-hooks (#38382)
* rm pre-commit-hooks

* rm test for the tests

* lil more
2026-07-19 12:10:48 -07:00
Adeeb Shihadeh 39e12c8bb1 rm cffi, it's a raylib transitive dep 2026-07-19 11:14:40 -07:00
Adeeb Shihadeh f0d93eb32d more ty, part 2 (#38379) 2026-07-19 11:02:00 -07:00
Adeeb Shihadeh 19ecc37de8 more ty (#38378)
* enable no matching overload

* enable call non callable

* enable unsupported-operator

* enable not subscriptable

* refactor pass
2026-07-19 09:45:46 -07:00
Adeeb Shihadeh ecac2d386b these go in tools 2026-07-19 09:02:02 -07:00
Adeeb Shihadeh e475d10adc more ruff (#38377)
* lil more ruff

* no exclusions!

* rm nb exception

* and generated

* not used

* c408

* random

* all

* unittest is fine
2026-07-19 08:51:16 -07:00
Adeeb Shihadeh 7d74c3c99b rm setuptools (#38376)
* rm setuptools

* lock
2026-07-19 08:25:31 -07:00
Adeeb Shihadeh cefcf10ec3 rm jinja2 2026-07-19 08:15:01 -07:00
Adeeb Shihadeh 1c07e00759 rm xattr (#38373)
* rm xattr

* doesn't add new coverage
2026-07-18 13:55:47 -07:00
Adeeb Shihadeh f0841b827a remove av (#38366) 2026-07-18 09:04:41 -07:00
Adeeb Shihadeh 3f49e2d33c jp: add thumbnail source (#38363) 2026-07-18 09:02:08 -07:00
Adeeb Shihadeh 3f93b00120 webrtc: remove av (#38369) 2026-07-18 08:52:02 -07:00
stef c21b0821da fix(ui): label alignment and text with icon (#38365)
fix align bottom and text positioning with icon
2026-07-17 16:23:44 -07:00
Adeeb Shihadeh b9f25f8a43 webrtcd: move to libdatachannel (#38228)
* try libdatachannel

* fix uv lock

* fix uv lock

* remove datachannel abstraction and just use libdatachannel

* clean

* fix livestream bitrate controller

* fix sample

* fix import breaking ci

* clean up and add catches

* bump tele

* move to teleop master

* fix linter

* .

* remove libdatachannel explicit reference from pyproject.toml

* add PyJWT crypto and catch in registration

* spelling

* add crypto back to lock

---------

Co-authored-by: stefpi <19478336+stefpi@users.noreply.github.com>
2026-07-17 15:28:39 -07:00
ZwX1616 c8786d930d DM: reasonable lockout ramp up (#38358) 2026-07-17 15:27:20 -07:00
Adeeb Shihadeh 3a55f31dc5 agnos 18.5 (#38302) 2026-07-17 15:24:34 -07:00
Adeeb Shihadeh a04c045cd7 cabana: de-Qt, part 3 (#38360) 2026-07-17 09:31:16 -07:00
rkdune 9aa5c3d2c6 expose submodule dependencies as an extra 2026-07-16 18:16:08 -07:00
Adeeb Shihadeh 5d23a78c77 cabana: de-Qt, part 2 (#38359) 2026-07-16 14:35:45 -07:00
Adeeb Shihadeh 06a73f538e cabana: de-Qt, part 1 (#38357)
* cabana: de-Qt, part 1

* lil more
2026-07-16 13:45:57 -07:00
Shane Smiskol 60716edc37 Fix thumbnail creation (#38354)
it's not a stream
2026-07-15 22:51:41 -07:00
Adeeb Shihadeh 61608db786 jp: fix linking on macOS (#38353)
* jp: fix linking on macOS

* mv to root
2026-07-15 21:39:47 -07:00
246 changed files with 4613 additions and 3052 deletions
+8
View File
@@ -93,6 +93,14 @@ jobs:
const { owner, repo } = context.repo;
const upstream = `${owner}/${repo}`;
const closed = context.payload.pull_request;
if (closed) {
if (closed.head.repo?.full_name === upstream) {
await github.rest.git.deleteRef({ owner, repo, ref: `heads/${closed.head.ref}` }).catch(console.log);
}
return;
}
for await (const response of github.paginate.iterator(github.rest.pulls.list, {
owner,
repo,
+5
View File
@@ -166,6 +166,11 @@ env = Environment(
tools=["default", "cython", "compilation_db", "rednose_filter"],
toolpath=["#site_scons/site_tools", "#rednose_repo/site_scons/site_tools"],
)
# SCons' Darwin linker tool doesn't define the variables used to expand RPATH.
if arch == "Darwin":
env["RPATHPREFIX"] = "-Wl,-rpath,"
env["RPATHSUFFIX"] = ""
env["_RPATH"] = "${_concat(RPATHPREFIX, RPATH, RPATHSUFFIX, __env__)}"
if arch != "larch64":
env['_LIBFLAGS'] = _libflags
+1 -1
View File
@@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1
export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="18.4"
export AGNOS_VERSION="18.5"
fi
export STAGING_ROOT="/data/safe_staging"
+3 -1
View File
@@ -812,6 +812,7 @@ struct SelfdriveState {
promptDistracted @8;
preAlert @9;
complete @10;
}
enum OpenpilotState @0xdbe58b96d2d1ac61 {
@@ -2160,7 +2161,8 @@ struct DriverMonitoringStateDEPRECATED @0xb83cda094a1da284 {
struct DriverMonitoringState {
lockout @0 :Bool;
lockoutRecoveryPercent @11 :Int8;
lockoutCount @15 :Int8;
lockoutMinutesRemaining @11 :Int8;
alert3Count @12 :Int8;
noResponseCount @13 :Int8;
noResponseForceDecel @14 :Bool;
+55 -24
View File
@@ -7,12 +7,43 @@ import os
import capnp
import time
from typing import Optional, List, Union, Dict
from typing import Union
from openpilot.cereal import log
from openpilot.cereal.services import SERVICE_LIST
from openpilot.common.utils import MovingAverage
__all__ = (
"NO_TRAVERSAL_LIMIT",
"Context",
"FrequencyTracker",
"IpcError",
"MultiplePublishersError",
"Poller",
"PubMaster",
"PubSocket",
"SocketEventHandle",
"SubMaster",
"SubSocket",
"delete_fake_prefix",
"drain_sock",
"drain_sock_raw",
"fake_event_handle",
"get_fake_prefix",
"log_from_bytes",
"new_message",
"pub_sock",
"recv_one",
"recv_one_or_none",
"recv_one_retry",
"recv_sock",
"reset_context",
"set_fake_prefix",
"sub_sock",
"toggle_fake_events",
"wait_for_one_event",
)
NO_TRAVERSAL_LIMIT = 2**64-1
@@ -22,8 +53,8 @@ def pub_sock(endpoint: str) -> PubSocket:
return msgq.pub_sock(endpoint, segment_size)
def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1",
conflate: bool = False, timeout: Optional[int] = None) -> SubSocket:
def sub_sock(endpoint: str, poller: Poller | None = None, addr: str = "127.0.0.1",
conflate: bool = False, timeout: int | None = None) -> SubSocket:
service = SERVICE_LIST.get(endpoint)
segment_size = service.queue_size if service else 0
return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate,
@@ -39,7 +70,7 @@ def log_from_bytes(dat: bytes, struct: capnp.lib.capnp._StructModule = log.Event
return msg
def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
def new_message(service: str | None, size: int | None = None, **kwargs) -> capnp.lib.capnp._DynamicStructBuilder:
args = {
'valid': False,
'logMonoTime': int(time.monotonic() * 1e9),
@@ -54,14 +85,14 @@ def new_message(service: Optional[str], size: Optional[int] = None, **kwargs) ->
return dat
def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> List[capnp.lib.capnp._DynamicStructReader]:
def drain_sock(sock: SubSocket, wait_for_one: bool = False) -> list[capnp.lib.capnp._DynamicStructReader]:
"""Receive all message currently available on the queue"""
msgs = drain_sock_raw(sock, wait_for_one=wait_for_one)
return [log_from_bytes(m) for m in msgs]
# TODO: print when we drop packets?
def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._DynamicStructReader]:
def recv_sock(sock: SubSocket, wait: bool = False) -> capnp.lib.capnp._DynamicStructReader | None:
"""Same as drain sock, but only returns latest message. Consider using conflate instead."""
dat = None
@@ -82,14 +113,14 @@ def recv_sock(sock: SubSocket, wait: bool = False) -> Optional[capnp.lib.capnp._
return dat
def recv_one(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]:
def recv_one(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None:
dat = sock.receive()
if dat is not None:
dat = log_from_bytes(dat)
return dat
def recv_one_or_none(sock: SubSocket) -> Optional[capnp.lib.capnp._DynamicStructReader]:
def recv_one_or_none(sock: SubSocket) -> capnp.lib.capnp._DynamicStructReader | None:
dat = sock.receive(non_blocking=True)
if dat is not None:
dat = log_from_bytes(dat)
@@ -148,27 +179,27 @@ class FrequencyTracker:
class SubMaster:
def __init__(self, services: List[str], poll: Optional[str] = None,
ignore_alive: Optional[List[str]] = None, ignore_avg_freq: Optional[List[str]] = None,
ignore_valid: Optional[List[str]] = None, addr: str = "127.0.0.1", frequency: Optional[float] = None):
def __init__(self, services: list[str], poll: str | None = None,
ignore_alive: list[str] | None = None, ignore_avg_freq: list[str] | None = None,
ignore_valid: list[str] | None = None, addr: str = "127.0.0.1", frequency: float | None = None):
self.frame = -1
self.services = services
self.seen = {s: False for s in services}
self.updated = {s: False for s in services}
self.recv_time = {s: 0. for s in services}
self.recv_frame = {s: 0 for s in services}
self.seen = dict.fromkeys(services, False)
self.updated = dict.fromkeys(services, False)
self.recv_time = dict.fromkeys(services, 0.0)
self.recv_frame = dict.fromkeys(services, 0)
self.sock = {}
self.data = {}
self.logMonoTime = {s: 0 for s in services}
self.logMonoTime = dict.fromkeys(services, 0)
# zero-frequency / on-demand services are always alive and presumed valid; all others must pass checks
on_demand = {s: SERVICE_LIST[s].frequency <= 1e-5 for s in services}
self.static_freq_services = set(s for s in services if not on_demand[s])
self.static_freq_services = {s for s in services if not on_demand[s]}
self.alive = {s: on_demand[s] for s in services}
self.freq_ok = {s: on_demand[s] for s in services}
self.valid = {s: on_demand[s] for s in services}
self.freq_tracker: Dict[str, FrequencyTracker] = {}
self.freq_tracker: dict[str, FrequencyTracker] = {}
self.poller = Poller()
polled_services = set([poll, ] if poll is not None else services)
self.non_polled_services = set(services) - polled_services
@@ -211,7 +242,7 @@ class SubMaster:
msgs.append(recv_one_or_none(self.sock[s]))
self.update_msgs(time.monotonic(), msgs)
def update_msgs(self, cur_time: float, msgs: List[capnp.lib.capnp._DynamicStructReader]) -> None:
def update_msgs(self, cur_time: float, msgs: list[capnp.lib.capnp._DynamicStructReader]) -> None:
self.frame += 1
self.updated = dict.fromkeys(self.services, False)
for msg in msgs:
@@ -234,21 +265,21 @@ class SubMaster:
self.alive[s] = (cur_time - self.recv_time[s]) < (10. / SERVICE_LIST[s].frequency) or (self.seen[s] and self.simulation)
self.freq_ok[s] = self.freq_tracker[s].valid or self.simulation
def all_alive(self, service_list: Optional[List[str]] = None) -> bool:
def all_alive(self, service_list: list[str] | None = None) -> bool:
return all(self.alive[s] for s in (service_list or self.services) if s not in self.ignore_alive)
def all_freq_ok(self, service_list: Optional[List[str]] = None) -> bool:
def all_freq_ok(self, service_list: list[str] | None = None) -> bool:
return all(self.freq_ok[s] for s in (service_list or self.services) if self._check_avg_freq(s))
def all_valid(self, service_list: Optional[List[str]] = None) -> bool:
def all_valid(self, service_list: list[str] | None = None) -> bool:
return all(self.valid[s] for s in (service_list or self.services) if s not in self.ignore_valid)
def all_checks(self, service_list: Optional[List[str]] = None) -> bool:
def all_checks(self, service_list: list[str] | None = None) -> bool:
return self.all_alive(service_list) and self.all_freq_ok(service_list) and self.all_valid(service_list)
class PubMaster:
def __init__(self, services: List[str]):
def __init__(self, services: list[str]):
self.sock = {}
for s in services:
self.sock[s] = pub_sock(s)
@@ -1,4 +1,3 @@
import os
import capnp
import multiprocessing
import numbers
@@ -6,7 +5,6 @@ import random
import threading
import time
from openpilot.common.parameterized import parameterized
import pytest
from openpilot.cereal import log
from opendbc.car.structs import car
@@ -24,10 +22,6 @@ def random_socks(num_socks=10):
def random_bytes(length=1000):
return bytes([random.randrange(0xFF) for _ in range(length)])
def zmq_sleep(t=1):
if "ZMQ" in os.environ:
time.sleep(t)
# TODO: this should take any capnp struct and returrn a msg with random populated data
def random_carstate():
@@ -53,16 +47,6 @@ def delayed_send(delay, sock, dat):
class TestMessaging:
def setUp(self):
# TODO: ZMQ tests are too slow; all sleeps will need to be
# replaced with logic to block on the necessary condition
if "ZMQ" in os.environ:
pytest.skip()
# ZMQ pub socket takes too long to die
# sleep to prevent multiple publishers error between tests
zmq_sleep()
@parameterized.expand(events)
def test_new_message(self, evt):
try:
@@ -89,7 +73,6 @@ class TestMessaging:
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=1000)
zmq_sleep()
# no wait and no msgs in queue
msgs = func(sub_sock)
@@ -110,7 +93,6 @@ class TestMessaging:
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=100)
zmq_sleep()
# no wait and no msg in queue, socket should timeout
recvd = messaging.recv_sock(sub_sock)
@@ -129,7 +111,6 @@ class TestMessaging:
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=1000)
zmq_sleep()
# no msg in queue, socket should timeout
recvd = messaging.recv_one(sub_sock)
@@ -142,12 +123,10 @@ class TestMessaging:
assert isinstance(recvd, capnp._DynamicStructReader)
assert_carstate(msg.carState, recvd.carState)
@pytest.mark.xfail(condition="ZMQ" in os.environ, reason='ZMQ detected')
def test_recv_one_or_none(self):
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock)
zmq_sleep()
# no msg in queue, socket shouldn't block
recvd = messaging.recv_one_or_none(sub_sock)
@@ -165,16 +144,13 @@ class TestMessaging:
sock_timeout = 0.1
pub_sock = messaging.pub_sock(sock)
sub_sock = messaging.sub_sock(sock, timeout=round(sock_timeout*1000))
zmq_sleep()
# this test doesn't work with ZMQ since multiprocessing interrupts it
if "ZMQ" not in os.environ:
# wait 5 socket timeouts and make sure it's still retrying
p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,))
p.start()
time.sleep(sock_timeout*5)
assert p.is_alive()
p.terminate()
# wait 5 socket timeouts and make sure it's still retrying
p = multiprocessing.Process(target=messaging.recv_one_retry, args=(sub_sock,))
p.start()
time.sleep(sock_timeout*5)
assert p.is_alive()
p.terminate()
# wait 5 socket timeouts before sending
msg = random_carstate()
@@ -1,21 +1,16 @@
import random
import time
from typing import Sized, cast
from typing import cast
from collections.abc import Sized
import openpilot.cereal.messaging as messaging
from openpilot.cereal.messaging.tests.test_messaging import events, random_sock, random_socks, \
random_bytes, random_carstate, assert_carstate, \
zmq_sleep
random_bytes, random_carstate, assert_carstate
from openpilot.cereal.services import SERVICE_LIST
class TestSubMaster:
def setup_method(self):
# ZMQ pub socket takes too long to die
# sleep to prevent multiple publishers error between tests
zmq_sleep(3)
def test_init(self):
sm = messaging.SubMaster(events)
for p in [sm.updated, sm.recv_time, sm.recv_frame, sm.alive,
@@ -42,7 +37,6 @@ class TestSubMaster:
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sm = messaging.SubMaster([sock,])
zmq_sleep()
msg = random_carstate()
pub_sock.send(msg.to_bytes())
@@ -54,7 +48,6 @@ class TestSubMaster:
sock = "carState"
pub_sock = messaging.pub_sock(sock)
sm = messaging.SubMaster([sock,])
zmq_sleep()
for i in range(10):
msg = messaging.new_message(sock)
@@ -91,21 +84,13 @@ class TestSubMaster:
for service, (max_freq, min_freq) in checks.items():
if max_freq is not None:
assert min_freq is not None
assert sm._check_avg_freq(service)
assert sm.freq_tracker[service].max_freq == max_freq*1.2
assert sm.freq_tracker[service].min_freq == min_freq*0.8
else:
assert not sm._check_avg_freq(service)
def test_alive(self):
pass
def test_ignore_alive(self):
pass
def test_valid(self):
pass
# SubMaster should always conflate
def test_conflate(self):
sock = "carState"
@@ -124,11 +109,6 @@ class TestSubMaster:
class TestPubMaster:
def setup_method(self):
# ZMQ pub socket takes too long to die
# sleep to prevent multiple publishers error between tests
zmq_sleep(3)
def test_init(self):
messaging.PubMaster(events)
@@ -136,7 +116,6 @@ class TestPubMaster:
socks = random_socks()
pm = messaging.PubMaster(socks)
sub_socks = {s: messaging.sub_sock(s, conflate=True, timeout=1000) for s in socks}
zmq_sleep()
# PubMaster accepts either a capnp msg builder or bytes
for capnp in [True, False]:
+2 -4
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
from enum import IntEnum
from typing import Optional
# TODO: this should be automatically determined using the capnp schema
@@ -11,7 +10,7 @@ class QueueSize(IntEnum):
class Service:
def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None,
def __init__(self, should_log: bool, frequency: float, decimation: int | None = None,
queue_size: QueueSize = QueueSize.SMALL):
self.should_log = should_log
self.frequency = frequency
@@ -125,8 +124,7 @@ def build_header():
for k, v in SERVICE_LIST.items():
should_log = "true" if v.should_log else "false"
decimation = -1 if v.decimation is None else v.decimation
h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \
(k, k, should_log, v.frequency, decimation, v.queue_size)
h += f' {{ "{k}", {{"{k}", {should_log}, {v.frequency:f}, {decimation:d}, {v.queue_size:d}}}}},\n'
h += "};\n"
h += "#endif\n"
+1
View File
@@ -38,6 +38,7 @@ class BaseApi:
}
if payload_extra is not None:
payload.update(payload_extra)
assert self.private_key is not None
token = jwt.encode(payload, self.private_key, algorithm=self.jwt_algorithm)
if isinstance(token, bytes):
token = token.decode('utf8')
+9 -8
View File
@@ -42,25 +42,26 @@ def get_existing_chunks(path):
class ChunkStream(io.RawIOBase):
def __init__(self, paths):
self._paths = iter(paths)
self._buf = memoryview(b'')
self._f = None
def readable(self):
return True
def readinto(self, b):
n = 0
view = memoryview(b)
while n < len(b):
if not self._buf:
if self._f is None:
p = next(self._paths, None)
if p is None:
break
with open(p, 'rb') as f:
self._buf = memoryview(f.read())
self._f = open(p, 'rb')
count = self._f.readinto(view[n:])
if not count:
self._f.close()
self._f = None
continue
take = min(len(b) - n, len(self._buf))
b[n:n + take] = self._buf[:take]
self._buf = self._buf[take:]
n += take
n += count
return n
def open_file_chunked(path):
+10 -10
View File
@@ -56,28 +56,28 @@
},
{
"name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8.img.xz",
"hash": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
"hash_raw": "8806802b195a5b1396a3ae8dd92a8b7711dc522f6aceafd820e871bae5c8a6d8",
"url": "https://commadist.azureedge.net/agnosupdate/boot-19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39.img.xz",
"hash": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
"hash_raw": "19ff57b68e219e4503fcaca716967098d5d0a1de8af833f04dbf13b99aeb4d39",
"size": 17487872,
"sparse": false,
"full_check": true,
"has_ab": true,
"ondevice_hash": "edca8bee1531e66953d107eeceeed2dc7b3ca46417e49d55508f94e58bf95db8"
"ondevice_hash": "ddfe93cc6a8531af92ee331d9bbaeae2f1d933bdb38e579769dc9fe7998eb626"
},
{
"name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img.xz",
"hash": "78acfe16a7b62a3a91fc7a81f40a693e4468cec1c69df7d0b1e550aacc646113",
"hash_raw": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img.xz",
"hash": "4dc41c2c072f5f5d5cd484cd6173049cd96acfb9a67bc20049775585fe881539",
"hash_raw": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
"size": 4718592000,
"sparse": true,
"full_check": false,
"has_ab": true,
"ondevice_hash": "743142c5a898f27b2a1029cca42c8a5d5d1fc0096414422b850fe84c8d0b8342",
"ondevice_hash": "cf1229630b7a2b8497705bca4ba947dbf0c217418ff4febff571aa4f4a878134",
"alt": {
"hash": "ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f",
"url": "https://commadist.azureedge.net/agnosupdate/system-ef0d879302cb29e72110e9c8d3f947c830fd7d37c8192744fc9dbea1af78501f.img",
"hash": "a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724",
"url": "https://commadist.azureedge.net/agnosupdate/system-a396dd98ffd49614fb198d1b022a0c7a6d0a1e563c20ce11b0a975105ab50724.img",
"size": 4718592000
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class StreamingDecompressor:
def __init__(self, url: str) -> None:
self.buf = b""
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': None}, timeout=60)
self.req = requests.get(url, stream=True, headers={'Accept-Encoding': 'identity'}, timeout=60)
self.it = self.req.iter_content(chunk_size=1024 * 1024)
self.decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO)
self.eof = False
+8 -8
View File
@@ -339,7 +339,7 @@ class Tici(HardwareBase):
# Ensure fan gpio is enabled so fan runs until shutdown, also turned on at boot by the ABL
gpio_init(GPIO.SOM_ST_IO, True)
gpio_set(GPIO.SOM_ST_IO, 1)
gpio_set(GPIO.SOM_ST_IO, True)
# *** IRQ config ***
@@ -389,21 +389,21 @@ class Tici(HardwareBase):
gpio_init(GPIO.STM_RST_N, True)
gpio_init(GPIO.STM_BOOT0, True)
gpio_set(GPIO.STM_RST_N, 1)
gpio_set(GPIO.STM_BOOT0, 0)
gpio_set(GPIO.STM_RST_N, True)
gpio_set(GPIO.STM_BOOT0, False)
time.sleep(0.01)
gpio_set(GPIO.STM_RST_N, 0)
gpio_set(GPIO.STM_RST_N, False)
def recover_internal_panda(self):
gpio_init(GPIO.STM_RST_N, True)
gpio_init(GPIO.STM_BOOT0, True)
gpio_set(GPIO.STM_RST_N, 1)
gpio_set(GPIO.STM_BOOT0, 1)
gpio_set(GPIO.STM_RST_N, True)
gpio_set(GPIO.STM_BOOT0, True)
time.sleep(0.01)
gpio_set(GPIO.STM_RST_N, 0)
gpio_set(GPIO.STM_RST_N, False)
time.sleep(0.01)
gpio_set(GPIO.STM_BOOT0, 0)
gpio_set(GPIO.STM_BOOT0, False)
def booted(self):
# this normally boots within 8s, but on rare occasions takes 30+s
+1 -1
View File
@@ -52,7 +52,7 @@ PPPD_CMD = [
"novj", "novjccomp", "ipcp-accept-local", "ipcp-accept-remote", "nomagic",
"user", '""', "password", '""',
]
INITIAL_STATE = {
INITIAL_STATE: dict[str, object] = {
"seconds_since_boot": 0,
"state": "INITIALIZING",
"connected": False, "ip_address": "",
+1
View File
@@ -37,6 +37,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}},
{"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}},
{"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}},
{"DriverLockoutCount", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, INT, "0"}},
{"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}},
{"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}},
{"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}},
+7 -5
View File
@@ -1,11 +1,13 @@
import numpy as np
from numbers import Number
from collections.abc import Sequence
Gain = int | float | tuple[Sequence[float], Sequence[float]] | list[list[float]]
class PIDController:
def __init__(self, k_p, k_i, k_d=0., pos_limit=1e308, neg_limit=-1e308, rate=100):
self._k_p: list[list[float]] = [[0], [k_p]] if isinstance(k_p, Number) else k_p
self._k_i: list[list[float]] = [[0], [k_i]] if isinstance(k_i, Number) else k_i
self._k_d: list[list[float]] = [[0], [k_d]] if isinstance(k_d, Number) else k_d
def __init__(self, k_p: Gain, k_i: Gain, k_d: Gain = 0., pos_limit=1e308, neg_limit=-1e308, rate=100):
self._k_p = ([0], [k_p]) if isinstance(k_p, (int, float)) else k_p
self._k_i = ([0], [k_i]) if isinstance(k_i, (int, float)) else k_i
self._k_d = ([0], [k_d]) if isinstance(k_d, (int, float)) else k_d
self.set_limits(pos_limit, neg_limit)
+2 -2
View File
@@ -27,7 +27,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
self.log_files = self.get_existing_logfiles()
log_indexes = [f.split(".")[-1] for f in self.log_files]
self.last_file_idx = max([int(i) for i in log_indexes if i.isdigit()] or [-1])
self.last_rollover = None
self.last_rollover = 0.0
self.doRollover()
def _open(self):
@@ -39,7 +39,7 @@ class SwaglogRotatingFileHandler(BaseRotatingHandler):
return stream
def get_existing_logfiles(self):
log_files = list()
log_files = []
base_dir = os.path.dirname(self.base_filename)
for fn in os.listdir(base_dir):
fp = os.path.join(base_dir, fn)
+3 -3
View File
@@ -112,14 +112,14 @@ class TestParams:
def test_params_default_value(self):
self.params.remove("LanguageSetting")
self.params.remove("LongitudinalPersonality")
self.params.remove("LiveParameters")
self.params.remove("LiveParametersV2")
assert self.params.get("LanguageSetting") is None
assert self.params.get("LanguageSetting", return_default=False) is None
assert isinstance(self.params.get("LanguageSetting", return_default=True), str)
assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int)
assert self.params.get("LiveParameters") is None
assert self.params.get("LiveParameters", return_default=True) is None
assert self.params.get("LiveParametersV2") is None
assert self.params.get("LiveParametersV2", return_default=True) is None
def test_params_get_type(self):
# json
+1 -2
View File
@@ -52,7 +52,7 @@ _ar_ox_config = DeviceCameraConfig(CameraConfig(1928, 1208, 2648.0), _ar_ox_fish
_os_config = DeviceCameraConfig(CameraConfig(2688 // 2, 1520 // 2, 1522.0 * 3 / 4), _os_fisheye, _os_fisheye)
_neo_config = DeviceCameraConfig(CameraConfig(1164, 874, 910.0), CameraConfig(816, 612, 650.0), _NoneCameraConfig())
DEVICE_CAMERAS = {
DEVICE_CAMERAS: dict[tuple[str, str], DeviceCameraConfig] = {
# A "device camera" is defined by a device type and sensor
# sensor type was never set on eon/neo/two
@@ -176,4 +176,3 @@ def img_from_device(pt_device):
pt_img = pt_view/pt_view[:, 2:3]
return pt_img.reshape(input_shape)[:, :2]
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3027da9834adf9c71177bc3086f8de3018ce22e4725b694d635723c5b3c860cd
size 97120
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca29ff46fbd6d00cc02596de8c9abfbf1cacdc6a7b2f98b27a74d24c790da004
size 52374
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42bd04a57b527c787a0555503e02a203f7d672c12d448769a3f41f17befbf013
size 48044
oid sha256:6f47633b5082b911e79dd13fc2d1f4c2a9d44d2fe0860f3d390ffc4e3d772f7d
size 97120
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:538271820d618046009bb456e930598f2736dd1a6174c11925ec3912581b05c1
size 52374
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:634fad6590295ef2e73d5f7e5aecbb5dd245a0f7692f1e300c2ebac95aabb8e3
size 73026
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b1e177499d9439367179cc57a6301b6162393972e3a136cc35c5fdac026bf10a
size 48044
oid sha256:a380fe0a022856b302841a0dd19b71eab318c6b08ec851f128942758a7f1631a
size 97120
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2c0af7f5fe57bb36ab96fae868e20feca763541c97a61b3a3a84a0e7fcb81163
size 83350
oid sha256:cc9e67cfaba77e8e4f4049f0add9238660e4cf8f886c6915600d8f47a30c98db
size 97120
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad19268e4aaaeac8dd21f6b26c16a121e7b3f50bba867748e7226727643ae682
size 144642
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:412ef25d2fb103c1ebd55c667313a5921493305fb4e1f4e1dafc08d3b95d86ab
size 73026
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4af81cbf1d96a42cc351878b015298aee82874b46baaf1a615ca91ec36c0ced6
size 83228
oid sha256:081d64ca28a84a59ff5e6aea4732865b8687585938b8a287bc7e6a49d0646508
size 97120
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1bb440f424c989c06e203bfba0b32d39aede8f6c78a9b335e5e778460202b601
size 73026
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a390831afca3bfc6ea3c2739b872ebf866e70df8ae30653f8587e5cd3993959
size 68306
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67e636d072703e6b1233a12344c0a6304fd43d64dbb31c66b71c2c8870a339c1
size 153764
+4 -3
View File
@@ -3,7 +3,7 @@ import copy
import os
import pytest
import random
import unittest # noqa: TID251
import unittest
from collections import defaultdict, Counter
import hypothesis.strategies as st
from hypothesis import Phase, given, settings
@@ -22,6 +22,7 @@ from openpilot.selfdrive.pandad import can_capnp_to_list
from openpilot.selfdrive.test.helpers import read_segment_list
from openpilot.common.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT
from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source
from openpilot.tools.lib.file_sources import Source
from openpilot.tools.lib.route import SegmentName
SafetyModel = car.CarParams.SafetyModel
@@ -131,7 +132,7 @@ class TestCarModelBase(unittest.TestCase):
segment_range = f"{cls.test_route.route}/{seg}"
try:
sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
sources: list[Source] = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source]
lr = LogReader(segment_range, sources=sources, sort_by_time=True)
return cls.get_testing_data_from_logreader(lr)
except (LogsUnavailable, AssertionError):
@@ -257,7 +258,7 @@ class TestCarModelBase(unittest.TestCase):
# Don't check relay malfunction on disabled routes (relay closed),
# or before fingerprinting is done (elm327 and noOutput)
if self.openpilot_enabled and t / 1e4 > self.car_safety_mode_frame:
if self.car_safety_mode_frame is not None and t / 1e4 > self.car_safety_mode_frame:
self.assertFalse(self.safety.get_relay_malfunction())
else:
self.safety.set_relay_malfunction(False)
@@ -147,6 +147,7 @@ class Controls(ControlsExt):
lat_delay = self.sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
actuators.curvature = self.desired_curvature
assert self.calibrated_pose is not None
steer, lateral_output, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp,
self.steer_limited_by_safety, self.desired_curvature,
self.calibrated_pose, curvature_limited, lat_delay)
@@ -15,6 +15,9 @@ MAX_LATERAL_JERK = 5.0 # m/s^3
MAX_LATERAL_ACCEL_NO_ROLL = 3.0 # m/s^2
def should_stop(v_ego: float, a_target: float) -> bool:
return bool(v_ego < 0.25 and a_target < 0.1)
def clamp(val, min_val, max_val):
clamped_val = float(np.clip(val, min_val, max_val))
return clamped_val, clamped_val != val
@@ -40,7 +43,7 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll) -> tuple[float, b
return float(new_curvature), limited_accel or limited_max_curv
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.3):
def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL):
if len(speeds) == len(t_idxs):
v_now = speeds[0]
a_now = accels[0]
@@ -53,8 +56,7 @@ def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.
v_now = 0.0
v_target = 0.0
a_target = 0.0
should_stop = (v_now < vEgoStopping and a_target < 0.1)
return a_target, should_stop
return a_target, should_stop(v_now, a_target)
def curv_from_psis(psi_target, psi_rate, vego, action_t):
vego = np.clip(vego, MIN_SPEED, np.inf)
@@ -10,7 +10,7 @@ CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N]
LongCtrlState = car.CarControl.Actuators.LongControlState
def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego,
def long_control_state_trans(CP_SP, active, long_control_state,
should_stop, brake_pressed, cruise_standstill):
# Gas Interceptor
cruise_standstill = cruise_standstill and not CP_SP.enableGasInterceptor
@@ -26,22 +26,17 @@ def long_control_state_trans(CP, CP_SP, active, long_control_state, v_ego,
if long_control_state == LongCtrlState.off:
if not starting_condition:
long_control_state = LongCtrlState.stopping
elif CP.startingState:
long_control_state = LongCtrlState.starting
else:
long_control_state = LongCtrlState.pid
elif long_control_state == LongCtrlState.stopping:
if starting_condition and CP.startingState:
long_control_state = LongCtrlState.starting
elif starting_condition:
if starting_condition:
long_control_state = LongCtrlState.pid
elif long_control_state in [LongCtrlState.starting, LongCtrlState.pid]:
elif long_control_state == LongCtrlState.pid:
if should_stop:
long_control_state = LongCtrlState.stopping
elif v_ego > CP.vEgoStarting:
long_control_state = LongCtrlState.pid
return long_control_state
class LongControl:
@@ -62,7 +57,7 @@ class LongControl:
self.pid.neg_limit = accel_limits[0]
self.pid.pos_limit = accel_limits[1]
self.long_control_state = long_control_state_trans(self.CP, self.CP_SP, active, self.long_control_state, CS.vEgo,
self.long_control_state = long_control_state_trans(self.CP_SP, active, self.long_control_state,
should_stop, CS.brakePressed,
CS.cruiseState.standstill)
if self.long_control_state == LongCtrlState.off:
@@ -73,11 +68,8 @@ class LongControl:
output_accel = self.last_output_accel
if output_accel > self.CP.stopAccel:
output_accel = min(output_accel, 0.0)
output_accel -= self.CP.stoppingDecelRate * DT_CTRL
self.reset()
elif self.long_control_state == LongCtrlState.starting:
output_accel = self.CP.startAccel
# TODO: can we just go straight to stopAccel?
output_accel -= 1.0 * DT_CTRL # m/s^2/s while trying to stop
self.reset()
else: # LongCtrlState.pid
@@ -137,7 +137,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
action_t = self.CP.longitudinalActuatorDelay + DT_MDL
output_a_target_mpc, output_should_stop_mpc = get_accel_from_plan(self.v_desired_trajectory, self.a_desired_trajectory, CONTROL_N_T_IDX,
action_t=action_t, vEgoStopping=self.CP.vEgoStopping)
action_t=action_t)
output_a_target_e2e = sm['modelV2'].action.desiredAcceleration
output_should_stop_e2e = sm['modelV2'].action.shouldStop
@@ -1,60 +1,43 @@
from openpilot.cereal import custom
from opendbc.car.structs import car
from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState, long_control_state_trans
class TestLongControlStateTransition:
def test_stay_stopped(self):
CP = car.CarParams.new_message()
CP_SP = custom.CarParamsSP.new_message()
active = True
current_state = LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=True, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=True, cruise_standstill=False)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=True)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
active = False
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.off
def test_engage():
CP = car.CarParams.new_message()
CP_SP = custom.CarParamsSP.new_message()
active = True
current_state = LongCtrlState.off
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=True, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=True, cruise_standstill=False)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=True)
assert next_state == LongCtrlState.stopping
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
def test_starting():
CP = car.CarParams.new_message(startingState=True, vEgoStarting=0.5)
CP_SP = custom.CarParamsSP.new_message()
active = True
current_state = LongCtrlState.starting
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=0.1,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.starting
next_state = long_control_state_trans(CP, CP_SP, active, current_state, v_ego=1.0,
next_state = long_control_state_trans(CP_SP, active, current_state,
should_stop=False, brake_pressed=False, cruise_standstill=False)
assert next_state == LongCtrlState.pid
@@ -7,8 +7,6 @@ from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS
np.random.seed(0)
LA_ERR_STD = 1.0
INPUT_NOISE_STD = 0.08
V_EGO = 30.0
@@ -58,16 +56,17 @@ def simulate_straight_road_msgs(est):
for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)):
est.handle_log(t, which, msg)
def test_estimated_offset():
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
msg = est.get_msg()
# TODO add lataccelfactor and friction check when we have more accurate estimates
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
class TestTorquedLatAccelOffset:
def test_estimated_offset(self):
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
msg = est.get_msg()
# TODO add lataccelfactor and friction check when we have more accurate estimates
assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1
def test_straight_road_roll_bias():
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
simulate_straight_road_msgs(est)
msg = est.get_msg()
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
def test_straight_road_roll_bias(self):
steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD)
est = get_warmed_up_estimator(steer_torques, lat_accels)
simulate_straight_road_msgs(est)
msg = est.get_msg()
assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw)
+5 -3
View File
@@ -1,4 +1,5 @@
import numpy as np
from collections.abc import Sequence
from typing import Any
from functools import cache
@@ -68,7 +69,8 @@ class NPQueue:
class PointBuckets:
def __init__(self, x_bounds: list[tuple[float, float]], min_points: list[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None:
def __init__(self, x_bounds: list[tuple[float, float]], min_points: Sequence[float], min_points_total: int, points_per_bucket: int, rowsize: int) -> None:
self._rng = np.random.default_rng()
self.x_bounds = x_bounds
self.buckets = {bounds: NPQueue(maxlen=points_per_bucket, rowsize=rowsize) for bounds in x_bounds}
self.buckets_min_points = dict(zip(x_bounds, min_points, strict=True))
@@ -98,9 +100,9 @@ class PointBuckets:
points = np.vstack([x.arr for x in self.buckets.values()])
if num_points is None:
return points
return points[np.random.choice(np.arange(len(points)), min(len(points), num_points), replace=False)]
return points[self._rng.choice(np.arange(len(points)), min(len(points), num_points), replace=False)]
def load_points(self, points: list[list[float]]) -> None:
def load_points(self, points: Sequence[Sequence[float]]) -> None:
for point in points:
self.add_point(*point)
+1 -1
View File
@@ -66,7 +66,7 @@ class LocationEstimator:
self.observations = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds}
self.observation_errors = {kind: np.zeros(3, dtype=np.float32) for kind in obs_kinds}
def reset(self, t: float, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P):
def reset(self, t: float | None, x_initial: np.ndarray = PoseKalman.initial_x, P_initial: np.ndarray = PoseKalman.initial_P):
self.kf.init_state(x_initial, covs=P_initial, filter_time=t)
def _validate_sensor_source(self, source: log.SensorEventData.SensorSource):
-21
View File
@@ -200,25 +200,6 @@ def check_valid_with_hysteresis(current_valid: bool, val: float, threshold: floa
return current_valid
# TODO: Remove this function after few releases (added in 0.9.9)
def migrate_cached_vehicle_params_if_needed(params: Params):
last_parameters_data_old = params.get("LiveParameters")
last_parameters_data = params.get("LiveParametersV2")
if last_parameters_data_old is None or last_parameters_data is not None:
return
try:
last_parameters_msg = messaging.new_message('liveParameters')
last_parameters_msg.liveParameters.valid = True
last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio']
last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor']
last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg']
params.put("LiveParametersV2", last_parameters_msg.to_bytes(), block=True)
except Exception as e:
cloudlog.error(f"Failed to perform parameter migration: {e}")
params.remove("LiveParameters")
def retrieve_initial_vehicle_params(params: Params, CP: car.CarParams, replay: bool, debug: bool):
last_parameters_data = params.get("LiveParametersV2")
last_carparams_data = params.get("CarParamsPrevRoute")
@@ -273,8 +254,6 @@ def main():
params = Params()
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
migrate_cached_vehicle_params_if_needed(params)
steer_ratio, stiffness_factor, angle_offset_deg, pInitial = retrieve_initial_vehicle_params(params, CP, REPLAY, DEBUG)
learner = VehicleParamsLearner(CP, steer_ratio, stiffness_factor, np.radians(angle_offset_deg), pInitial)
@@ -81,6 +81,7 @@ class TestLagd:
assert retrieve_initial_lag(params, CP) is None
def test_ncc(self):
rng = np.random.default_rng()
lag_frames = random.randint(1, 19)
desired_sig = np.sin(np.arange(0.0, 10.0, 0.1))
@@ -91,15 +92,15 @@ class TestLagd:
assert np.argmax(corr) == lag_frames
# add some noise
desired_sig += np.random.normal(0, 0.05, len(desired_sig))
actual_sig += np.random.normal(0, 0.05, len(actual_sig))
desired_sig += rng.normal(0, 0.05, len(desired_sig))
actual_sig += rng.normal(0, 0.05, len(actual_sig))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
# mask out 40% of the values, and make them noise
mask = np.random.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
desired_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
actual_sig[~mask] = np.random.normal(0, 1, size=np.sum(~mask))
mask = rng.choice([True, False], size=len(desired_sig), p=[0.6, 0.4])
desired_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
actual_sig[~mask] = rng.normal(0, 1, size=np.sum(~mask))
corr = masked_normalized_cross_correlation(desired_sig, actual_sig, mask, 200)[len(desired_sig) - 1:len(desired_sig) + 20]
assert np.argmax(corr) in range(lag_frames - MAX_ERR_FRAMES, lag_frames + MAX_ERR_FRAMES + 1)
@@ -37,9 +37,9 @@ def get_select_fields_data(logs):
def sig_smooth(signal):
return masked_symmetric_moving_average(signal, np.ones_like(signal), 5, 1.0)
def get_nested_keys(msg, keys):
val = None
val = msg
for key in keys:
val = getattr(msg if val is None else val, key) if isinstance(key, str) else val[key]
val = getattr(val, key) if isinstance(key, str) else val[key]
return val
lp = [x.livePose for x in logs if x.which() == 'livePose']
data = defaultdict(list)
@@ -2,7 +2,7 @@ import random
import numpy as np
from openpilot.cereal import messaging
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed
from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params
from openpilot.selfdrive.locationd.models.car_kf import CarKalman
from openpilot.selfdrive.locationd.test.test_locationd_scenarios import TEST_ROUTE
from openpilot.selfdrive.test.process_replay.migration import migrate, migrate_carParams
@@ -30,38 +30,9 @@ class TestParamsd:
params.put("LiveParametersV2", msg.to_bytes(), block=True)
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
migrate_cached_vehicle_params_if_needed(params) # this is not tested here but should not mess anything up or throw an error
sr, sf, offset, p_init = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
np.testing.assert_equal(p_init.shape, CarKalman.P_initial.shape)
np.testing.assert_allclose(np.diagonal(p_init), msg.liveParameters.debugFilterState.std)
# TODO Remove this test after the support for old format is removed
def test_read_saved_params_old_format(self):
params = Params()
lr = migrate(LogReader(TEST_ROUTE), [migrate_carParams])
CP = next(m for m in lr if m.which() == "carParams").carParams
msg = get_random_live_parameters(CP)
params.put("LiveParameters", msg.liveParameters.to_dict(), block=True)
params.put("CarParamsPrevRoute", CP.as_builder().to_bytes(), block=True)
params.remove("LiveParametersV2")
migrate_cached_vehicle_params_if_needed(params)
sr, sf, offset, _ = retrieve_initial_vehicle_params(params, CP, replay=True, debug=True)
np.testing.assert_allclose(sr, msg.liveParameters.steerRatio)
np.testing.assert_allclose(sf, msg.liveParameters.stiffnessFactor)
np.testing.assert_allclose(offset, msg.liveParameters.angleOffsetAverageDeg)
assert params.get("LiveParametersV2") is not None
def test_read_saved_params_corrupted_old_format(self):
params = Params()
params.put("LiveParameters", {}, block=True)
params.remove("LiveParametersV2")
migrate_cached_vehicle_params_if_needed(params)
assert params.get("LiveParameters") is None
assert params.get("LiveParametersV2") is None
@@ -2,24 +2,25 @@ from opendbc.car.structs import car
from openpilot.selfdrive.locationd.torqued import TorqueEstimator
def test_cal_percent():
est = TorqueEstimator(car.CarParams())
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 0
class TestTorqued:
def test_cal_percent(self):
est = TorqueEstimator(car.CarParams())
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 0
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
est.filtered_points.buckets_min_points.values(), strict=True):
for _ in range(int(min_pts)):
est.filtered_points.add_point((low + high) / 2.0, 0.0)
for (low, high), min_pts in zip(est.filtered_points.buckets.keys(),
est.filtered_points.buckets_min_points.values(), strict=True):
for _ in range(int(min_pts)):
est.filtered_points.add_point((low + high) / 2.0, 0.0)
# enough bucket points, but not enough total points
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
# enough bucket points, but not enough total points
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == (len(est.filtered_points) / est.min_points_total * 100 + 100) / 2
# add enough points to bucket with most capacity
key = list(est.filtered_points.buckets)[0]
for _ in range(est.min_points_total - len(est.filtered_points)):
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
# add enough points to bucket with most capacity
key = list(est.filtered_points.buckets)[0]
for _ in range(est.min_points_total - len(est.filtered_points)):
est.filtered_points.add_point((key[0] + key[1]) / 2.0, 0.0)
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 100
msg = est.get_msg()
assert msg.liveTorqueParameters.calPerc == 100
+5 -4
View File
@@ -62,14 +62,14 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
self.lag = 0.0
self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters
if decimated:
self.min_bucket_points = MIN_BUCKET_POINTS / 10
self.min_bucket_points: list[float] = (MIN_BUCKET_POINTS / 10).tolist()
self.min_points_total = MIN_POINTS_TOTAL_QLOG
self.fit_points = FIT_POINTS_TOTAL_QLOG
self.factor_sanity = FACTOR_SANITY_QLOG
self.friction_sanity = FRICTION_SANITY_QLOG
else:
self.min_bucket_points = MIN_BUCKET_POINTS
self.min_bucket_points = MIN_BUCKET_POINTS.tolist()
self.min_points_total = MIN_POINTS_TOTAL
self.fit_points = FIT_POINTS_TOTAL
self.factor_sanity = FACTOR_SANITY
@@ -120,9 +120,10 @@ class TorqueEstimator(ParameterEstimator, TorqueEstimatorExt):
'latAccelOffset': cache_ltp.latAccelOffsetFiltered,
'frictionCoefficient': cache_ltp.frictionCoefficientFiltered
}
initial_params['points'] = cache_ltp.points
cached_points: list[list[float]] = [list(point) for point in cache_ltp.points]
initial_params['points'] = cached_points
self.decay = cache_ltp.decay
self.filtered_points.load_points(initial_params['points'])
self.filtered_points.load_points(cached_points)
cloudlog.info("restored torque params from cache")
except Exception:
cloudlog.exception("failed to restore cached torque params")
+6 -2
View File
@@ -13,6 +13,7 @@ from collections import namedtuple
import numpy as np
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
def _patch_tinygrad_fetch_fw():
import hashlib
@@ -242,7 +243,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
SEED = 42
def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_queues(Device.DEFAULT)
np.random.seed(seed)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None
@@ -250,7 +251,7 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues):
for i in range(n_runs):
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
Device.default.synchronize()
random_inputs = make_random_inputs()
st = time.perf_counter()
@@ -311,6 +312,9 @@ if __name__ == "__main__":
p.add_argument('--frame-skip', type=int, required=True)
args = p.parse_args()
if 'USB+AMD' in os.environ.get('DEV', ''):
wait_usbgpu_link()
model_path = read_file_chunked_to_disk(args.onnx)
model_w, model_h = args.model_size
+4 -1
View File
@@ -25,6 +25,7 @@ from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_drivi
from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
@@ -94,7 +95,7 @@ class ModelState(ModelStateBase):
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.full_frames: dict[str, Tensor] = {}
self._blob_cache: dict[int, Tensor] = {}
self._blob_cache: dict[tuple[str, int], Tensor] = {}
self.parser = Parser()
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
self.run_policy = jits['run_policy']
@@ -173,6 +174,8 @@ def main(demo=False):
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
if USBGPU:
wait_usbgpu_link()
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
@@ -41,7 +41,7 @@ class Parser:
raw = outs[name]
outs[name] = sigmoid(raw)
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None):
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=()):
if self.check_missing(outs, name):
return
raw = outs[name]
+34
View File
@@ -0,0 +1,34 @@
import time
from pathlib import Path
from openpilot.common.swaglog import cloudlog
from openpilot.common.hardware.usb import CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID, usb_devices, controller, read_int
STABLE_SECONDS = 2.0
STABLE_THRESHOLD = 5.0 # link errors per second
def _chestnut_portli() -> Path | None:
for device in usb_devices():
if read_int(device / "idVendor", 16) == CHESTNUT_VENDOR_ID and \
read_int(device / "idProduct", 16) == CHESTNUT_PRODUCT_ID:
ctrl = controller(device)
if ctrl is not None and (ctrl / "portli").exists():
return ctrl / "portli"
return None
def wait_usbgpu_link(timeout: float = 30.0) -> None:
portli = _chestnut_portli()
if portli is None:
return
t0 = time.monotonic()
while time.monotonic() - t0 < timeout:
start = read_int(portli, 0)
time.sleep(STABLE_SECONDS)
rate = (read_int(portli, 0) - start) / STABLE_SECONDS
if rate <= STABLE_THRESHOLD:
return
cloudlog.warning(f"usbgpu link not stable: {rate:.0f} errors/s")
cloudlog.error("usbgpu link never stabilized")
+19 -11
View File
@@ -41,7 +41,7 @@ class DRIVER_MONITOR_SETTINGS:
# lockout specs
self._MAX_ALERT_3 = 2
self._MAX_NO_RESPONSE = 1
self._LOCKOUT_TIME = int(1800 / DT_DMON)
self._LOCKOUT_TIMES = [int(60 * n_min / DT_DMON) for n_min in [1, 5, 15, 30]]
self._TIMEOUT_RECOVERY_FACTOR_MAX = 5.
self._TIMEOUT_RECOVERY_FACTOR_MIN = 1.25
@@ -152,7 +152,10 @@ class DriverMonitoring:
self.cnt_since_alert_3 = 0
self.no_response_timeout = int(self.settings._NO_RESPONSE_TIMEOUT / DT_DMON)
self.no_response_cnt = 0
self.lockout_time = 0
self.lockout_active = Params().get_bool("DriverTooDistracted")
self.lockout_count = Params().get("DriverLockoutCount") or 0
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(max(self.lockout_count - 1, 0), len(self.settings._LOCKOUT_TIMES) - 1)]
self.lockout_time_elapsed = 0
self.step_change = 0.
self.active_policy = MonitoringPolicy.vision
self.driver_interacting = False
@@ -163,7 +166,6 @@ class DriverMonitoring:
self.threshold_alert_2 = 0.
self.dcam_uncertain_cnt = 0
self.dcam_reset_cnt = 0
self.too_distracted = Params().get_bool("DriverTooDistracted")
self._reset_awareness()
self._set_policy(MonitoringPolicy.vision)
@@ -310,16 +312,20 @@ class DriverMonitoring:
self.driver_interacting = driver_engaged
if self.alert_3_cnt >= self.settings._MAX_ALERT_3 or self.no_response_cnt >= self.settings._MAX_NO_RESPONSE:
self.too_distracted = True
if not self.lockout_active:
self.lockout_count += 1
self.lockout_duration = self.settings._LOCKOUT_TIMES[min(self.lockout_count - 1, len(self.settings._LOCKOUT_TIMES) - 1)]
Params().put("DriverLockoutCount", self.lockout_count)
self.lockout_active = True
if self.too_distracted:
self.lockout_time += 1
if self.lockout_time > self.settings._LOCKOUT_TIME:
self.too_distracted = False
if self.lockout_active:
self.lockout_time_elapsed += 1
if self.lockout_time_elapsed > self.lockout_duration:
self.lockout_active = False
self.alert_3_cnt = 0
self.cnt_since_alert_3 = 0
self.no_response_cnt = 0
self.lockout_time = 0
self.lockout_time_elapsed = 0
always_on_valid = self.always_on and not wrong_gear
if (self.driver_interacting and self.awareness > 0 and self.active_policy == MonitoringPolicy.wheeltouch) or \
@@ -379,8 +385,10 @@ class DriverMonitoring:
dat = messaging.new_message('driverMonitoringState', valid=valid)
dm = dat.driverMonitoringState
dm.lockout = self.too_distracted
dm.lockoutRecoveryPercent = to_percent(self.lockout_time / self.settings._LOCKOUT_TIME)
dm.lockout = self.lockout_active
dm.lockoutCount = self.lockout_count
if self.lockout_active:
dm.lockoutMinutesRemaining = max(1, round((self.lockout_duration - self.lockout_time_elapsed) * DT_DMON / 60.))
dm.alert3Count = self.alert_3_cnt
dm.noResponseCount = self.no_response_cnt
dm.noResponseForceDecel = self.alert_level == AlertLevel.three and self.cnt_since_alert_3 >= self.no_response_timeout
@@ -86,21 +86,17 @@ class TestMonitoring:
# engaged, distracted past red and beyond the no-response window -> unavailability response + lockout
def test_distracted_lockout(self):
alert_lvls, d_status = self._run_seq(always_distracted, always_false, always_true, always_false)
s = d_status.settings
assert alert_lvls[int(DISTRACTED_SECONDS_TO_RED / DT_DMON)] == 3
assert d_status.alert_3_cnt == 1
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
assert d_status.too_distracted
assert d_status.lockout_time > 0
assert d_status.lockout_active
assert d_status.lockout_time_elapsed > 0
assert d_status.lockout_count >= 1
# no face -> wheeltouch red, sustained past the no-response timeout -> unavailability response + lockout
def test_invisible_lockout(self):
_, d_status = self._run_seq(always_no_face, always_false, always_true, always_false)
s = d_status.settings
assert d_status.active_policy == log.DriverMonitoringState.MonitoringPolicy.wheeltouch
assert d_status.alert_3_cnt == 1
assert d_status.no_response_cnt == s._MAX_NO_RESPONSE
assert d_status.too_distracted
assert d_status.lockout_active
assert d_status.lockout_count >= 1
# engaged, no face detected the whole time, no action
def test_fully_invisible_driver(self):
@@ -275,7 +271,7 @@ def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas,
captured['op_engaged'] = op_engaged
return orig(driver_engaged, op_engaged, lowspeed, wrong_gear)
dm._update_events = spy
object.__setattr__(dm, '_update_events', spy)
dm.run_step(sm, demo=False)
assert captured['op_engaged'] == expected_op_engaged
assert captured['driver_engaged'] == expected_driver_engaged
@@ -60,7 +60,7 @@ class TestPandad:
def test_in_reset(self):
gpio_init(GPIO.STM_RST_N, True)
gpio_set(GPIO.STM_RST_N, 1)
gpio_set(GPIO.STM_RST_N, True)
assert not Panda.list()
self._run_test()
@@ -38,10 +38,10 @@ class TestBoarddSpi:
total_recv_count = 0
total_sent_count = 0
sent_msgs = {bus: list() for bus in range(3)}
sent_msgs = {bus: [] for bus in range(3)}
st = time.monotonic()
ts = {s: list() for s in socks.keys()}
ts = {s: [] for s in socks.keys()}
for _ in range(int(os.getenv("TEST_TIME", "20"))):
# send some CAN messages
if not JUNGLE_SPAM:
+2 -4
View File
@@ -7,9 +7,8 @@ from opendbc.car.structs import car
import openpilot.cereal.messaging as messaging
from openpilot.common.constants import CV
from openpilot.common.git import get_short_branch
from openpilot.common.realtime import DT_CTRL, DT_DMON
from openpilot.common.realtime import DT_CTRL
from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER
from openpilot.selfdrive.monitoring.policy import DRIVER_MONITOR_SETTINGS
from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER
from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION
from openpilot.common.hardware import HARDWARE
@@ -25,7 +24,6 @@ VisualAlert = car.CarControl.HUDControl.VisualAlert
AudibleAlert = log.SelfdriveState.AudibleAlert
EventName = log.OnroadEvent.EventName
DMON_LOCKOUT_TIME = DRIVER_MONITOR_SETTINGS()._LOCKOUT_TIME
# get event name from enum
EVENT_NAME = {v: k for k, v in EventName.schema.enumerants.items()}
@@ -101,7 +99,7 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag
def too_distracted_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert:
if sm['driverMonitoringState'].lockout:
mins_left = max(1, round((100 - sm['driverMonitoringState'].lockoutRecoveryPercent) / 100 * DMON_LOCKOUT_TIME * DT_DMON / 60.))
mins_left = sm['driverMonitoringState'].lockoutMinutesRemaining
return NoEntryAlert("Too Distracted", f"{mins_left} minute{'s' if mins_left != 1 else ''} Left", priority=Priority.HIGH)
return NoEntryAlert("Pay Attention to Engage", priority=Priority.HIGH)
@@ -13,11 +13,11 @@ ALL_STATES = tuple(State.schema.enumerants.values())
ENABLE_EVENT_TYPES = (ET.ENABLE, ET.PRE_ENABLE, ET.OVERRIDE_LATERAL, ET.OVERRIDE_LONGITUDINAL)
def make_event(event_types):
event = {}
def make_event(event_types: list[str | None]):
EVENTS[0] = {}
for ev in event_types:
event[ev] = NormalPermanentAlert("alert")
EVENTS[0] = event
if ev is not None:
EVENTS[0][ev] = NormalPermanentAlert("alert")
return 0
+1 -1
View File
@@ -7,7 +7,7 @@ from openpilot.common.utils import tabulate
DEMO_ROUTE = "5beb9b58bd12b691/0000010a--a51155e496"
MB = 1024 * 1024
TABULATE_OPTS = dict(tablefmt="simple_grid", stralign="center", numalign="center")
TABULATE_OPTS = {"tablefmt": "simple_grid", "stralign": "center", "numalign": "center"}
def _get_procs():
@@ -297,7 +297,7 @@ def migrate_carOutput(msgs):
co = messaging.new_message('carOutput')
co.valid = msg.valid
co.logMonoTime = msg.logMonoTime
co.carOutput.actuatorsOutput = msg.carControl.actuatorsOutputDEPRECATED
co.carOutput.actuatorsOutput = msg.carControl.deprecated.actuatorsOutput
add_ops.append(as_reader(co))
return [], add_ops, []
@@ -323,10 +323,10 @@ def migrate_pandaStates(msgs):
safety_param = safety_param_migration[fingerprint].value
elif len(CP.safetyConfigs):
safety_param = CP.safetyConfigs[0].safetyParam
if CP.safetyConfigs[0].safetyParamDEPRECATED != 0:
safety_param = CP.safetyConfigs[0].safetyParamDEPRECATED
if CP.safetyConfigs[0].deprecated.safetyParam != 0:
safety_param = CP.safetyConfigs[0].deprecated.safetyParam
else:
safety_param = CP.safetyParamDEPRECATED
safety_param = CP.deprecated.safetyParam
ops = []
for index, msg in msgs:
@@ -216,8 +216,7 @@ class ProcessContainer:
def _start_process(self):
if self.capture is not None:
self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher)
self.process.prepare()
self.process.launcher = LauncherWithCapture(self.capture, self.process.launcher) # ty: ignore[invalid-assignment] # intentional wrapper
self.process.start()
def start(
@@ -635,10 +634,10 @@ def replay_process(
fingerprint: str | None = None, return_all_logs: bool = False, custom_params: dict[str, Any] | None = None,
captured_output_store: dict[str, dict[str, str]] | None = None, disable_progress: bool = False
) -> list[capnp._DynamicStructReader]:
if isinstance(cfg, Iterable):
cfgs = list(cfg)
else:
if isinstance(cfg, ProcessConfig):
cfgs = [cfg]
else:
cfgs = list(cfg)
all_msgs = migrate_all(lr,
manager_states=True,
+7 -4
View File
@@ -204,7 +204,10 @@ class FaceAnimator:
frames_back = round(rewind_elapsed / self._animation.frame_duration)
frame_index = self._rewind_from - frames_back
if frame_index <= 0:
return self._switch_to_next(now)
if self._next is None:
self._rewinding = False
return self._animation.frames[0]
return self._switch_to_next(now, self._next)
return self._animation.frames[frame_index]
# Play starting frames first (once)
@@ -223,7 +226,7 @@ class FaceAnimator:
if self._next is not None:
if frame_index == 0 and (len(self._animation.frames) == 1 or self._seen_nonzero):
return self._switch_to_next(now)
return self._switch_to_next(now, self._next)
# No natural return to frame 0 — start rewinding
if self._animation.mode in (AnimationMode.ONCE_FORWARD, AnimationMode.REPEAT_FORWARD):
self._rewinding = True
@@ -232,8 +235,8 @@ class FaceAnimator:
return self._animation.frames[frame_index]
def _switch_to_next(self, now: float) -> list[tuple[int, int]]:
self._animation = self._next
def _switch_to_next(self, now: float, animation: Animation) -> list[tuple[int, int]]:
self._animation = animation
self._next = None
self._rewinding = False
self._seen_nonzero = False
+5 -1
View File
@@ -34,7 +34,11 @@ class MainLayout(Widget):
# Initialize layouts
self._home_layout = HomeLayout()
self._home_body_layout = BodyLayout()
self._layouts = {MainState.HOME: self._home_layout, MainState.SETTINGS: SettingsLayout(), MainState.ONROAD: AugmentedRoadView()}
self._layouts: dict[MainState, Widget] = {
MainState.HOME: self._home_layout,
MainState.SETTINGS: SettingsLayout(),
MainState.ONROAD: AugmentedRoadView(),
}
self._sidebar_rect = rl.Rectangle(0, 0, 0, 0)
self._content_rect = rl.Rectangle(0, 0, 0, 0)
@@ -105,7 +105,6 @@ class DeviceLayout(Widget):
self._params.remove("CalibrationParams")
self._params.remove("LiveTorqueParameters")
self._params.remove("LiveParameters")
self._params.remove("LiveParametersV2")
self._params.remove("LiveDelay")
self._params.put_bool("OnroadCycleRequested", True, block=True)
@@ -1,5 +1,5 @@
import pyray as rl
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import IntEnum
from collections.abc import Callable
from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout
@@ -43,7 +43,7 @@ class PanelType(IntEnum):
class PanelInfo:
name: str
instance: Widget
button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
button_rect: rl.Rectangle = field(default_factory=lambda: rl.Rectangle(0, 0, 0, 0))
class SettingsLayout(Widget):
+1 -1
View File
@@ -68,7 +68,7 @@ class Sidebar(Widget, SidebarSP):
def __init__(self):
Widget.__init__(self)
SidebarSP.__init__(self)
self._net_type = NETWORK_TYPES.get(NetworkType.none)
self._net_type = NETWORK_TYPES[NetworkType.none]
self._net_strength = 0
self._temp_status = MetricData(tr_noop("TEMP"), tr_noop("GOOD"), Colors.GOOD)
+1 -1
View File
@@ -251,7 +251,7 @@ class MiciHomeLayout(Widget):
self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled)
self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled)
self._mic_icon.set_visible(ui_state.recording_audio)
self._body_icon.set_visible(ui_state.is_body)
self._body_icon.set_visible(bool(ui_state.is_body))
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
self._status_bar_layout.render(footer_rect)
+1 -1
View File
@@ -148,4 +148,4 @@ class MiciMainLayout(Scroller):
def _on_body_changed(self):
self._car_onroad_layout.set_visible(not ui_state.is_body)
self._body_onroad_layout.set_visible(ui_state.is_body)
self._body_onroad_layout.set_visible(bool(ui_state.is_body))
@@ -250,12 +250,12 @@ class MiciOffroadAlerts(Scroller):
{alert_data.key: self.params.get(alert_data.key) for alert_data in self.sorted_alerts})
time.sleep(REFRESH_INTERVAL)
def _refresh(self) -> int:
def _refresh(self, pending_params: dict) -> int:
"""Refresh alerts from params and return active count."""
active_count = 0
# Handle UpdateAvailable alert specially
update_available = self._pending_params["UpdateAvailable"]
update_available = pending_params["UpdateAvailable"]
update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None)
if update_alert_data:
@@ -263,7 +263,7 @@ class MiciOffroadAlerts(Scroller):
version_string = ""
# Get new version description and parse version and date
new_desc = self._pending_params["UpdaterNewDescription"] or ""
new_desc = pending_params["UpdaterNewDescription"] or ""
if new_desc:
# format: "version / branch / commit / date"
parts = new_desc.split(" / ")
@@ -284,7 +284,7 @@ class MiciOffroadAlerts(Scroller):
continue # Skip, already handled above
text = ""
alert_json = self._pending_params[alert_data.key]
alert_json = pending_params[alert_data.key]
if alert_json:
text = alert_json.get("text", "").replace("%1", alert_json.get("extra", ""))
@@ -311,8 +311,9 @@ class MiciOffroadAlerts(Scroller):
def _update_state(self):
"""Periodically refresh alerts."""
# Refresh alerts when thread updates params
if self._pending_params is not None:
self._refresh()
pending_params = self._pending_params
if pending_params is not None:
self._refresh(pending_params)
self._pending_params = None
def _render(self, rect: rl.Rectangle):
@@ -16,7 +16,7 @@ from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.widgets.label import UnifiedLabel
from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer
from openpilot.system.ui.widgets.html_render import HtmlRenderer
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
@@ -160,7 +160,7 @@ class DeviceLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self._fcc_dialog: HtmlModal | None = None
self._fcc_dialog: MiciFccModal | None = None
def power_off_callback():
ui_state.params.put_bool("DoShutdown", True, block=True)
@@ -172,7 +172,6 @@ class DeviceLayoutMici(NavScroller):
params = ui_state.params
params.remove("CalibrationParams")
params.remove("LiveTorqueParameters")
params.remove("LiveParameters")
params.remove("LiveParametersV2")
params.remove("LiveDelay")
params.put_bool("OnroadCycleRequested", True, block=True)
@@ -219,7 +219,7 @@ class CameraView(Widget):
[0.0, 0.0, 1.0]
])
def _render(self, rect: rl.Rectangle):
def _render(self, rect: rl.Rectangle, /):
if self._switching:
self._handle_switch()
@@ -47,8 +47,8 @@ class ModelPoints:
@dataclass
class LeadVehicle:
glow: list[float] = field(default_factory=list)
chevron: list[float] = field(default_factory=list)
glow: list[tuple[float, float]] = field(default_factory=list)
chevron: list[tuple[float, float]] = field(default_factory=list)
fill_alpha: int = 0
@@ -41,80 +41,81 @@ def get_child_widgets(widget) -> list:
return children
@pytest.mark.skip(reason="segfaults")
def test_dialogs_do_not_leak():
import pyray as rl
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
from openpilot.system.ui.lib.application import gui_app
class TestWidgetLeaks:
@pytest.mark.skip(reason="segfaults")
def test_dialogs_do_not_leak(self):
import pyray as rl
rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN)
from openpilot.system.ui.lib.application import gui_app
# mici dialogs
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
# mici dialogs
from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog as MiciDriverCameraDialog
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog
from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal
# tici dialogs
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.html_render import HtmlModal
from openpilot.system.ui.widgets.keyboard import Keyboard
# tici dialogs
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog as TiciDriverCameraDialog
from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow
from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.html_render import HtmlModal
from openpilot.system.ui.widgets.keyboard import Keyboard
gui_app.init_window("ref-test")
gui_app.init_window("ref-test")
leaked_widgets = set()
leaked_widgets = set()
for ctor in (
# mici
MiciDriverCameraDialog, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
lambda: BigInputDialog("test"),
lambda: MiciFccModal(text="test"),
# tici
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
lambda: ConfirmDialog("test", "ok"),
lambda: MultiOptionDialog("test", ["a", "b"]),
lambda: HtmlModal(text="test"),
):
widget = ctor()
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
for ctor in (
# mici
MiciDriverCameraDialog, MiciPairingDialog,
lambda: MiciTrainingGuide(lambda: None),
lambda: MiciOnboardingWindow(lambda: None),
lambda: BigDialog("test", "test"),
lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None),
lambda: BigInputDialog("test"),
lambda: MiciFccModal(text="test"),
# tici
TiciDriverCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard,
lambda: ConfirmDialog("test", "ok"),
lambda: MultiOptionDialog("test", ["a", "b"]),
lambda: HtmlModal(text="test"),
):
widget = ctor()
all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]]
del widget
del widget
for ref in all_refs:
if ref() is not None:
obj = ref()
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
leaked_widgets.add(name)
for ref in all_refs:
if ref() is not None:
obj = ref()
name = f"{type(obj).__module__}.{type(obj).__qualname__}"
leaked_widgets.add(name)
print(f"\n=== Widget {name} alive after del")
print(" Referrers:")
for r in gc.get_referrers(obj):
if r is obj:
continue
print(f"\n=== Widget {name} alive after del")
print(" Referrers:")
for r in gc.get_referrers(obj):
if r is obj:
continue
if hasattr(r, '__self__') and r.__self__ is not obj:
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
elif hasattr(r, '__func__'):
print(f" method: {r.__name__}")
else:
print(f" {type(r).__module__}.{type(r).__qualname__}")
del obj
if hasattr(r, '__self__') and r.__self__ is not obj:
print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}")
elif hasattr(r, '__func__'):
print(f" method: {r.__name__}")
else:
print(f" {type(r).__module__}.{type(r).__qualname__}")
del obj
gui_app.close()
gui_app.close()
unexpected = leaked_widgets - KNOWN_LEAKS
assert not unexpected, f"New leaked widgets: {unexpected}"
unexpected = leaked_widgets - KNOWN_LEAKS
assert not unexpected, f"New leaked widgets: {unexpected}"
fixed = KNOWN_LEAKS - leaked_widgets
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
fixed = KNOWN_LEAKS - leaked_widgets
assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}"
if __name__ == "__main__":
test_dialogs_do_not_leak()
TestWidgetLeaks().test_dialogs_do_not_leak()
@@ -375,6 +375,7 @@ class GreyBigButton(BigButton):
class BigMultiParamToggle(BigMultiToggle):
def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable | None = None,
select_callback: Callable | None = None):
assert Params is not None
super().__init__(text, options, toggle_callback, select_callback)
self._param = param
@@ -392,6 +393,7 @@ class BigMultiParamToggle(BigMultiToggle):
class BigParamControl(BigToggle):
def __init__(self, text: str, param: str, toggle_callback: Callable | None = None):
assert Params is not None
super().__init__(text, "", toggle_callback=toggle_callback)
self.param = param
self.params = Params()
@@ -409,6 +411,7 @@ class BigParamControl(BigToggle):
class BigCircleParamControl(BigCircleToggle):
def __init__(self, icon: rl.Texture, param: str, toggle_callback: Callable | None = None,
icon_offset: tuple[int, int] = (0, 0)):
assert Params is not None
super().__init__(icon, toggle_callback, icon_offset=icon_offset)
self._param = param
self.params = Params()
@@ -39,8 +39,8 @@ class ModelPoints:
@dataclass
class LeadVehicle:
glow: list[float] = field(default_factory=list)
chevron: list[float] = field(default_factory=list)
glow: list[tuple[float, float]] = field(default_factory=list)
chevron: list[tuple[float, float]] = field(default_factory=list)
fill_alpha: int = 0
+21 -6
View File
@@ -24,7 +24,7 @@ ALERT_RAMP_TIME = 4 # seconds to ramp to max volume for warningImmediate
SELFDRIVE_STATE_TIMEOUT = 5 # 5 seconds
FILTER_DT = 1. / (micd.SAMPLE_RATE / micd.FFT_SAMPLES)
AMBIENT_DB = 24 # DB where MIN_VOLUME is applied
AMBIENT_DB = 26 # DB where MIN_VOLUME is applied
DB_SCALE = 30 # AMBIENT_DB + DB_SCALE is where MAX_VOLUME is applied
VOLUME_BASE = 20
@@ -48,14 +48,15 @@ sound_list: dict[int, tuple[str, int | None, float]] = {
AudibleAlert.disengage: ("disengage.wav", 1, MAX_VOLUME),
AudibleAlert.refuse: ("refuse.wav", 1, MAX_VOLUME),
AudibleAlert.prompt: ("prompt.wav", 1, MAX_VOLUME),
AudibleAlert.promptRepeat: ("prompt.wav", None, MAX_VOLUME),
AudibleAlert.promptDistracted: ("prompt_distracted.wav", None, MAX_VOLUME),
AudibleAlert.prompt: ("warning.wav", 1, MAX_VOLUME),
AudibleAlert.promptRepeat: ("warning.wav", None, MAX_VOLUME),
AudibleAlert.promptDistracted: ("dm_warning.wav", None, MAX_VOLUME),
AudibleAlert.preAlert: ("pre_alert.wav", 1, MAX_VOLUME),
AudibleAlert.complete: ("complete.wav", 1, MAX_VOLUME),
AudibleAlert.warningSoft: ("warning_soft.wav", None, MAX_VOLUME),
AudibleAlert.warningImmediate: ("warning_immediate.wav", None, MAX_VOLUME),
AudibleAlert.warningSoft: ("critical.wav", None, MAX_VOLUME),
AudibleAlert.warningImmediate: ("dm_critical.wav", None, MAX_VOLUME),
**sound_list_sp,
}
@@ -89,6 +90,7 @@ class Soundd(QuietMode):
self.ramp_start_time = 0.
self.selfdrive_timeout_alert = False
self.pending_stop = False
self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
@@ -127,6 +129,10 @@ class Soundd(QuietMode):
self.current_sound_frame += frames_to_write
current_sound_frame = self.current_sound_frame % len(sound_data)
loops = self.current_sound_frame // len(sound_data)
if self.pending_stop and current_sound_frame == 0:
self.current_alert = AudibleAlert.none
self.pending_stop = False
break
return ret * self.current_volume
@@ -137,6 +143,15 @@ class Soundd(QuietMode):
def update_alert(self, new_alert):
current_alert_played_once = self.current_alert == AudibleAlert.none or self.current_sound_frame >= len(self.loaded_sounds[self.current_alert])
# let looping sounds finish the current loop instead of cutting off mid tone
if new_alert == AudibleAlert.none and self.current_alert != AudibleAlert.none and sound_list[self.current_alert][1] is None:
if current_alert_played_once:
self.pending_stop = True
else:
self.current_alert = AudibleAlert.none
self.current_sound_frame = 0
return
self.pending_stop = False
if self.current_alert != new_alert and (new_alert != AudibleAlert.none or current_alert_played_once):
if new_alert == AudibleAlert.warningImmediate:
self.ramp_start_volume = self.current_volume
@@ -238,7 +238,7 @@ class ModelsLayout(Widget):
self.lagd_toggle.action_item.set_state(live_delay)
self.delay_control.set_visible(not live_delay and advanced_controls)
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
if self.lane_turn_value_control.action_item.value_change_step != new_step:
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
self.lane_turn_value_control.action_item.value_change_step = new_step
self._update_lagd_description(live_delay)
@@ -38,8 +38,8 @@ class NetworkUISP(NetworkUI):
self.scan_button.set_text(tr("Scan"))
self.scan_button.set_enabled(True)
def _render(self, rect: rl.Rectangle):
super()._render(rect)
def _render(self, _):
super()._render(_)
if self._current_panel == PanelType.WIFI:
self.scan_button.set_position(self._rect.x, self._rect.y + 20)
@@ -37,7 +37,7 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
OP.PANEL_COLOR = rl.Color(10, 10, 10, 255)
ICON_SIZE = 70
OP.PanelType = IntEnum(
OP.PanelType = IntEnum( # type: ignore[assignment] # ty: ignore[invalid-assignment]
"PanelType",
[es.name for es in OP.PanelType] + [
"SUNNYLINK",
@@ -180,20 +180,18 @@ class SettingsLayoutSP(OP.SettingsLayout):
self._sidebar_scroller.render(nav_rect)
return
def _handle_mouse_release(self, mouse_pos: MousePos) -> bool:
def _handle_mouse_release(self, mouse_pos: MousePos) -> None:
# Check close button
if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect):
if self._close_callback:
self._close_callback()
return True
return
# Check navigation buttons
for panel_type, panel_info in self._panels.items():
if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid():
self.set_current_panel(panel_type)
return True
return False
return
def show_event(self):
super().show_event()
@@ -138,7 +138,7 @@ class ModelsLayoutMici(NavScroller):
self._show_selection_view(btns, self._show_folders)
def _reset_main_view(self):
self._scroller._items = self.main_items
self._scroller._items = self.main_items # type: ignore[assignment] # ty: ignore[invalid-assignment]
self.set_back_callback(self.original_back_callback)
self._scroller.scroll_panel.set_offset(0)
self._scroller.scroll_to(0)
@@ -16,6 +16,9 @@ class SunnylinkConsentPage(NavScroller):
def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None):
super().__init__()
assert on_accept is not None and callable(on_accept)
assert on_decline is not None and callable(on_decline)
self._accept_button = BigConfirmationCircleButton("enable\nsunnylink", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64),
on_accept, exit_on_confirm=False)
@@ -198,7 +198,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
self._draw_ahead_info(sign_rect)
def _draw_sign_main(self, rect, alpha=1.0):
speed_limit_warning_enabled = ui_state.speed_limit_mode >= SpeedLimitMode.warning
speed_limit_warning_enabled = ui_state.speed_limit_mode is not None and ui_state.speed_limit_mode >= SpeedLimitMode.warning
has_limit = self.speed_limit_valid or self.speed_limit_last_valid
is_overspeed = has_limit and round(self.speed_limit_final_last) < round(self.speed)
@@ -224,8 +224,7 @@ class UIStateSP:
class DeviceSP:
@staticmethod
def _set_awake(on: bool, _ui_state):
def _set_awake(self, on: bool, _ui_state=None):
if _ui_state.boot_offroad_mode == 1 and not on:
_ui_state.params.put_bool("OffroadMode", True)
@@ -357,7 +357,7 @@ def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None:
params = Params()
main_layout._alerts_layout._pending_params = ({"UpdaterNewDescription": params.get("UpdaterNewDescription")} |
{alert_data.key: params.get(alert_data.key) for alert_data in main_layout._alerts_layout.sorted_alerts})
main_layout._alerts_layout._refresh()
main_layout._alerts_layout._update_state()
swipe_right(width, wait_after=WAIT_SHORT) # open alerts
script.setup(setup_offroad_alerts_and_refresh) # show alerts
@@ -49,7 +49,7 @@ def patch_submaster(message_chunks):
sm.recv_frame[service] = sm.frame
sm.valid[service] = True
sm.frame += 1
ui_state.sm.update = mock_update
ui_state.sm.update = mock_update # ty: ignore[invalid-assignment] # profiling hook
if __name__ == "__main__":
@@ -92,7 +92,7 @@ if __name__ == "__main__":
vipc.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 5, W, H)
vipc.start_listener()
yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2
yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()
yuv_data = np.random.default_rng().integers(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes()
with cProfile.Profile() as pr:
for _ in gui_app.render():
if ui_state.sm.frame >= len(message_chunks):
@@ -2,7 +2,8 @@ import time
from openpilot.selfdrive.test.helpers import with_processes
@with_processes(["ui"])
def test_raylib_ui():
"""Test initialization of the UI widgets is successful."""
time.sleep(1)
class TestRaylibUi:
@with_processes(["ui"])
def test_raylib_ui(self):
"""Test initialization of the UI widgets is successful."""
time.sleep(1)
@@ -46,61 +46,59 @@ def load_po_text(po_path: Path) -> str:
return po_path.read_text(encoding='utf-8')
@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values()))
def test_translation_file_exists(language_code: str):
po_path = PO_DIR / f"app_{language_code}.po"
assert po_path.exists(), f"missing translation file: {po_path}"
class TestTranslations:
@pytest.mark.parametrize("language_code", sorted(TRANSLATION_LANGUAGES.values()))
def test_translation_file_exists(self, language_code: str):
po_path = PO_DIR / f"app_{language_code}.po"
assert po_path.exists(), f"missing translation file: {po_path}"
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_placeholders_are_preserved(self, po_path: Path):
_, entries = parse_po(po_path)
language = po_path.stem.removeprefix("app_")
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_placeholders_are_preserved(po_path: Path):
_, entries = parse_po(po_path)
language = po_path.stem.removeprefix("app_")
for entry in entries:
source_placeholders = extract_placeholders(entry.msgid)
for entry in entries:
source_placeholders = extract_placeholders(entry.msgid)
if entry.is_plural:
plural_placeholders = extract_placeholders(entry.msgid_plural)
message = (
f"{language}: source plural placeholders do not match singular for "
+ f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
)
assert plural_placeholders == source_placeholders, message
if entry.is_plural:
plural_placeholders = extract_placeholders(entry.msgid_plural)
message = (
f"{language}: source plural placeholders do not match singular for "
+ f"{entry.msgid!r}: {source_placeholders} vs {plural_placeholders}"
)
assert plural_placeholders == source_placeholders, message
for idx, msgstr in sorted(entry.msgstr_plural.items()):
if not msgstr:
continue
for idx, msgstr in sorted(entry.msgstr_plural.items()):
if not msgstr:
translated_placeholders = extract_placeholders(msgstr)
message = (
f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
+ f"expected {source_placeholders}, got {translated_placeholders}"
)
assert translated_placeholders == source_placeholders, message
else:
if not entry.msgstr:
continue
translated_placeholders = extract_placeholders(msgstr)
translated_placeholders = extract_placeholders(entry.msgstr)
message = (
f"{language}: plural form {idx} changes placeholders for {entry.msgid!r}: "
f"{language}: translation changes placeholders for {entry.msgid!r}: "
+ f"expected {source_placeholders}, got {translated_placeholders}"
)
assert translated_placeholders == source_placeholders, message
else:
if not entry.msgstr:
continue
translated_placeholders = extract_placeholders(entry.msgstr)
message = (
f"{language}: translation changes placeholders for {entry.msgid!r}: "
+ f"expected {source_placeholders}, got {translated_placeholders}"
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_refs_do_not_include_line_numbers(self, po_path: Path):
for line in load_po_text(po_path).splitlines():
assert not LINE_NUMBER_REF_RE.match(line), (
f"{po_path.name}: line-number source reference found: {line}"
)
assert translated_placeholders == source_placeholders, message
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_refs_do_not_include_line_numbers(po_path: Path):
for line in load_po_text(po_path).splitlines():
assert not LINE_NUMBER_REF_RE.match(line), (
f"{po_path.name}: line-number source reference found: {line}"
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_entities_are_valid(self, po_path: Path):
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
assert not matches, (
f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
)
@pytest.mark.parametrize("po_path", sorted(PO_DIR.glob("app_*.po")), ids=lambda p: p.name)
def test_translation_entities_are_valid(po_path: Path):
matches = BAD_ENTITY_RE.findall(load_po_text(po_path))
assert not matches, (
f"{po_path.name}: found '@...;' entity typo(s): {', '.join(sorted(set(matches)))}"
)
+12 -10
View File
@@ -67,22 +67,22 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]:
cur_field: str | None = None
plural_idx = 0
def finish():
nonlocal cur, header
if cur is None:
def finish(entry: POEntry | None):
nonlocal header
if entry is None:
return
if cur.msgid == "" and cur.msgstr:
header = cur
elif cur.msgid != "" or cur.is_plural:
entries.append(cur)
cur = None
if entry.msgid == "" and entry.msgstr:
header = entry
elif entry.msgid != "" or entry.is_plural:
entries.append(entry)
for raw in lines:
line = raw.rstrip('\n')
stripped = line.strip()
if not stripped:
finish()
finish(cur)
cur = None
cur_field = None
continue
@@ -123,6 +123,8 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]:
continue
if stripped.startswith('msgstr '):
if cur is None:
cur = POEntry()
cur.msgstr = _parse_quoted(stripped[len('msgstr '):])
cur_field = 'msgstr'
continue
@@ -138,7 +140,7 @@ def parse_po(path: str | Path) -> tuple[POEntry | None, list[POEntry]]:
elif cur_field == 'msgstr_plural':
cur.msgstr_plural[plural_idx] += val
finish()
finish(cur)
return header, entries
@@ -12,9 +12,9 @@ POT_FILE = os.path.join(str(TRANSLATIONS_DIR), "app.pot")
def update_translations():
files = []
for root, _, filenames in chain(os.walk(SYSTEM_UI_DIR),
os.walk(os.path.join(UI_DIR, "widgets")),
os.walk(os.path.join(UI_DIR, "layouts")),
os.walk(os.path.join(UI_DIR, "onroad"))):
os.walk(os.path.join(str(UI_DIR), "widgets")),
os.walk(os.path.join(str(UI_DIR), "layouts")),
os.walk(os.path.join(str(UI_DIR), "onroad"))):
for filename in filenames:
if filename.endswith(".py"):
files.append(os.path.relpath(os.path.join(root, filename), BASEDIR))
@@ -25,7 +25,7 @@ def update_translations():
# Generate/update translation files for each language
for name in multilang.languages.values():
po_file = os.path.join(TRANSLATIONS_DIR, f"app_{name}.po")
po_file = os.path.join(str(TRANSLATIONS_DIR), f"app_{name}.po")
if os.path.exists(po_file):
merge_po(po_file, POT_FILE)
else:
+4 -4
View File
@@ -314,9 +314,9 @@ class Device(DeviceSP):
brightness = 0
if brightness != self._last_brightness:
self._brightness_target = brightness
self._brightness_target = int(brightness)
self._brightness_event.set()
self._last_brightness = brightness
self._last_brightness = int(brightness)
def _update_wakefulness(self):
# Handle interactive timeout
@@ -337,9 +337,9 @@ class Device(DeviceSP):
self._set_awake(ui_state.ignition or not interaction_timeout or PC)
def _set_awake(self, on: bool):
def _set_awake(self, on: bool, _ui_state=None):
if on != self._awake:
DeviceSP._set_awake(on, ui_state)
super()._set_awake(on, _ui_state or ui_state)
self._awake = on
cloudlog.debug(f"setting display power {int(on)}")
HARDWARE.set_display_power(on)
@@ -29,7 +29,7 @@ def make_event(event_types):
event = {}
for ev in event_types:
event[ev] = NormalPermanentAlert("alert")
EVENTS_SP[0] = event
EVENTS_SP[0] = event # type: ignore[assignment] # ty: ignore[invalid-assignment]
return 0
+1 -1
View File
@@ -128,7 +128,7 @@ def main_thread():
cloudlog.exception(f"mapd: failed to make {Paths.mapd_root()}")
while True:
show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal")
show_alert = bool(get_files_for_cleanup() and params.get_bool("OsmLocal"))
set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.")
update_osm_db()
@@ -129,7 +129,7 @@ def compile_split_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, Device.DEFAULT)
np.random.seed(seed)
rng = np.random.default_rng(seed)
Tensor.manual_seed(seed)
testing = test_val is not None or test_buffers is not None
@@ -139,7 +139,7 @@ def compile_split_policy(nv12: NV12Frame, model_w, model_h, prepare_only, frame_
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
@@ -319,11 +319,11 @@ def make_run_vision_multi_policy(vision_runner, policy_runners, nv12: NV12Frame,
def _warmup_and_serialize(run_jit, input_queues, npy, nv12):
for i in range(3):
np.random.seed(42 + i)
rng = np.random.default_rng(42 + i)
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
v[:] = rng.standard_normal(v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
run_jit(**input_queues, frame=frame, big_frame=big_frame)
@@ -61,6 +61,7 @@ class Parser:
weights[fidx] = weights[fidx][idxs]
pred_mu[fidx] = pred_mu[fidx][idxs]
pred_std[fidx] = pred_std[fidx][idxs]
assert out_shape is not None
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_weights'] = weights
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
@@ -78,8 +79,10 @@ class Parser:
pred_std_final = pred_std
if out_N > 1:
assert out_shape is not None
final_shape = tuple([raw.shape[0], out_N] + list(out_shape))
else:
assert out_shape is not None
final_shape = tuple([raw.shape[0],] + list(out_shape))
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
@@ -65,6 +65,7 @@ class Parser:
weights[fidx] = weights[fidx][idxs]
pred_mu[fidx] = pred_mu[fidx][idxs]
pred_std[fidx] = pred_std[fidx][idxs]
assert out_shape is not None
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_weights'] = weights
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
@@ -82,8 +83,10 @@ class Parser:
pred_std_final = pred_std
if out_N > 1:
assert out_shape is not None
final_shape = tuple([raw.shape[0], out_N] + list(out_shape))
else:
assert out_shape is not None
final_shape = tuple([raw.shape[0],] + list(out_shape))
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
@@ -61,7 +61,7 @@ class TestFrameSkipBufferLengthEquivalence:
class TestTemporalSamplingEquivalence:
def test_non20hz_desire_sampling_identity(self):
buf = np.random.randn(100, 1, 8).astype(np.float32)
buf = np.random.default_rng(0).standard_normal((100, 1, 8)).astype(np.float32)
frame_skip = 1
sampled = buf[::frame_skip].reshape(-1, 8)
assert sampled.shape == (100, 8)
@@ -150,7 +150,7 @@ class TestOutputSlicePreservation:
def test_vision_hidden_state_slice_used_for_features(self):
mock_slices = {'hidden_state': slice(0, 512), 'plan': slice(512, 1024)}
features_slice = mock_slices['hidden_state']
fake_output = np.random.randn(1, 1024).astype(np.float32)
fake_output = np.random.default_rng(0).standard_normal((1, 1024)).astype(np.float32)
features = fake_output[:, features_slice]
assert features.shape == (1, 512)
@@ -1,3 +1,5 @@
from typing import Any
import numpy as np
from openpilot.cereal import log
@@ -14,7 +16,7 @@ class MockStruct:
def test_recovery_power_scaling():
state = MockStruct(
state: Any = MockStruct(
PLANPLUS_CONTROL=0.75,
LONG_SMOOTH_SECONDS=0.3,
LAT_SMOOTH_SECONDS=0.1,
@@ -35,10 +37,10 @@ def test_recovery_power_scaling():
recorded_curv_plans.append(plan.copy())
return 0.0
modeld.get_accel_from_plan = mock_accel
modeld.get_curvature_from_output = mock_curvature
plan = np.random.rand(1, 100, 15).astype(np.float32)
planplus = np.random.rand(1, 100, 15).astype(np.float32)
modeld.get_accel_from_plan = mock_accel # ty: ignore[invalid-assignment]
modeld.get_curvature_from_output = mock_curvature # ty: ignore[invalid-assignment]
plan = np.random.default_rng(0).random((1, 100, 15)).astype(np.float32)
planplus = np.random.default_rng(1).random((1, 100, 15)).astype(np.float32)
merged_plan = plan + planplus
model_output: dict = {
@@ -60,7 +62,7 @@ def test_recovery_power_scaling():
state.PLANPLUS_CONTROL = control
recorded_vel.clear()
recorded_curv_plans.clear()
ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego)
ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego) # type: ignore[arg-type]
expected_accel_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + planplus[0, :, Plan.VELOCITY][:, 0]
np.testing.assert_allclose(recorded_vel[0], expected_accel_plan_vel, rtol=1e-5, atol=1e-6)
+4 -4
View File
@@ -66,8 +66,8 @@ def compile_v2_warp(cam_w, cam_h, buffer_length):
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
new_frame_np = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
new_big_frame_np = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
for i in range(10):
img_inputs = [full_buffer,
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
@@ -91,8 +91,8 @@ def compile_v2_warp(cam_w, cam_h, buffer_length):
print(f" Saved to {pkl_path}")
jit = pickle.load(open(pkl_path, "rb"))
verify_frame = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
verify_big_frame = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
verify_frame = np.random.default_rng(0).integers(0, 256, yuv_size, dtype=np.uint8)
verify_big_frame = np.random.default_rng(1).integers(0, 256, yuv_size, dtype=np.uint8)
fresh_inputs = [
Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize(),
Tensor.from_blob(verify_frame.ctypes.data, (yuv_size,), dtype='uint8').realize(),
+1 -1
View File
@@ -126,7 +126,7 @@ def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | byte
params = params or Params()
try:
active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {})
if active_bundle_dict and is_bundle_version_compatible(active_bundle_dict):
if isinstance(active_bundle_dict, dict) and active_bundle_dict and is_bundle_version_compatible(active_bundle_dict):
return custom.ModelManagerSP.ModelBundle(**active_bundle_dict)
except Exception:
pass
+7 -6
View File
@@ -69,7 +69,7 @@ class ModelManagerSP:
total_size = int(response.headers.get("content-length", 0))
bytes_downloaded = 0
with open(path, 'wb') as f:
with open(path, 'wb') as f: # noqa: ASYNC230
async for chunk in response.content.iter_chunked(self._chunk_size): # type: bytes
f.write(chunk)
bytes_downloaded += len(chunk)
@@ -110,7 +110,7 @@ class ModelManagerSP:
async with session.get(chunk_url) as response:
response.raise_for_status()
chunk_size = int(response.headers.get("content-length", 0))
with open(chunk_path, 'wb') as f:
with open(chunk_path, 'wb') as f: # noqa: ASYNC230
async for data in response.content.iter_chunked(self._chunk_size):
f.write(data)
chunk_downloaded += len(data)
@@ -124,9 +124,9 @@ class ModelManagerSP:
self._sync_artifact_progress(artifact)
self._report_status()
with open(manifest_path, 'w') as f:
with open(manifest_path, 'w') as f: # noqa: ASYNC230
f.write(str(num_chunks))
if os.path.isfile(base_path):
if os.path.isfile(base_path): # noqa: ASYNC240
os.remove(base_path)
del self._download_start_times[artifact.fileName]
@@ -165,7 +165,7 @@ class ModelManagerSP:
except Exception as e:
cloudlog.error(f"Error downloading {filename}: {str(e)}")
for f in [full_path] + [p for p in (os.path.join(destination_path, f) for f in os.listdir(destination_path)) if filename in p]:
if os.path.isfile(f):
if os.path.isfile(f): # noqa: ASYNC240
os.remove(f)
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
artifact.downloadProgress.eta = 0
@@ -222,7 +222,8 @@ class ModelManagerSP:
self.selected_bundle = None
except Exception:
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
if self.selected_bundle is not None:
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
raise
finally:

Some files were not shown because too many files have changed in this diff Show More