From 037862a7b7086bdfbb224ce0adc9786495f49e60 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Mon, 6 Jun 2022 12:51:25 +0200 Subject: [PATCH 001/436] bump laika --- laika_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laika_repo b/laika_repo index e816bbe6bd6e30..ba6ed3277cadfd 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit e816bbe6bd6e304dfd7669174ad12491b3792801 +Subproject commit ba6ed3277cadfdc8697206784afbd7f9a223798b From ca682b389d75f433d28063c8d774460df0c0fa83 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 6 Jun 2022 13:27:45 -0700 Subject: [PATCH 002/436] move clocksd to system/ (#24761) --- SConstruct | 7 ++++--- release/files_common | 6 +++--- selfdrive/manager/process_config.py | 2 +- {selfdrive => system}/clocksd/.gitignore | 0 {selfdrive => system}/clocksd/SConscript | 0 {selfdrive => system}/clocksd/clocksd.cc | 0 6 files changed, 8 insertions(+), 7 deletions(-) rename {selfdrive => system}/clocksd/.gitignore (100%) rename {selfdrive => system}/clocksd/SConscript (100%) rename {selfdrive => system}/clocksd/clocksd.cc (100%) diff --git a/SConstruct b/SConstruct index e0e24d2664aba9..0aeb382c23400e 100644 --- a/SConstruct +++ b/SConstruct @@ -378,8 +378,10 @@ Export('rednose_config') SConscript(['rednose/SConscript']) # Build system services - -SConscript(['system/proclogd/SConscript']) +SConscript([ + 'system/clocksd/SConscript', + 'system/proclogd/SConscript', +]) if arch != "Darwin": SConscript(['system/logcatd/SConscript']) @@ -402,7 +404,6 @@ SConscript(['selfdrive/controls/lib/lateral_mpc_lib/SConscript']) SConscript(['selfdrive/controls/lib/longitudinal_mpc_lib/SConscript']) SConscript(['selfdrive/boardd/SConscript']) -SConscript(['selfdrive/clocksd/SConscript']) SConscript(['selfdrive/loggerd/SConscript']) diff --git a/release/files_common b/release/files_common index 9c6a2cec472a52..ceb0110606e932 100644 --- a/release/files_common +++ b/release/files_common @@ -119,9 +119,9 @@ selfdrive/car/tesla/*.py selfdrive/car/toyota/*.py selfdrive/car/volkswagen/*.py -selfdrive/clocksd/.gitignore -selfdrive/clocksd/SConscript -selfdrive/clocksd/clocksd.cc +system/clocksd/.gitignore +system/clocksd/SConscript +system/clocksd/clocksd.cc selfdrive/debug/*.py diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 4a71aa508b2b05..5d996d1169fc72 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -21,7 +21,7 @@ def logging(started, params, CP: car.CarParams) -> bool: DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"), # due to qualcomm kernel bugs SIGKILLing camerad sometimes causes page table corruption NativeProcess("camerad", "selfdrive/camerad", ["./camerad"], unkillable=True, callback=driverview), - NativeProcess("clocksd", "selfdrive/clocksd", ["./clocksd"]), + NativeProcess("clocksd", "system/clocksd", ["./clocksd"]), NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], enabled=(not PC or WEBCAM), callback=driverview), NativeProcess("logcatd", "system/logcatd", ["./logcatd"]), NativeProcess("encoderd", "selfdrive/loggerd", ["./encoderd"]), diff --git a/selfdrive/clocksd/.gitignore b/system/clocksd/.gitignore similarity index 100% rename from selfdrive/clocksd/.gitignore rename to system/clocksd/.gitignore diff --git a/selfdrive/clocksd/SConscript b/system/clocksd/SConscript similarity index 100% rename from selfdrive/clocksd/SConscript rename to system/clocksd/SConscript diff --git a/selfdrive/clocksd/clocksd.cc b/system/clocksd/clocksd.cc similarity index 100% rename from selfdrive/clocksd/clocksd.cc rename to system/clocksd/clocksd.cc From 7e5eec2e4fe826a79386ae89fb2ddfeb6e59f93b Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Mon, 6 Jun 2022 17:03:34 -0400 Subject: [PATCH 003/436] ui: fix prime ad storage duration (#24756) fix ui prime ad storage duration --- selfdrive/ui/qt/widgets/prime.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 27d472535b2204..8a208bf3cf1231 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -198,7 +198,7 @@ PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { main_layout->addWidget(features, 0, Qt::AlignBottom); main_layout->addSpacing(30); - QVector bullets = {"Remote access", "14 days of storage", "Developer perks"}; + QVector bullets = {"Remote access", "1 year of storage", "Developer perks"}; for (auto &b: bullets) { const QString check = " "; QLabel *l = new QLabel(check + b); From 397da56c85d4d4932d25435dcb1d57f2cd2d8c4b Mon Sep 17 00:00:00 2001 From: Lukas Petersson Date: Mon, 6 Jun 2022 23:21:12 +0200 Subject: [PATCH 004/436] process replay: regen in parallel (#24628) * regen in parallel * prefixes * clean regen * clean output * tqdm loc * del swp file * add routes back * cleanup * disable tqdm * unique dirs * unique dirs * outdir in regen_all * formatting when played from other dirs * prefix dongle id * local disable_tqdm * formatting * bug fix * dont spam fakedata * 16 char fake dongle ids * formatting * formatting * more descriptive dongle * fix azure path * couple more fixes * handle failures nicely Co-authored-by: Adeeb Shihadeh --- selfdrive/test/process_replay/helpers.py | 2 +- selfdrive/test/process_replay/regen.py | 23 ++++++----- selfdrive/test/process_replay/regen_all.py | 46 ++++++++++++++-------- selfdrive/test/update_ci_routes.py | 10 +++-- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/selfdrive/test/process_replay/helpers.py b/selfdrive/test/process_replay/helpers.py index b650ecb69ae075..8571f36c36f7cf 100644 --- a/selfdrive/test/process_replay/helpers.py +++ b/selfdrive/test/process_replay/helpers.py @@ -23,4 +23,4 @@ def __exit__(self, exc_type, exc_obj, exc_tb): os.remove(symlink_path) shutil.rmtree(self.msgq_path, ignore_errors=True) del os.environ['OPENPILOT_PREFIX'] - return True + return False diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index bcc91b3ab6f708..653efaf32cfee2 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -3,8 +3,8 @@ import os import time import multiprocessing -from tqdm import tqdm import argparse +from tqdm import tqdm # run DM procs os.environ["USE_WEBCAM"] = "1" @@ -24,7 +24,6 @@ from tools.lib.framereader import FrameReader from tools.lib.logreader import LogReader - def replay_panda_states(s, msgs): pm = messaging.PubMaster([s, 'peripheralState']) rk = Ratekeeper(service_list[s].frequency, print_delay_threshold=None) @@ -118,7 +117,7 @@ def replay_service(s, msgs): rk.keep_time() -def replay_cameras(lr, frs): +def replay_cameras(lr, frs, disable_tqdm=False): eon_cameras = [ ("roadCameraState", DT_MDL, eon_f_frame_size, VisionStreamType.VISION_STREAM_ROAD, True), ("driverCameraState", DT_DMON, eon_d_frame_size, VisionStreamType.VISION_STREAM_DRIVER, False), @@ -163,7 +162,7 @@ def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): if fr is not None: print(f"Decompressing frames {s}") frames = [] - for i in tqdm(range(fr.frame_count)): + for i in tqdm(range(fr.frame_count), disable=disable_tqdm): img = fr.get(i, pix_fmt='nv12')[0] frames.append(img.flatten().tobytes()) @@ -177,7 +176,7 @@ def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): return vs, p -def regen_segment(lr, frs=None, outdir=FAKEDATA): +def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): lr = list(lr) if frs is None: frs = dict() @@ -207,7 +206,7 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA): elif msg.which() == 'liveCalibration': params.put("CalibrationParams", msg.as_builder().to_bytes()) - vs, cam_procs = replay_cameras(lr, frs) + vs, cam_procs = replay_cameras(lr, frs, disable_tqdm=disable_tqdm) fake_daemons = { 'sensord': [ @@ -219,7 +218,7 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA): multiprocessing.Process(target=replay_panda_states, args=('pandaStates', lr)), ], 'managerState': [ - multiprocessing.Process(target=replay_manager_state, args=('managerState', lr)), + multiprocessing.Process(target=replay_manager_state, args=('managerState', lr)), ], 'thermald': [ multiprocessing.Process(target=replay_device_state, args=('deviceState', lr)), @@ -236,13 +235,13 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA): time.sleep(5) # start procs up - ignore = list(fake_daemons.keys()) + ['ui', 'manage_athenad', 'uploader'] + ignore = list(fake_daemons.keys()) + ['ui', 'manage_athenad', 'uploader', 'soundd'] ensure_running(managed_processes.values(), started=True, params=Params(), CP=car.CarParams(), not_run=ignore) for procs in fake_daemons.values(): for p in procs: p.start() - for _ in tqdm(range(60)): + for _ in tqdm(range(60), disable=disable_tqdm): # ensure all procs are running for d, procs in fake_daemons.items(): for p in procs: @@ -268,7 +267,7 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA): return seg_path -def regen_and_save(route, sidx, upload=False, use_route_meta=False): +def regen_and_save(route, sidx, upload=False, use_route_meta=False, outdir=FAKEDATA, disable_tqdm=False): if use_route_meta: r = Route(args.route) lr = LogReader(r.log_paths()[args.seg]) @@ -276,7 +275,7 @@ def regen_and_save(route, sidx, upload=False, use_route_meta=False): else: lr = LogReader(f"cd:/{route.replace('|', '/')}/{sidx}/rlog.bz2") fr = FrameReader(f"cd:/{route.replace('|', '/')}/{sidx}/fcamera.hevc") - rpath = regen_segment(lr, {'roadCameraState': fr}) + rpath = regen_segment(lr, {'roadCameraState': fr}, outdir=outdir, disable_tqdm=disable_tqdm) # compress raw rlog before uploading with open(os.path.join(rpath, "rlog"), "rb") as f: @@ -294,7 +293,7 @@ def regen_and_save(route, sidx, upload=False, use_route_meta=False): print("\n\n", "*"*30, "\n\n") print("New route:", relr, "\n") if upload: - upload_route(relr) + upload_route(relr, exclude_patterns=['*.hevc', ]) return relr diff --git a/selfdrive/test/process_replay/regen_all.py b/selfdrive/test/process_replay/regen_all.py index 4f057bbf50c922..765e5c3b682c90 100755 --- a/selfdrive/test/process_replay/regen_all.py +++ b/selfdrive/test/process_replay/regen_all.py @@ -1,22 +1,36 @@ #!/usr/bin/env python3 +import argparse +import concurrent.futures +import os +import random +from tqdm import tqdm + +from selfdrive.test.process_replay.helpers import OpenpilotPrefix from selfdrive.test.process_replay.regen import regen_and_save -from selfdrive.test.process_replay.test_processes import original_segments as segments +from selfdrive.test.process_replay.test_processes import FAKEDATA, original_segments as segments -if __name__ == "__main__": - new_segments = [] - for segment in segments: +def regen_job(segment): + with OpenpilotPrefix(): route = segment[1].rsplit('--', 1)[0] sidx = int(segment[1].rsplit('--', 1)[1]) - print("Regen", route, sidx) - relr = regen_and_save(route, sidx, upload=True, use_route_meta=False) + fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for i in range(11)) + try: + relr = regen_and_save(route, sidx, upload=True, use_route_meta=False, outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=True) + relr = '|'.join(relr.split('/')[-2:]) + return f' ("{segment[0]}", "{relr}"), ' + except Exception as e: + return f" {segment} failed: {str(e)}" + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate new segments from old ones") + parser.add_argument("-j", "--jobs", type=int, default=1) + args = parser.parse_args() - print("\n\n", "*"*30, "\n\n") - print("New route:", relr, "\n") - relr = relr.replace('/', '|') - new_segments.append(f' ("{segment[0]}", "{relr}"), ') - print() - print() - print() - print('COPY THIS INTO test_processes.py') - for seg in new_segments: - print(seg) + with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: + p = list(pool.map(regen_job, segments)) + msg = "Copy these new segments into test_processes.py:" + for seg in tqdm(p, desc="Generating segments"): + msg += "\n" + str(seg) + print() + print() + print(msg) diff --git a/selfdrive/test/update_ci_routes.py b/selfdrive/test/update_ci_routes.py index b388e599f777b0..99a63b8dfd6eff 100755 --- a/selfdrive/test/update_ci_routes.py +++ b/selfdrive/test/update_ci_routes.py @@ -18,8 +18,12 @@ SOURCE_KEYS = [azureutil.get_user_token(account, bucket) for account, bucket in SOURCES] SERVICE = BlockBlobService(_DATA_ACCOUNT_CI, sas_token=DEST_KEY) -def upload_route(path): +def upload_route(path, exclude_patterns=None): + if exclude_patterns is None: + exclude_patterns = ['*/dcamera.hevc'] + r, n = path.rsplit("--", 1) + r = '/'.join(r.split('/')[-2:]) # strip out anything extra in the path destpath = f"{r}/{n}" cmd = [ "azcopy", @@ -28,9 +32,7 @@ def upload_route(path): f"https://{_DATA_ACCOUNT_CI}.blob.core.windows.net/openpilotci/{destpath}?{DEST_KEY}", "--recursive=false", "--overwrite=false", - "--exclude-pattern=*/dcamera.hevc", - "--exclude-pattern=*.mkv", - ] + ] + [f"--exclude-pattern={p}" for p in exclude_patterns] subprocess.check_call(cmd) def sync_to_ci_public(route): From 8d8055f00f11fcdf0a8a235de5218d9c2fd2f5ad Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 7 Jun 2022 12:25:58 +0200 Subject: [PATCH 005/436] Laikad: cleanup fetching orbits (#24759) * Seperate prediction orbits from regular observation orbits and download them efficient * Cleanup * clean and update laika * Fix test * Fix test * Fix checking pos fix * space --- laika_repo | 2 +- selfdrive/locationd/laikad.py | 26 ++++++++++--------------- selfdrive/locationd/test/test_laikad.py | 12 ++++++------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/laika_repo b/laika_repo index ba6ed3277cadfd..d87194613455b4 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit ba6ed3277cadfdc8697206784afbd7f9a223798b +Subproject commit d87194613455b42af19ff2b5a3f7d1cae5852885 diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index eee07e780527d9..d8e32005f86e50 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -6,11 +6,9 @@ import numpy as np from collections import defaultdict -from numpy.linalg import linalg - from cereal import log, messaging from laika import AstroDog -from laika.constants import SECS_IN_HR, SECS_IN_MIN +from laika.constants import SECS_IN_MIN from laika.ephemeris import EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId @@ -26,10 +24,9 @@ class Laikad: - def __init__(self, valid_const=("GPS",), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)): - self.astro_dog = AstroDog(valid_const=valid_const, use_internet=auto_update, valid_ephem_types=valid_ephem_types) + def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)): + self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) self.gnss_kf = GNSSKalman(GENERATED_DIR) - self.latest_epoch_fetched = GPSTime(0, 0) self.latest_time_msg = None def process_ublox_msg(self, ublox_msg, ublox_mono_time: int): @@ -43,7 +40,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int): # To get a position fix a minimum of 5 measurements are needed. # Each report can contain less and some measurements can't be processed. corrected_measurements = [] - if len(pos_fix) > 0 and linalg.norm(pos_fix[1]) < 100: + if len(pos_fix) > 0 and abs(np.array(pos_fix[1])).mean() < 1000: corrected_measurements = correct_measurements(measurements, pos_fix[0][:3], self.astro_dog) t = ublox_mono_time * 1e-9 @@ -110,18 +107,15 @@ def init_gnss_localizer(self, est_pos): def orbit_thread(self, end_event: threading.Event): while not end_event.is_set(): if self.latest_time_msg: - self.fetch_orbits(self.latest_time_msg) + self.fetch_orbits(self.latest_time_msg + SECS_IN_MIN) time.sleep(0.1) def fetch_orbits(self, t: GPSTime): - if self.latest_epoch_fetched < t + SECS_IN_MIN: - cloudlog.info("Start to download/parse orbits") - orbit_ephems = self.astro_dog.download_parse_orbit_data(t, skip_before_epoch=t - 2 * SECS_IN_HR) - if len(orbit_ephems) > 0: - cloudlog.info(f"downloaded and parsed correctly new orbits {len(orbit_ephems)}, Constellations:{set([e.prn[0] for e in orbit_ephems])}") - self.astro_dog.add_ephems(orbit_ephems, self.astro_dog.orbits) - latest_orbit = max(orbit_ephems, key=lambda e: e.epoch) # type: ignore - self.latest_epoch_fetched = latest_orbit.epoch + if t not in self.astro_dog.orbit_fetched_times: + cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") + start_time = time.monotonic() + self.astro_dog.get_orbit_data(t, only_predictions=True) + cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.2f}s") def create_measurement_msg(meas: GNSSMeasurement): diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index baa17792bddc57..8cb0773b37eda8 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -53,7 +53,6 @@ def test_laika_online(self): def test_laika_online_nav_only(self): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.NAV) correct_msgs = verify_messages(self.logs, laikad) - correct_msgs_expected = 560 self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @@ -86,7 +85,7 @@ def test_laika_get_orbits(self): break # Pretend thread has loaded the orbits on startup by using the time of the first gps message. laikad.fetch_orbits(first_gps_time) - self.assertEqual(31, len(laikad.astro_dog.orbits.keys())) + self.assertEqual(29, len(laikad.astro_dog.orbits.keys())) correct_msgs = verify_messages(self.logs, laikad) correct_msgs_expected = 560 self.assertEqual(correct_msgs_expected, len(correct_msgs)) @@ -96,10 +95,11 @@ def test_laika_get_orbits(self): def test_laika_get_orbits_now(self): laikad = Laikad(auto_update=False) laikad.fetch_orbits(GPSTime.from_datetime(datetime.utcnow())) - print(laikad.latest_epoch_fetched.as_datetime()) - - print(min(laikad.astro_dog.orbits[list(laikad.astro_dog.orbits.keys())[0]], key=lambda e: e.epoch).epoch.as_datetime()) - + prn = "G01" + self.assertLess(0, len(laikad.astro_dog.orbits[prn])) + prn = "R01" + self.assertLess(0, len(laikad.astro_dog.orbits[prn])) + print(min(laikad.astro_dog.orbits[prn], key=lambda e: e.epoch).epoch.as_datetime()) if __name__ == "__main__": unittest.main() From b215d611b1adf5ea2b93e6628936f2e73539f34f Mon Sep 17 00:00:00 2001 From: ClockeNessMnstr Date: Tue, 7 Jun 2022 12:41:03 -0400 Subject: [PATCH 006/436] update DH names + notes for MPC output curvatures (#24701) * update names + notes for MPC outputs "current_curvature" is not the correct description of what the MPC is outputting in it's curvature_ego state. The MPC is integrating it's free variable, curvature_rate, such that curvature[0] is actually the desired_curvature before any delay. inversely: the curvature_rate_desired is the desired rate of change to the setpoint and not the actual curvature rate. If we were to set the initial curvature = measured curvature in the MPC initiation these names would be correct. This was possibly how it was initially set up but the nomenclature here is now confusing. * more notes * match * Clarify #1 --- selfdrive/controls/lib/drive_helpers.py | 23 +++++++++++---------- selfdrive/controls/lib/latcontrol_indi.py | 1 + selfdrive/controls/lib/latcontrol_torque.py | 2 ++ selfdrive/controls/lib/lateral_planner.py | 3 +++ 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 4afa8d89edbaea..91981259aa04a2 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -97,26 +97,27 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): psis = [0.0]*CONTROL_N curvatures = [0.0]*CONTROL_N curvature_rates = [0.0]*CONTROL_N - + v_ego = max(v_ego, 0.1) + # TODO this needs more thought, use .2s extra for now to estimate other delays delay = CP.steerActuatorDelay + .2 - current_curvature = curvatures[0] - psi = interp(delay, T_IDXS[:CONTROL_N], psis) - desired_curvature_rate = curvature_rates[0] - + # MPC can plan to turn the wheel and turn back before t_delay. This means # in high delay cases some corrections never even get commanded. So just use # psi to calculate a simple linearization of desired curvature - curvature_diff_from_psi = psi / (max(v_ego, 1e-1) * delay) - current_curvature - desired_curvature = current_curvature + 2 * curvature_diff_from_psi - - v_ego = max(v_ego, 0.1) + current_curvature_desired = curvatures[0] + psi = interp(delay, T_IDXS[:CONTROL_N], psis) + average_curvature_desired = psi / (v_ego * delay) + desired_curvature = 2 * average_curvature_desired - current_curvature_desired + + # This is the "desired rate of the setpoint" not an actual desired rate + desired_curvature_rate = curvature_rates[0] max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) safe_desired_curvature_rate = clip(desired_curvature_rate, -max_curvature_rate, max_curvature_rate) safe_desired_curvature = clip(desired_curvature, - current_curvature - max_curvature_rate * DT_MDL, - current_curvature + max_curvature_rate * DT_MDL) + current_curvature_desired - max_curvature_rate * DT_MDL, + current_curvature_desired + max_curvature_rate * DT_MDL) return safe_desired_curvature, safe_desired_curvature_rate diff --git a/selfdrive/controls/lib/latcontrol_indi.py b/selfdrive/controls/lib/latcontrol_indi.py index b5041eb172fc2c..79c881d11be37e 100644 --- a/selfdrive/controls/lib/latcontrol_indi.py +++ b/selfdrive/controls/lib/latcontrol_indi.py @@ -78,6 +78,7 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi steers_des += math.radians(params.angleOffsetDeg) indi_log.steeringAngleDesiredDeg = math.degrees(steers_des) + # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature rate_des = VM.get_steer_from_curvature(-desired_curvature_rate, CS.vEgo, 0) indi_log.steeringRateDesiredDeg = math.degrees(rate_des) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 8b41dbf402c59d..47f0b69f2bda76 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -59,6 +59,8 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi actual_curvature_llk = llk.angularVelocityCalibrated.value[2] / CS.vEgo actual_curvature = interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_llk]) desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + + # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 actual_lateral_accel = actual_curvature * CS.vEgo ** 2 diff --git a/selfdrive/controls/lib/lateral_planner.py b/selfdrive/controls/lib/lateral_planner.py index 6349f05cc216db..f3f59ee308d2a8 100644 --- a/selfdrive/controls/lib/lateral_planner.py +++ b/selfdrive/controls/lib/lateral_planner.py @@ -79,6 +79,9 @@ def update(self, sm): y_pts, heading_pts) # init state for next + # mpc.u_sol is the desired curvature rate given x0 curv state. + # with x0[3] = measured_curvature, this would be the actual desired rate. + # instead, interpolate x_sol so that x0[3] is the desired curvature for lat_control. self.x0[3] = interp(DT_MDL, self.t_idxs[:LAT_MPC_N + 1], self.lat_mpc.x_sol[:, 3]) # Check for infeasible MPC solution From e7234e22b4d11cc8f349d02855df9b3414096c0d Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 7 Jun 2022 20:55:39 +0200 Subject: [PATCH 007/436] Laikad: Use process for parsing orbits (#24769) * Use Process instead of Thread to fetch orbits * small refactor * Cleanup --- selfdrive/locationd/laikad.py | 66 ++++++++++++++----------- selfdrive/locationd/test/test_laikad.py | 10 ++-- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index d8e32005f86e50..f0c52e7e03aa04 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import threading import time -from typing import List +from multiprocessing import Process, Queue +from typing import List, Optional import numpy as np from collections import defaultdict @@ -27,14 +27,17 @@ class Laikad: def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) self.gnss_kf = GNSSKalman(GENERATED_DIR) - self.latest_time_msg = None + self.orbit_p: Optional[Process] = None + self.orbit_q = Queue() def process_ublox_msg(self, ublox_msg, ublox_mono_time: int): if ublox_msg.which == 'measurementReport': report = ublox_msg.measurementReport - new_meas = read_raw_ublox(report) if report.gpsWeek > 0: - self.latest_time_msg = GPSTime(report.gpsWeek, report.rcvTow) + latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) + self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block=False) + new_meas = read_raw_ublox(report) + measurements = process_measurements(new_meas, self.astro_dog) pos_fix = calc_pos_fix(measurements, min_measurements=4) # To get a position fix a minimum of 5 measurements are needed. @@ -104,18 +107,28 @@ def init_gnss_localizer(self, est_pos): self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) - def orbit_thread(self, end_event: threading.Event): - while not end_event.is_set(): - if self.latest_time_msg: - self.fetch_orbits(self.latest_time_msg + SECS_IN_MIN) - time.sleep(0.1) + def get_orbit_data(self, t: GPSTime, queue): + cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") + start_time = time.monotonic() + self.astro_dog.get_orbit_data(t, only_predictions=True) + cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.2f}s") + queue.put((self.astro_dog.orbits, self.astro_dog.orbit_fetched_times)) - def fetch_orbits(self, t: GPSTime): + def fetch_orbits(self, t: GPSTime, block): if t not in self.astro_dog.orbit_fetched_times: - cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") - start_time = time.monotonic() - self.astro_dog.get_orbit_data(t, only_predictions=True) - cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.2f}s") + if self.orbit_p is None: + self.orbit_p = Process(target=self.get_orbit_data, args=(t, self.orbit_q)) + self.orbit_p.start() + ret = None + if block: + ret = self.orbit_q.get(block=True) + elif not self.orbit_q.empty(): + ret = self.orbit_q.get() + + if ret: + self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret + self.orbit_p.join() + self.orbit_p = None def create_measurement_msg(meas: GNSSMeasurement): @@ -162,21 +175,14 @@ def main(): pm = messaging.PubMaster(['gnssMeasurements']) laikad = Laikad() - - end_event = threading.Event() - threading.Thread(target=laikad.orbit_thread, args=(end_event,)).start() - try: - while not end_event.is_set(): - sm.update() - - if sm.updated['ubloxGnss']: - ublox_msg = sm['ubloxGnss'] - msg = laikad.process_ublox_msg(ublox_msg, sm.logMonoTime['ubloxGnss']) - if msg is not None: - pm.send('gnssMeasurements', msg) - except (KeyboardInterrupt, SystemExit): - end_event.set() - raise + while True: + sm.update() + + if sm.updated['ubloxGnss']: + ublox_msg = sm['ubloxGnss'] + msg = laikad.process_ublox_msg(ublox_msg, sm.logMonoTime['ubloxGnss']) + if msg is not None: + pm.send('gnssMeasurements', msg) if __name__ == "__main__": diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 8cb0773b37eda8..630a7525eb4752 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -83,18 +83,14 @@ def test_laika_get_orbits(self): if len(new_meas) != 0: first_gps_time = new_meas[0].recv_time break - # Pretend thread has loaded the orbits on startup by using the time of the first gps message. - laikad.fetch_orbits(first_gps_time) + # Pretend process has loaded the orbits on startup by using the time of the first gps message. + laikad.fetch_orbits(first_gps_time, block=True) self.assertEqual(29, len(laikad.astro_dog.orbits.keys())) - correct_msgs = verify_messages(self.logs, laikad) - correct_msgs_expected = 560 - self.assertEqual(correct_msgs_expected, len(correct_msgs)) - self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @unittest.skip("Use to debug live data") def test_laika_get_orbits_now(self): laikad = Laikad(auto_update=False) - laikad.fetch_orbits(GPSTime.from_datetime(datetime.utcnow())) + laikad.fetch_orbits(GPSTime.from_datetime(datetime.utcnow()), block=True) prn = "G01" self.assertLess(0, len(laikad.astro_dog.orbits[prn])) prn = "R01" From 4d836c6edd6e0f2639a3e2500794990da339eddc Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 7 Jun 2022 13:59:48 -0700 Subject: [PATCH 008/436] UI: remove DM icon (#24771) --- selfdrive/ui/qt/onroad.cc | 10 +--------- selfdrive/ui/qt/onroad.h | 5 ----- selfdrive/ui/ui.cc | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index ce61c094bd6bd7..e549f62a232253 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -166,7 +166,6 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { NvgWindow::NvgWindow(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraViewWidget("camerad", type, true, parent) { engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size}); - dm_img = loadPixmap("../assets/img_driver_face.png", {img_size, img_size}); } void NvgWindow::updateState(const UIState &s) { @@ -186,13 +185,11 @@ void NvgWindow::updateState(const UIState &s) { setProperty("speed", QString::number(std::nearbyint(cur_speed))); setProperty("maxSpeed", maxspeed_str); setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); - setProperty("hideDM", cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE); setProperty("status", s.status); - // update engageability and DM icons at 2Hz + // update engageability at 2Hz if (sm.frame % (UI_FREQ / 2) == 0) { setProperty("engageable", cs.getEngageable() || cs.getEnabled()); - setProperty("dmActive", sm["driverMonitoringState"].getDriverMonitoringState().getIsActiveMode()); } } @@ -234,11 +231,6 @@ void NvgWindow::drawHud(QPainter &p) { engage_img, bg_colors[status], 1.0); } - // dm icon - if (!hideDM) { - drawIcon(p, radius / 2 + (bdr_s * 2), rect().bottom() - footer_h / 2, - dm_img, QColor(0, 0, 0, 70), dmActive ? 1.0 : 0.2); - } p.restore(); } diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 6ca2b3c738506c..e1f665149e005e 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -32,8 +32,6 @@ class NvgWindow : public CameraViewWidget { Q_PROPERTY(QString maxSpeed MEMBER maxSpeed); Q_PROPERTY(bool is_cruise_set MEMBER is_cruise_set); Q_PROPERTY(bool engageable MEMBER engageable); - Q_PROPERTY(bool dmActive MEMBER dmActive); - Q_PROPERTY(bool hideDM MEMBER hideDM); Q_PROPERTY(int status MEMBER status); public: @@ -45,7 +43,6 @@ class NvgWindow : public CameraViewWidget { void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); QPixmap engage_img; - QPixmap dm_img; const int radius = 192; const int img_size = (radius / 2) * 1.5; QString speed; @@ -53,8 +50,6 @@ class NvgWindow : public CameraViewWidget { QString maxSpeed; bool is_cruise_set = false; bool engageable = false; - bool dmActive = false; - bool hideDM = false; int status = STATUS_DISENGAGED; protected: diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index c1af4ed6f42253..f7c2c90437f690 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -231,7 +231,7 @@ void UIState::updateStatus() { UIState::UIState(QObject *parent) : QObject(parent) { sm = std::make_unique>({ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "roadCameraState", - "pandaStates", "carParams", "driverMonitoringState", "sensorEvents", "carState", "liveLocationKalman", + "pandaStates", "carParams", "sensorEvents", "carState", "liveLocationKalman", "wideRoadCameraState", "managerState", "navInstruction", "navRoute", }); From 6a58dd808f3e168fe72129b09ca5f829a4c146db Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 7 Jun 2022 15:01:19 -0700 Subject: [PATCH 009/436] PlotJuggler: add torque control layout (#24726) * add torque control PJ layout * less custom transformation * Use curvature, less noisy * remove that --- .../plotjuggler/layouts/torque-controller.xml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tools/plotjuggler/layouts/torque-controller.xml diff --git a/tools/plotjuggler/layouts/torque-controller.xml b/tools/plotjuggler/layouts/torque-controller.xml new file mode 100644 index 00000000000000..661bfe094de445 --- /dev/null +++ b/tools/plotjuggler/layouts/torque-controller.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + return (value * v1 ^ 2) - (v2 * 9.81) + /controlsState/curvature + + /carState/vEgo + /liveParameters/roll + + + + + return (value * v1 ^ 2) - (v2 * 9.81) + /controlsState/desiredCurvature + + /carState/vEgo + /liveParameters/roll + + + + + + From bdc05fd13cdbdb4879fbadf5d5262bd8236b3243 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 7 Jun 2022 17:18:03 -0700 Subject: [PATCH 010/436] Honda: rename Bosch harness to Bosch A --- selfdrive/car/docs_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 381bcfa7f869d3..95561872accfce 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -120,7 +120,7 @@ def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: class Harness(Enum): nidec = "Honda Nidec" - bosch = "Honda Bosch" + bosch = "Honda Bosch A" toyota = "Toyota" subaru = "Subaru" fca = "FCA" From b95c449847da37809ab3cd9a582cd910a612a1cf Mon Sep 17 00:00:00 2001 From: Jason Shuler Date: Tue, 7 Jun 2022 20:23:08 -0400 Subject: [PATCH 011/436] GM: interface cleanup (#24774) * Move all defaults above models * Remove reduntant/defaults * make minEnableSpeed common * Update selfdrive/car/gm/interface.py Co-authored-by: Adeeb Shihadeh Co-authored-by: Shane Smiskol Co-authored-by: Adeeb Shihadeh --- selfdrive/car/gm/interface.py | 36 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index ce14faec5edc18..bc430865862f8f 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -58,7 +58,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.openpilotLongitudinalControl = True tire_stiffness_factor = 0.444 # not optimized yet - # Start with a baseline lateral tuning for all GM vehicles. Override tuning as needed in each model section below. + # Start with a baseline tuning for all GM vehicles. Override tuning as needed in each model section below. ret.minSteerSpeed = 7 * CV.MPH_TO_MS ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] @@ -66,14 +66,22 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerRateCost = 1.0 ret.steerActuatorDelay = 0.1 # Default delay, not measured yet + ret.longitudinalTuning.kpBP = [5., 35.] + ret.longitudinalTuning.kpV = [2.4, 1.5] + ret.longitudinalTuning.kiBP = [0.] + ret.longitudinalTuning.kiV = [0.36] + + ret.steerLimitTimer = 0.4 + ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz + + # supports stop and go, but initial engage must (conservatively) be above 18mph + ret.minEnableSpeed = 18 * CV.MPH_TO_MS + if candidate == CAR.VOLT: - # supports stop and go, but initial engage must be above 18mph (which include conservatism) - ret.minEnableSpeed = 18 * CV.MPH_TO_MS ret.mass = 1607. + STD_CARGO_KG ret.wheelbase = 2.69 ret.steerRatio = 17.7 # Stock 15.7, LiveParameters tire_stiffness_factor = 0.469 # Stock Michelin Energy Saver A/S, LiveParameters - ret.steerRatioRear = 0. ret.centerToFront = ret.wheelbase * 0.45 # Volt Gen 1, TODO corner weigh ret.maxLateralAccel = 1.6 @@ -85,12 +93,9 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerActuatorDelay = 0.2 elif candidate == CAR.MALIBU: - # supports stop and go, but initial engage must be above 18mph (which include conservatism) - ret.minEnableSpeed = 18 * CV.MPH_TO_MS ret.mass = 1496. + STD_CARGO_KG ret.wheelbase = 2.83 ret.steerRatio = 15.8 - ret.steerRatioRear = 0. ret.centerToFront = ret.wheelbase * 0.4 # wild guess elif candidate == CAR.HOLDEN_ASTRA: @@ -98,34 +103,27 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.wheelbase = 2.662 # Remaining parameters copied from Volt for now ret.centerToFront = ret.wheelbase * 0.4 - ret.minEnableSpeed = 18 * CV.MPH_TO_MS ret.steerRatio = 15.7 - ret.steerRatioRear = 0. elif candidate == CAR.ACADIA: ret.minEnableSpeed = -1. # engage speed is decided by pcm ret.mass = 4353. * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.86 ret.steerRatio = 14.4 # end to end is 13.46 - ret.steerRatioRear = 0. ret.centerToFront = ret.wheelbase * 0.4 ret.maxLateralAccel = 1.4 - ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_acadia() + ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_acadia() elif candidate == CAR.BUICK_REGAL: - ret.minEnableSpeed = 18 * CV.MPH_TO_MS ret.mass = 3779. * CV.LB_TO_KG + STD_CARGO_KG # (3849+3708)/2 ret.wheelbase = 2.83 # 111.4 inches in meters ret.steerRatio = 14.4 # guess for tourx - ret.steerRatioRear = 0. ret.centerToFront = ret.wheelbase * 0.4 # guess for tourx elif candidate == CAR.CADILLAC_ATS: - ret.minEnableSpeed = 18 * CV.MPH_TO_MS ret.mass = 1601. + STD_CARGO_KG ret.wheelbase = 2.78 ret.steerRatio = 15.3 - ret.steerRatioRear = 0. ret.centerToFront = ret.wheelbase * 0.49 elif candidate == CAR.ESCALADE_ESV: @@ -148,14 +146,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, tire_stiffness_factor=tire_stiffness_factor) - ret.longitudinalTuning.kpBP = [5., 35.] - ret.longitudinalTuning.kpV = [2.4, 1.5] - ret.longitudinalTuning.kiBP = [0.] - ret.longitudinalTuning.kiV = [0.36] - - ret.steerLimitTimer = 0.4 - ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz - return ret # returns a car.CarState From 30e1f282134f7693a451e783b1d2d8ee68a79c06 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 7 Jun 2022 17:49:07 -0700 Subject: [PATCH 012/436] IsoTpParallelQuery: handle response pending (#24724) * handle response pending * match commit * remove total timeout, just keep track of individual response timeouts * fix * add back total timeout * this isn't reliable enough * keep track of pending responses to print warning * tx_addr is (addr, subaddr) * debug only query hyundai import time reponse pending no cache all cars no timeout to test before * Revert "debug" This reverts commit abe9cfc1b668034d7fa5ca5cbe9efe8834db3e7b. * always print pending always debug * Only debug * Update selfdrive/car/isotp_parallel_query.py * remove variable only for debugging --- selfdrive/car/isotp_parallel_query.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index becd66ba1f360f..dc79babb138ac9 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -10,7 +10,7 @@ class IsoTpParallelQuery: - def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addr=False, debug=False): + def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offset=0x8, functional_addr=False, debug=False, response_pending_timeout=10): self.sendcan = sendcan self.logcan = logcan self.bus = bus @@ -18,6 +18,7 @@ def __init__(self, sendcan, logcan, bus, addrs, request, response, response_offs self.response = response self.debug = debug self.functional_addr = functional_addr + self.response_pending_timeout = response_pending_timeout self.real_addrs = [] for a in addrs: @@ -71,10 +72,7 @@ def _drain_rx(self): messaging.drain_sock(self.logcan) self.msg_buffer = defaultdict(list) - def get_data(self, timeout, total_timeout=None): - if total_timeout is None: - total_timeout = 10 * timeout - + def get_data(self, timeout, total_timeout=60.): self._drain_rx() # Create message objects @@ -100,7 +98,7 @@ def get_data(self, timeout, total_timeout=None): results = {} start_time = time.monotonic() - last_response_time = start_time + response_timeouts = {tx_addr: start_time + timeout for tx_addr in self.msg_addrs} while True: self.rx() @@ -123,7 +121,7 @@ def get_data(self, timeout, total_timeout=None): response_valid = dat[:len(expected_response)] == expected_response if response_valid: - last_response_time = time.monotonic() + response_timeouts[tx_addr] = time.monotonic() + timeout if counter + 1 < len(self.request): msg.send(self.request[counter + 1]) request_counter[tx_addr] += 1 @@ -131,13 +129,19 @@ def get_data(self, timeout, total_timeout=None): results[tx_addr] = dat[len(expected_response):] request_done[tx_addr] = True else: - request_done[tx_addr] = True - cloudlog.warning(f"iso-tp query bad response: {tx_addr} - 0x{dat.hex()}") + error_code = dat[2] if len(dat) > 2 else -1 + if error_code == 0x78: + response_timeouts[tx_addr] = time.monotonic() + self.response_pending_timeout + if self.debug: + cloudlog.warning(f"iso-tp query response pending: {tx_addr}") + else: + request_done[tx_addr] = True + cloudlog.warning(f"iso-tp query bad response: {tx_addr} - 0x{dat.hex()}") cur_time = time.monotonic() - if cur_time - last_response_time > timeout: + if cur_time - max(response_timeouts.values()) > 0: for tx_addr in msgs: - if (request_counter[tx_addr] > 0) and (not request_done[tx_addr]): + if request_counter[tx_addr] > 0 and not request_done[tx_addr]: cloudlog.warning(f"iso-tp query timeout after receiving response: {tx_addr}") break From afe208ecb996d10cda598fe2f2fade7353d420cf Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Tue, 7 Jun 2022 19:16:36 -0700 Subject: [PATCH 013/436] Install readme (#24776) README: better install on device instructions --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9d279fb84d69d6..c426b839d5e459 100755 --- a/README.md +++ b/README.md @@ -35,16 +35,17 @@ What is openpilot? -Running in a car +Running on a dedicated device in a car ------ To use openpilot in a car, you need four things -* This software. It's free and available right here. +* A supported device to run this software: a [comma three](https://comma.ai/shop/products/three). +* This software. The setup procedure of the comma three allows the user to enter a url for custom software. +The url, openpilot.comma.ai will install the release version of openpilot. To install openpilot master, you can use installer.comma.ai/commaai/master, and replacing commaai with another github username can install a fork. * One of [the 150+ supported cars](docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, and more. If your car is not supported, but has adaptive cruise control and lane keeping assist, it's likely able to run openpilot. -* A supported device to run this software: a [comma three](https://comma.ai/shop/products/three), or if you like to experiment, a [Ubuntu computer with webcams](https://github.com/commaai/openpilot/tree/master/tools/webcam). -* A way to connect to your car. With a comma three, you need only a [car harness](https://comma.ai/shop/products/car-harness). With a PC, you also need a [black panda](https://comma.ai/shop/products/panda). +* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car. -We have detailed instructions for [how to install the device in a car](https://comma.ai/setup). +We have detailed instructions for [how to mount the device in a car](https://comma.ai/setup). Running on PC ------ @@ -55,6 +56,7 @@ With openpilot's tools you can plot logs, replay drives and watch the full-res c You can also run openpilot in simulation [with the CARLA simulator](tools/sim/README.md). This allows openpilot to drive around a virtual car on your Ubuntu machine. The whole setup should only take a few minutes, but does require a decent GPU. +A PC running openpilot can also control your vehicle if it is connected to a [a webcam](https://github.com/commaai/openpilot/tree/master/tools/webcam), a [black panda](https://comma.ai/shop/products/panda), and [a harness](https://comma.ai/shop/products/car-harness). Community and Contributing ------ From ce6b8f6a461d864d52ef8c6a00466d951aecbdd6 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Wed, 8 Jun 2022 00:57:15 -0400 Subject: [PATCH 014/436] VW MQB: Add FW for 2020 Volkswagen Tiguan (#24777) --- selfdrive/car/volkswagen/values.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 44743c948e0636..8cba9988bdd32f 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -533,6 +533,7 @@ class VWCarInfo(CarInfo): }, CAR.TIGUAN_MK2: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8704E906027NB\xf1\x899504', b'\xf1\x8704L906026EJ\xf1\x893661', b'\xf1\x8704L906027G \xf1\x899893', b'\xf1\x875N0906259 \xf1\x890002', @@ -544,6 +545,7 @@ class VWCarInfo(CarInfo): b'\xf1\x8709G927158DT\xf1\x893698', b'\xf1\x8709G927158GC\xf1\x893821', b'\xf1\x8709G927158GD\xf1\x893820', + b'\xf1\x870D9300043 \xf1\x895202', b'\xf1\x870DL300011N \xf1\x892001', b'\xf1\x870DL300011N \xf1\x892012', b'\xf1\x870DL300013A \xf1\x893005', @@ -555,11 +557,13 @@ class VWCarInfo(CarInfo): b'\xf1\x875Q0959655BM\xf1\x890403\xf1\x82\02316143231313500314641011750179333423100', b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\02312110031333300314240583752379333423100', b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\02331310031333336313140013950399333423100', + b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333334313140013750379333423100', b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x1331310031333334313140573752379333423100', b'\xf1\x875Q0959655CB\xf1\x890421\xf1\x82\x1316143231313500314647021750179333613100', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820529A6060603', + b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A60604A1', b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\00571A60634A1', b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A60604A1', b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A60604A1', From abef7ca32cac34fd15cb17e1fafb96e961486ba3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 7 Jun 2022 22:43:09 -0700 Subject: [PATCH 015/436] Kia Ceed: fix eps ECU type (#24767) should be eps --- selfdrive/car/hyundai/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 1cd2acabebec4f..54bbd80475330a 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -845,7 +845,7 @@ class Buttons: }, CAR.KIA_CEED: { (Ecu.fwdRadar, 0x7D0, None): [b'\xf1\000CD__ SCC F-CUP 1.00 1.02 99110-J7000 ', ], - (Ecu.esp, 0x7D4, None): [b'\xf1\000CD MDPS C 1.00 1.06 56310-XX000 4CDEC106', ], + (Ecu.eps, 0x7D4, None): [b'\xf1\000CD MDPS C 1.00 1.06 56310-XX000 4CDEC106', ], (Ecu.fwdCamera, 0x7C4, None): [b'\xf1\000CD LKAS AT EUR LHD 1.00 1.01 99211-J7000 B40', ], (Ecu.engine, 0x7E0, None): [b'\001TCD-JECU4F202H0K', ], (Ecu.transmission, 0x7E1, None): [ From c8db61a3c7aba6f96c9f282f4984c84fd6550f62 Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Wed, 8 Jun 2022 00:47:10 -0500 Subject: [PATCH 016/436] Add missing MIRAI ESP f/w (#24779) * Add missing ESP f/w `@JaySheikh#7759` 2021 Mirai DongleID/route b2c2b20082938bed|2022-06-08--05-04-03 [confirmed working] * formatting Co-authored-by: Shane Smiskol --- selfdrive/car/toyota/values.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 3204d36b5aded2..a9517d26bf9633 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1825,7 +1825,10 @@ class ToyotaCarInfo(CarInfo): }, CAR.MIRAI: { (Ecu.esp, 0x7D1, None): [b'\x01898A36203000\x00\x00\x00\x00',], - (Ecu.esp, 0x7B0, None): [b'\x01F15266203200\x00\x00\x00\x00',], # a second ESP ECU + (Ecu.esp, 0x7B0, None): [ # a second ESP ECU + b'\x01F15266203200\x00\x00\x00\x00', + b'\x01F15266203500\x00\x00\x00\x00', + ], (Ecu.eps, 0x7A1, None): [b'\x028965B6204100\x00\x00\x00\x008965B6203100\x00\x00\x00\x00',], (Ecu.fwdRadar, 0x750, 0xf): [b'\x018821F6201200\x00\x00\x00\x00',], (Ecu.fwdCamera, 0x750, 0x6d): [b'\x028646F6201400\x00\x00\x00\x008646G5301200\x00\x00\x00\x00',], From 45c43f9c8719efc300a0cb373f1dc57e0abe3ece Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 8 Jun 2022 12:32:22 +0200 Subject: [PATCH 017/436] Laikad: kill process correctly. Fixes CI (#24780) Kill process correctly --- selfdrive/locationd/laikad.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index f0c52e7e03aa04..a55302458e9e19 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -130,6 +130,10 @@ def fetch_orbits(self, t: GPSTime, block): self.orbit_p.join() self.orbit_p = None + def __del__(self): + if self.orbit_p is not None: + self.orbit_p.kill() + def create_measurement_msg(meas: GNSSMeasurement): c = log.GnssMeasurements.CorrectedMeasurement.new_message() From a9bdc792a184d191824d2ec089658eb3a96f1e45 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 8 Jun 2022 16:33:05 +0200 Subject: [PATCH 018/436] LaikaD: Fix offline handling (#24781) * Test handling no internet correctly. * Clean * Comment * remove del --- selfdrive/locationd/laikad.py | 30 ++++++++++++++----------- selfdrive/locationd/test/test_laikad.py | 22 ++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index a55302458e9e19..4ec159cd254140 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -30,12 +30,12 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.orbit_p: Optional[Process] = None self.orbit_q = Queue() - def process_ublox_msg(self, ublox_msg, ublox_mono_time: int): + def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': report = ublox_msg.measurementReport if report.gpsWeek > 0: latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) - self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block=False) + self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) new_meas = read_raw_ublox(report) measurements = process_measurements(new_meas, self.astro_dog) @@ -110,25 +110,29 @@ def init_gnss_localizer(self, est_pos): def get_orbit_data(self, t: GPSTime, queue): cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") start_time = time.monotonic() - self.astro_dog.get_orbit_data(t, only_predictions=True) + try: + self.astro_dog.get_orbit_data(t, only_predictions=True) + except RuntimeError as e: + cloudlog.info(f"No orbit data found. {e}") + return cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.2f}s") - queue.put((self.astro_dog.orbits, self.astro_dog.orbit_fetched_times)) + if queue is not None: + queue.put((self.astro_dog.orbits, self.astro_dog.orbit_fetched_times)) def fetch_orbits(self, t: GPSTime, block): if t not in self.astro_dog.orbit_fetched_times: + if block: + self.get_orbit_data(t, None) + return if self.orbit_p is None: self.orbit_p = Process(target=self.get_orbit_data, args=(t, self.orbit_q)) self.orbit_p.start() - ret = None - if block: - ret = self.orbit_q.get(block=True) - elif not self.orbit_q.empty(): + if not self.orbit_q.empty(): ret = self.orbit_q.get() - - if ret: - self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret - self.orbit_p.join() - self.orbit_p = None + if ret: + self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret + self.orbit_p.join() + self.orbit_p = None def __del__(self): if self.orbit_p is not None: diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 630a7525eb4752..7a8c1ecb13564d 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import unittest from datetime import datetime +from unittest import mock +from unittest.mock import Mock from laika.ephemeris import EphemerisType from laika.gps_time import GPSTime @@ -21,7 +23,7 @@ def get_log(segs=range(0)): def verify_messages(lr, laikad): good_msgs = [] for m in lr: - msg = laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime) + msg = laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime, block=True) if msg is not None and len(msg.gnssMeasurements.correctedMeasurements) > 0: good_msgs.append(msg) return good_msgs @@ -52,28 +54,21 @@ def test_laika_online(self): def test_laika_online_nav_only(self): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.NAV) + # Disable fetch_orbits to test NAV only + laikad.fetch_orbits = Mock() correct_msgs = verify_messages(self.logs, laikad) correct_msgs_expected = 560 self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) - def test_laika_offline(self): - # Set auto_update to false forces to use ephemeris messages + @mock.patch('laika.downloader.download_and_cache_file') + def test_laika_offline(self, downloader_mock): + downloader_mock.side_effect = IOError laikad = Laikad(auto_update=False) correct_msgs = verify_messages(self.logs, laikad) - self.assertEqual(256, len(correct_msgs)) self.assertEqual(256, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) - def test_laika_offline_ephem_at_start(self): - # Test offline but process ephemeris msgs of segment first - laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.NAV) - ephemeris_logs = [m for m in self.logs if m.ubloxGnss.which() == 'ephemeris'] - correct_msgs = verify_messages(ephemeris_logs+self.logs, laikad) - - self.assertEqual(554, len(correct_msgs)) - self.assertGreaterEqual(554, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) - def test_laika_get_orbits(self): laikad = Laikad(auto_update=False) first_gps_time = None @@ -97,5 +92,6 @@ def test_laika_get_orbits_now(self): self.assertLess(0, len(laikad.astro_dog.orbits[prn])) print(min(laikad.astro_dog.orbits[prn], key=lambda e: e.epoch).epoch.as_datetime()) + if __name__ == "__main__": unittest.main() From d6c07a6b158595c1ab10256dc6eee4fbdf902379 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Wed, 8 Jun 2022 20:13:46 -0700 Subject: [PATCH 019/436] fullframe DM model (#24762) * get log * simplify two nonsense * not needed * libyuv is a joke * clean up * try small * fast but not bad * working * clean up driverview * simplified * thats mirrored * smol * tweak * ref is screen * w/ ee * update camera model * no if TICI * start * update pose thresh * less cpu more dsp * new libyuv * new snpe * add files * test * should be fast * update out len * trigger test * use master snpe * add cereal * update cereal * refactor parsing * missing ; * get * wrong type * test model * use driver data * 10829278-72fe-4283-a118-2cef959ce174/1550 * no pf * adapt driverview * ; * rhd learner * update libyuv buildi x64 * ad4337ea * remove blink slack * test * no * use toggle * b16 * fix for nv12 * 5b02cff5 both * update test * update cereal * update cereal * update cereal * v2 packets * revert libyuv * no / * update snpemodel * ; * memcpy * fix test * use toggle in driverview * update power * update replay * Revert "update replay" This reverts commit 1d0979ca59dbc89bc5890656e9501e83f0556d50. * update model ref * halve cpu * fake 8bit onnx runner * same thresh as report * cereal master Co-authored-by: Comma Device --- cereal | 2 +- common/modeldata.h | 6 - selfdrive/camerad/cameras/camera_common.cc | 2 +- selfdrive/camerad/cameras/camera_qcom2.cc | 2 +- selfdrive/hardware/tici/test_power_draw.py | 2 +- selfdrive/modeld/dmonitoringmodeld.cc | 6 +- selfdrive/modeld/models/dmonitoring.cc | 234 ++++++------------ selfdrive/modeld/models/dmonitoring.h | 33 ++- .../modeld/models/dmonitoring_model.current | 4 +- .../modeld/models/dmonitoring_model.onnx | 4 +- .../modeld/models/dmonitoring_model_q.dlc | 4 +- selfdrive/modeld/runners/onnx_runner.py | 21 +- selfdrive/modeld/runners/onnxmodel.cc | 6 +- selfdrive/modeld/runners/onnxmodel.h | 3 +- selfdrive/modeld/runners/snpemodel.cc | 12 +- selfdrive/modeld/runners/snpemodel.h | 3 +- selfdrive/monitoring/dmonitoringd.py | 7 +- selfdrive/monitoring/driver_monitor.py | 88 +++---- selfdrive/monitoring/test_monitoring.py | 24 +- selfdrive/test/process_replay/model_replay.py | 8 +- .../process_replay/model_replay_ref_commit | 2 +- .../test/process_replay/process_replay.py | 2 +- selfdrive/test/test_onroad.py | 6 +- selfdrive/ui/qt/offroad/driverview.cc | 43 ++-- selfdrive/ui/qt/widgets/cameraview.cc | 9 +- 25 files changed, 225 insertions(+), 308 deletions(-) diff --git a/cereal b/cereal index 7cbb4f1c8cb7cf..96cbed052ab1d6 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 7cbb4f1c8cb7cfc798a058c611801442b40feb52 +Subproject commit 96cbed052ab1d69ea15da94dc2b882d59a786347 diff --git a/common/modeldata.h b/common/modeldata.h index 06d9398031f1c1..410a69ea654cb5 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -24,12 +24,6 @@ constexpr auto T_IDXS_FLOAT = build_idxs(10.0); constexpr auto X_IDXS = build_idxs(192.0); constexpr auto X_IDXS_FLOAT = build_idxs(192.0); -namespace tici_dm_crop { - const int x_offset = -72; - const int y_offset = -144; - const int width = 954; -}; - const mat3 fcam_intrinsic_matrix = (mat3){{2648.0, 0.0, 1928.0 / 2, 0.0, 2648.0, 1208.0 / 2, 0.0, 0.0, 1.0}}; diff --git a/selfdrive/camerad/cameras/camera_common.cc b/selfdrive/camerad/cameras/camera_common.cc index 049324f0856a8b..21e225d5380ff0 100644 --- a/selfdrive/camerad/cameras/camera_common.cc +++ b/selfdrive/camerad/cameras/camera_common.cc @@ -239,7 +239,7 @@ static kj::Array yuv420_to_jpeg(const CameraBuf *b, int thumbnail_w int in_stride = b->cur_yuv_buf->stride; // make the buffer big enough. jpeg_write_raw_data requires 16-pixels aligned height to be used. - std::unique_ptr buf(new uint8_t[(thumbnail_width * ((thumbnail_height + 15) & ~15) * 3) / 2]); + std::unique_ptr buf(new uint8_t[(thumbnail_width * ((thumbnail_height + 15) & ~15) * 3) / 2]); uint8_t *y_plane = buf.get(); uint8_t *u_plane = y_plane + thumbnail_width * thumbnail_height; uint8_t *v_plane = u_plane + (thumbnail_width * thumbnail_height) / 4; diff --git a/selfdrive/camerad/cameras/camera_qcom2.cc b/selfdrive/camerad/cameras/camera_qcom2.cc index d43beb0921e0f0..b949dfb80fa049 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.cc +++ b/selfdrive/camerad/cameras/camera_qcom2.cc @@ -837,7 +837,7 @@ void cameras_init(VisionIpcServer *v, MultiCameraState *s, cl_device_id device_i s->road_cam.camera_init(s, v, CAMERA_ID_AR0231, 1, 20, device_id, ctx, VISION_STREAM_RGB_ROAD, VISION_STREAM_ROAD, !env_disable_road); s->wide_road_cam.camera_init(s, v, CAMERA_ID_AR0231, 0, 20, device_id, ctx, VISION_STREAM_RGB_WIDE_ROAD, VISION_STREAM_WIDE_ROAD, !env_disable_wide_road); - s->sm = new SubMaster({"driverState"}); + s->sm = new SubMaster({"driverStateV2"}); s->pm = new PubMaster({"roadCameraState", "driverCameraState", "wideRoadCameraState", "thumbnail"}); } diff --git a/selfdrive/hardware/tici/test_power_draw.py b/selfdrive/hardware/tici/test_power_draw.py index ab2d691a09217d..5eb84514e5ea74 100755 --- a/selfdrive/hardware/tici/test_power_draw.py +++ b/selfdrive/hardware/tici/test_power_draw.py @@ -21,7 +21,7 @@ class Proc: PROCS = [ Proc('camerad', 2.15), Proc('modeld', 1.0), - Proc('dmonitoringmodeld', 0.25), + Proc('dmonitoringmodeld', 0.35), Proc('encoderd', 0.23), ] diff --git a/selfdrive/modeld/dmonitoringmodeld.cc b/selfdrive/modeld/dmonitoringmodeld.cc index 68c49572e66696..cde13a9beeb6ba 100644 --- a/selfdrive/modeld/dmonitoringmodeld.cc +++ b/selfdrive/modeld/dmonitoringmodeld.cc @@ -12,7 +12,7 @@ ExitHandler do_exit; void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { - PubMaster pm({"driverState"}); + PubMaster pm({"driverStateV2"}); SubMaster sm({"liveCalibration"}); float calib[CALIB_LEN] = {0}; double last = 0; @@ -31,11 +31,11 @@ void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { } double t1 = millis_since_boot(); - DMonitoringResult res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); + DMonitoringModelResult model_res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); double t2 = millis_since_boot(); // send dm packet - dmonitoring_publish(pm, extra.frame_id, res, (t2 - t1) / 1000.0, model.output); + dmonitoring_publish(pm, extra.frame_id, model_res, (t2 - t1) / 1000.0, model.output); //printf("dmonitoring process: %.2fms, from last %.2fms\n", t2 - t1, t1 - last); last = t1; diff --git a/selfdrive/modeld/models/dmonitoring.cc b/selfdrive/modeld/models/dmonitoring.cc index e134ad3a5af5c4..20294c3f3ced62 100644 --- a/selfdrive/modeld/models/dmonitoring.cc +++ b/selfdrive/modeld/models/dmonitoring.cc @@ -10,8 +10,8 @@ #include "selfdrive/modeld/models/dmonitoring.h" -constexpr int MODEL_WIDTH = 320; -constexpr int MODEL_HEIGHT = 640; +constexpr int MODEL_WIDTH = 1440; +constexpr int MODEL_HEIGHT = 960; template static inline T *get_buffer(std::vector &buf, const size_t size) { @@ -19,199 +19,115 @@ static inline T *get_buffer(std::vector &buf, const size_t size) { return buf.data(); } -static inline void init_yuv_buf(std::vector &buf, const int width, int height) { - uint8_t *y = get_buffer(buf, width * height * 3 / 2); - uint8_t *u = y + width * height; - uint8_t *v = u + (width / 2) * (height / 2); - - // needed on comma two to make the padded border black - // equivalent to RGB(0,0,0) in YUV space - memset(y, 16, width * height); - memset(u, 128, (width / 2) * (height / 2)); - memset(v, 128, (width / 2) * (height / 2)); -} - void dmonitoring_init(DMonitoringModelState* s) { s->is_rhd = Params().getBool("IsRHD"); - for (int x = 0; x < std::size(s->tensor); ++x) { - s->tensor[x] = (x - 128.f) * 0.0078125f; - } - init_yuv_buf(s->resized_buf, MODEL_WIDTH, MODEL_HEIGHT); #ifdef USE_ONNX_MODEL - s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); + s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); #else - s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); + s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); #endif s->m->addCalib(s->calib, CALIB_LEN); } -static inline auto get_yuv_buf(std::vector &buf, const int width, int height) { - uint8_t *y = get_buffer(buf, width * height * 3 / 2); - uint8_t *u = y + width * height; - uint8_t *v = u + (width /2) * (height / 2); - return std::make_tuple(y, u, v); +void parse_driver_data(DriverStateResult &ds_res, const DMonitoringModelState* s, int out_idx_offset) { + for (int i = 0; i < 3; ++i) { + ds_res.face_orientation[i] = s->output[out_idx_offset+i] * REG_SCALE; + ds_res.face_orientation_std[i] = exp(s->output[out_idx_offset+6+i]); + } + for (int i = 0; i < 2; ++i) { + ds_res.face_position[i] = s->output[out_idx_offset+3+i] * REG_SCALE; + ds_res.face_position_std[i] = exp(s->output[out_idx_offset+9+i]); + } + for (int i = 0; i < 4; ++i) { + ds_res.ready_prob[i] = sigmoid(s->output[out_idx_offset+35+i]); + } + for (int i = 0; i < 2; ++i) { + ds_res.not_ready_prob[i] = sigmoid(s->output[out_idx_offset+39+i]); + } + ds_res.face_prob = sigmoid(s->output[out_idx_offset+12]); + ds_res.left_eye_prob = sigmoid(s->output[out_idx_offset+21]); + ds_res.right_eye_prob = sigmoid(s->output[out_idx_offset+30]); + ds_res.left_blink_prob = sigmoid(s->output[out_idx_offset+31]); + ds_res.right_blink_prob = sigmoid(s->output[out_idx_offset+32]); + ds_res.sunglasses_prob = sigmoid(s->output[out_idx_offset+33]); + ds_res.occluded_prob = sigmoid(s->output[out_idx_offset+34]); } -struct Rect {int x, y, w, h;}; -void crop_nv12_to_yuv(uint8_t *raw, int stride, int uv_offset, uint8_t *y, uint8_t *u, uint8_t *v, const Rect &rect) { - uint8_t *raw_y = raw; - uint8_t *raw_uv = raw_y + uv_offset; - for (int r = 0; r < rect.h / 2; r++) { - memcpy(y + 2 * r * rect.w, raw_y + (2 * r + rect.y) * stride + rect.x, rect.w); - memcpy(y + (2 * r + 1) * rect.w, raw_y + (2 * r + rect.y + 1) * stride + rect.x, rect.w); - for (int h = 0; h < rect.w / 2; h++) { - u[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2]; - v[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2 + 1]; - } - } +void fill_driver_data(cereal::DriverStateV2::DriverData::Builder ddata, const DriverStateResult &ds_res) { + ddata.setFaceOrientation(ds_res.face_orientation); + ddata.setFaceOrientationStd(ds_res.face_orientation_std); + ddata.setFacePosition(ds_res.face_position); + ddata.setFacePositionStd(ds_res.face_position_std); + ddata.setFaceProb(ds_res.face_prob); + ddata.setLeftEyeProb(ds_res.left_eye_prob); + ddata.setRightEyeProb(ds_res.right_eye_prob); + ddata.setLeftBlinkProb(ds_res.left_blink_prob); + ddata.setRightBlinkProb(ds_res.right_blink_prob); + ddata.setSunglassesProb(ds_res.sunglasses_prob); + ddata.setOccludedProb(ds_res.occluded_prob); + ddata.setReadyProb(ds_res.ready_prob); + ddata.setNotReadyProb(ds_res.not_ready_prob); } -DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { - const int cropped_height = tici_dm_crop::width / 1.33; - Rect crop_rect = {width / 2 - tici_dm_crop::width / 2 + tici_dm_crop::x_offset, - height / 2 - cropped_height / 2 + tici_dm_crop::y_offset, - cropped_height / 2, - cropped_height}; - if (!s->is_rhd) { - crop_rect.x += tici_dm_crop::width - crop_rect.w; - } +DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { + int v_off = height - MODEL_HEIGHT; + int h_off = (width - MODEL_WIDTH) / 2; + int yuv_buf_len = MODEL_WIDTH * MODEL_HEIGHT; - int resized_width = MODEL_WIDTH; - int resized_height = MODEL_HEIGHT; - - auto [cropped_y, cropped_u, cropped_v] = get_yuv_buf(s->cropped_buf, crop_rect.w, crop_rect.h); - if (!s->is_rhd) { - crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, cropped_y, cropped_u, cropped_v, crop_rect); - } else { - auto [mirror_y, mirror_u, mirror_v] = get_yuv_buf(s->premirror_cropped_buf, crop_rect.w, crop_rect.h); - crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, mirror_y, mirror_u, mirror_v, crop_rect); - libyuv::I420Mirror(mirror_y, crop_rect.w, - mirror_u, crop_rect.w / 2, - mirror_v, crop_rect.w / 2, - cropped_y, crop_rect.w, - cropped_u, crop_rect.w / 2, - cropped_v, crop_rect.w / 2, - crop_rect.w, crop_rect.h); - } + uint8_t *raw_buf = (uint8_t *) stream_buf; + // vertical crop free + uint8_t *raw_y_start = raw_buf + stride * v_off; - auto [resized_buf, resized_u, resized_v] = get_yuv_buf(s->resized_buf, resized_width, resized_height); - uint8_t *resized_y = resized_buf; - libyuv::FilterMode mode = libyuv::FilterModeEnum::kFilterBilinear; - libyuv::I420Scale(cropped_y, crop_rect.w, - cropped_u, crop_rect.w / 2, - cropped_v, crop_rect.w / 2, - crop_rect.w, crop_rect.h, - resized_y, resized_width, - resized_u, resized_width / 2, - resized_v, resized_width / 2, - resized_width, resized_height, - mode); - - - int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v - float *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); - // one shot conversion, O(n) anyway - // yuvframe2tensor, normalize - for (int r = 0; r < MODEL_HEIGHT/2; r++) { - for (int c = 0; c < MODEL_WIDTH/2; c++) { - // Y_ul - net_input_buf[(r*MODEL_WIDTH/2) + c + (0*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c]]; - // Y_dl - net_input_buf[(r*MODEL_WIDTH/2) + c + (1*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c]]; - // Y_ur - net_input_buf[(r*MODEL_WIDTH/2) + c + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c+1]]; - // Y_dr - net_input_buf[(r*MODEL_WIDTH/2) + c + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c+1]]; - // U - net_input_buf[(r*MODEL_WIDTH/2) + c + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_u[r*resized_width/2 + c]]; - // V - net_input_buf[(r*MODEL_WIDTH/2) + c + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_v[r*resized_width/2 + c]]; - } - } - - //printf("preprocess completed. %d \n", yuv_buf_len); - //FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); - //fwrite(resized_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); - //fclose(dump_yuv_file); + uint8_t *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); - // *** testing *** - // idat = np.frombuffer(open("/tmp/inputdump.yuv", "rb").read(), np.float32).reshape(6, 160, 320) - // imshow(cv2.cvtColor(tensor_to_frames(idat[None]/0.0078125+128)[0], cv2.COLOR_YUV2RGB_I420)) + // here makes a uint8 copy + for (int r = 0; r < MODEL_HEIGHT; ++r) { + memcpy(net_input_buf + r * MODEL_WIDTH, raw_y_start + r * stride + h_off, MODEL_WIDTH); + } - //FILE *dump_yuv_file2 = fopen("/tmp/inputdump.yuv", "wb"); - //fwrite(net_input_buf, MODEL_HEIGHT*MODEL_WIDTH*3/2, sizeof(float), dump_yuv_file2); - //fclose(dump_yuv_file2); + // printf("preprocess completed. %d \n", yuv_buf_len); + // FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); + // fwrite(net_input_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); + // fclose(dump_yuv_file); double t1 = millis_since_boot(); - s->m->addImage(net_input_buf, yuv_buf_len); + s->m->addImage((float*)net_input_buf, yuv_buf_len / 4); for (int i = 0; i < CALIB_LEN; i++) { s->calib[i] = calib[i]; } s->m->execute(); double t2 = millis_since_boot(); - DMonitoringResult ret = {0}; - for (int i = 0; i < 3; ++i) { - ret.face_orientation[i] = s->output[i] * REG_SCALE; - ret.face_orientation_meta[i] = exp(s->output[6 + i]); - } - for (int i = 0; i < 2; ++i) { - ret.face_position[i] = s->output[3 + i] * REG_SCALE; - ret.face_position_meta[i] = exp(s->output[9 + i]); - } - for (int i = 0; i < 4; ++i) { - ret.ready_prob[i] = sigmoid(s->output[39 + i]); - } - for (int i = 0; i < 2; ++i) { - ret.not_ready_prob[i] = sigmoid(s->output[43 + i]); - } - ret.face_prob = sigmoid(s->output[12]); - ret.left_eye_prob = sigmoid(s->output[21]); - ret.right_eye_prob = sigmoid(s->output[30]); - ret.left_blink_prob = sigmoid(s->output[31]); - ret.right_blink_prob = sigmoid(s->output[32]); - ret.sg_prob = sigmoid(s->output[33]); - ret.poor_vision = sigmoid(s->output[34]); - ret.partial_face = sigmoid(s->output[35]); - ret.distracted_pose = sigmoid(s->output[36]); - ret.distracted_eyes = sigmoid(s->output[37]); - ret.occluded_prob = sigmoid(s->output[38]); - ret.dsp_execution_time = (t2 - t1) / 1000.; - return ret; + DMonitoringModelResult model_res = {0}; + parse_driver_data(model_res.driver_state_lhd, s, 0); + parse_driver_data(model_res.driver_state_rhd, s, 41); + model_res.poor_vision_prob = sigmoid(s->output[82]); + model_res.wheel_on_right_prob = sigmoid(s->output[83]); + model_res.dsp_execution_time = (t2 - t1) / 1000.; + + return model_res; } -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred) { +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred) { // make msg MessageBuilder msg; - auto framed = msg.initEvent().initDriverState(); + auto framed = msg.initEvent().initDriverStateV2(); framed.setFrameId(frame_id); framed.setModelExecutionTime(execution_time); - framed.setDspExecutionTime(res.dsp_execution_time); - - framed.setFaceOrientation(res.face_orientation); - framed.setFaceOrientationStd(res.face_orientation_meta); - framed.setFacePosition(res.face_position); - framed.setFacePositionStd(res.face_position_meta); - framed.setFaceProb(res.face_prob); - framed.setLeftEyeProb(res.left_eye_prob); - framed.setRightEyeProb(res.right_eye_prob); - framed.setLeftBlinkProb(res.left_blink_prob); - framed.setRightBlinkProb(res.right_blink_prob); - framed.setSunglassesProb(res.sg_prob); - framed.setPoorVision(res.poor_vision); - framed.setPartialFace(res.partial_face); - framed.setDistractedPose(res.distracted_pose); - framed.setDistractedEyes(res.distracted_eyes); - framed.setOccludedProb(res.occluded_prob); - framed.setReadyProb(res.ready_prob); - framed.setNotReadyProb(res.not_ready_prob); + framed.setDspExecutionTime(model_res.dsp_execution_time); + + framed.setPoorVisionProb(model_res.poor_vision_prob); + framed.setWheelOnRightProb(model_res.wheel_on_right_prob); + fill_driver_data(framed.initLeftDriverData(), model_res.driver_state_lhd); + fill_driver_data(framed.initRightDriverData(), model_res.driver_state_rhd); + if (send_raw_pred) { framed.setRawPredictions(raw_pred.asBytes()); } - pm.send("driverState", msg); + pm.send("driverStateV2", msg); } void dmonitoring_free(DMonitoringModelState* s) { diff --git a/selfdrive/modeld/models/dmonitoring.h b/selfdrive/modeld/models/dmonitoring.h index a1be91e3bb20a8..874722cd93fb88 100644 --- a/selfdrive/modeld/models/dmonitoring.h +++ b/selfdrive/modeld/models/dmonitoring.h @@ -9,44 +9,43 @@ #define CALIB_LEN 3 -#define OUTPUT_SIZE 45 +#define OUTPUT_SIZE 84 #define REG_SCALE 0.25f -typedef struct DMonitoringResult { +typedef struct DriverStateResult { float face_orientation[3]; - float face_orientation_meta[3]; + float face_orientation_std[3]; float face_position[2]; - float face_position_meta[2]; + float face_position_std[2]; float face_prob; float left_eye_prob; float right_eye_prob; float left_blink_prob; float right_blink_prob; - float sg_prob; - float poor_vision; - float partial_face; - float distracted_pose; - float distracted_eyes; + float sunglasses_prob; float occluded_prob; float ready_prob[4]; float not_ready_prob[2]; +} DriverStateResult; + +typedef struct DMonitoringModelResult { + DriverStateResult driver_state_lhd; + DriverStateResult driver_state_rhd; + float poor_vision_prob; + float wheel_on_right_prob; float dsp_execution_time; -} DMonitoringResult; +} DMonitoringModelResult; typedef struct DMonitoringModelState { RunModel *m; bool is_rhd; float output[OUTPUT_SIZE]; - std::vector resized_buf; - std::vector cropped_buf; - std::vector premirror_cropped_buf; - std::vector net_input_buf; + std::vector net_input_buf; float calib[CALIB_LEN]; - float tensor[UINT8_MAX + 1]; } DMonitoringModelState; void dmonitoring_init(DMonitoringModelState* s); -DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred); +DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred); void dmonitoring_free(DMonitoringModelState* s); diff --git a/selfdrive/modeld/models/dmonitoring_model.current b/selfdrive/modeld/models/dmonitoring_model.current index 74bcfe17a4ebe4..ba2865429e6452 100644 --- a/selfdrive/modeld/models/dmonitoring_model.current +++ b/selfdrive/modeld/models/dmonitoring_model.current @@ -1,2 +1,2 @@ -a8236e30-5bee-4689-8ea0-fc102e2770e5 -d508c79bae1c1c451f3af3e2bc231ce33678cb43 \ No newline at end of file +5b02cff5-2b29-431d-b186-372e9c6fd0c7 +bf33cc569076984626ac7e027f927aa593261fa7 \ No newline at end of file diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 51b0d1ed760980..1fca1317d5ca44 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00731ebd06fcff7e5837607b91bc56cad3bed5d7ee89052c911c981e8f665308 -size 3679940 +oid sha256:aca1dd411b5f488bea605dc360656e631fc4301968a589ea072e2220c8092600 +size 9157561 diff --git a/selfdrive/modeld/models/dmonitoring_model_q.dlc b/selfdrive/modeld/models/dmonitoring_model_q.dlc index 2e54f7ee4b098e..c1cba151764929 100644 --- a/selfdrive/modeld/models/dmonitoring_model_q.dlc +++ b/selfdrive/modeld/models/dmonitoring_model_q.dlc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:667df5e925570a0f6a33dfb890e186a1f13f101885b46db47ec45305737dffb6 -size 1145921 +oid sha256:d146b1d1fd9d40d57d058e51d285f83676866e26d9e5aff9fa27623ce343b58a +size 2636941 diff --git a/selfdrive/modeld/runners/onnx_runner.py b/selfdrive/modeld/runners/onnx_runner.py index e282a66b66a8d0..ac7cc68814df22 100755 --- a/selfdrive/modeld/runners/onnx_runner.py +++ b/selfdrive/modeld/runners/onnx_runner.py @@ -9,20 +9,24 @@ import onnxruntime as ort # pylint: disable=import-error -def read(sz): +def read(sz, tf8=False): dd = [] gt = 0 - while gt < sz * 4: - st = os.read(0, sz * 4 - gt) + szof = 1 if tf8 else 4 + while gt < sz * szof: + st = os.read(0, sz * szof - gt) assert(len(st) > 0) dd.append(st) gt += len(st) - return np.frombuffer(b''.join(dd), dtype=np.float32) + r = np.frombuffer(b''.join(dd), dtype=np.uint8 if tf8 else np.float32).astype(np.float32) + if tf8: + r = r / 255. + return r def write(d): os.write(1, d.tobytes()) -def run_loop(m): +def run_loop(m, tf8_input=False): ishapes = [[1]+ii.shape[1:] for ii in m.get_inputs()] keys = [x.name for x in m.get_inputs()] @@ -33,10 +37,10 @@ def run_loop(m): print("ready to run onnx model", keys, ishapes, file=sys.stderr) while 1: inputs = [] - for shp in ishapes: + for k, shp in zip(keys, ishapes): ts = np.product(shp) #print("reshaping %s with offset %d" % (str(shp), offset), file=sys.stderr) - inputs.append(read(ts).reshape(shp)) + inputs.append(read(ts, (k=='input_img' and tf8_input)).reshape(shp)) ret = m.run(None, dict(zip(keys, inputs))) #print(ret, file=sys.stderr) for r in ret: @@ -44,6 +48,7 @@ def run_loop(m): if __name__ == "__main__": + print(sys.argv, file=sys.stderr) print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) options = ort.SessionOptions() options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL @@ -63,6 +68,6 @@ def run_loop(m): print("Onnx selected provider: ", [provider], file=sys.stderr) ort_session = ort.InferenceSession(sys.argv[1], options, providers=[provider]) print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - run_loop(ort_session) + run_loop(ort_session, tf8_input=("--use_tf8" in sys.argv)) except KeyboardInterrupt: pass diff --git a/selfdrive/modeld/runners/onnxmodel.cc b/selfdrive/modeld/runners/onnxmodel.cc index 9b4d6fd0152b95..1f9f551abc5351 100644 --- a/selfdrive/modeld/runners/onnxmodel.cc +++ b/selfdrive/modeld/runners/onnxmodel.cc @@ -14,12 +14,13 @@ #include "common/swaglog.h" #include "common/util.h" -ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra) { +ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra, bool _use_tf8) { LOGD("loading model %s", path); output = _output; output_size = _output_size; use_extra = _use_extra; + use_tf8 = _use_tf8; int err = pipe(pipein); assert(err == 0); @@ -28,11 +29,12 @@ ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int std::string exe_dir = util::dir_name(util::readlink("/proc/self/exe")); std::string onnx_runner = exe_dir + "/runners/onnx_runner.py"; + std::string tf8_arg = use_tf8 ? "--use_tf8" : ""; proc_pid = fork(); if (proc_pid == 0) { LOGD("spawning onnx process %s", onnx_runner.c_str()); - char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, nullptr}; + char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, (char*)tf8_arg.c_str(), nullptr}; dup2(pipein[0], 0); dup2(pipeout[1], 1); close(pipein[0]); diff --git a/selfdrive/modeld/runners/onnxmodel.h b/selfdrive/modeld/runners/onnxmodel.h index 567d81d29ef8f7..4ac599e2afbbf0 100644 --- a/selfdrive/modeld/runners/onnxmodel.h +++ b/selfdrive/modeld/runners/onnxmodel.h @@ -6,7 +6,7 @@ class ONNXModel : public RunModel { public: - ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false); + ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false, bool _use_tf8 = false); ~ONNXModel(); void addRecurrent(float *state, int state_size); void addDesire(float *state, int state_size); @@ -31,6 +31,7 @@ class ONNXModel : public RunModel { int calib_size; float *image_input_buf = NULL; int image_buf_size; + bool use_tf8; float *extra_input_buf = NULL; int extra_buf_size; bool use_extra; diff --git a/selfdrive/modeld/runners/snpemodel.cc b/selfdrive/modeld/runners/snpemodel.cc index 1861494d591e3b..4d6917e894c539 100644 --- a/selfdrive/modeld/runners/snpemodel.cc +++ b/selfdrive/modeld/runners/snpemodel.cc @@ -14,10 +14,11 @@ void PrintErrorStringAndExit() { std::exit(EXIT_FAILURE); } -SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra) { +SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra, bool luse_tf8) { output = loutput; output_size = loutput_size; use_extra = luse_extra; + use_tf8 = luse_tf8; #ifdef QCOM2 if (runtime==USE_GPU_RUNTIME) { Runtime = zdl::DlSystem::Runtime_t::GPU; @@ -70,14 +71,16 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int printf("model: %s -> %s\n", input_tensor_name, output_tensor_name); zdl::DlSystem::UserBufferEncodingFloat userBufferEncodingFloat; + zdl::DlSystem::UserBufferEncodingTf8 userBufferEncodingTf8(0, 1./255); // network takes 0-1 zdl::DlSystem::IUserBufferFactory& ubFactory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + size_t size_of_input = use_tf8 ? sizeof(uint8_t) : sizeof(float); // create input buffer { const auto &inputDims_opt = snpe->getInputDimensions(input_tensor_name); const zdl::DlSystem::TensorShape& bufferShape = *inputDims_opt; std::vector strides(bufferShape.rank()); - strides[strides.size() - 1] = sizeof(float); + strides[strides.size() - 1] = size_of_input; size_t product = 1; for (size_t i = 0; i < bufferShape.rank(); i++) product *= bufferShape[i]; size_t stride = strides[strides.size() - 1]; @@ -86,7 +89,10 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int strides[i-1] = stride; } printf("input product is %lu\n", product); - inputBuffer = ubFactory.createUserBuffer(NULL, product*sizeof(float), strides, &userBufferEncodingFloat); + inputBuffer = ubFactory.createUserBuffer(NULL, + product*size_of_input, + strides, + use_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingTf8 : (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingFloat); inputMap.add(input_tensor_name, inputBuffer.get()); } diff --git a/selfdrive/modeld/runners/snpemodel.h b/selfdrive/modeld/runners/snpemodel.h index ba51fdced079f6..ed9d58d1e11335 100644 --- a/selfdrive/modeld/runners/snpemodel.h +++ b/selfdrive/modeld/runners/snpemodel.h @@ -23,7 +23,7 @@ class SNPEModel : public RunModel { public: - SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false); + SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false, bool use_tf8 = false); void addRecurrent(float *state, int state_size); void addTrafficConvention(float *state, int state_size); void addCalib(float *state, int state_size); @@ -52,6 +52,7 @@ class SNPEModel : public RunModel { std::unique_ptr inputBuffer; float *input; size_t input_size; + bool use_tf8; // snpe output stuff zdl::DlSystem::UserBufferMap outputMap; diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 9a3196030ba643..c31e9da43b9f20 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -18,7 +18,7 @@ def dmonitoringd_thread(sm=None, pm=None): pm = messaging.PubMaster(['driverMonitoringState']) if sm is None: - sm = messaging.SubMaster(['driverState', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverState']) + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverStateV2']) driver_status = DriverStatus(rhd=Params().get_bool("IsRHD")) @@ -34,7 +34,7 @@ def dmonitoringd_thread(sm=None, pm=None): while True: sm.update() - if not sm.updated['driverState']: + if not sm.updated['driverStateV2']: continue # Get interaction @@ -51,7 +51,7 @@ def dmonitoringd_thread(sm=None, pm=None): # Get data from dmonitoringmodeld events = Events() - driver_status.update_states(sm['driverState'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) + driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) # Block engaging after max number of distrations if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \ @@ -79,6 +79,7 @@ def dmonitoringd_thread(sm=None, pm=None): "isLowStd": driver_status.pose.low_std, "hiStdCount": driver_status.hi_stds, "isActiveMode": driver_status.active_monitoring_mode, + "isRHD": driver_status.wheel_on_right, } pm.send('driverMonitoringState', dat) diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index 662e0d76ce7167..4affa1e79660a1 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -5,6 +5,7 @@ from common.realtime import DT_DMON from common.filter_simple import FirstOrderFilter from common.stat_live import RunningStatFilter +from common.transformations.camera import tici_d_frame_size EventName = car.CarEvent.EventName @@ -26,32 +27,29 @@ def __init__(self): self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6. self._FACE_THRESHOLD = 0.5 - self._PARTIAL_FACE_THRESHOLD = 0.8 self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.925 - self._BLINK_THRESHOLD = 0.8 - self._BLINK_THRESHOLD_SLACK = 0.9 - self._BLINK_THRESHOLD_STRICT = self._BLINK_THRESHOLD + self._BLINK_THRESHOLD = 0.861 self._EE_THRESH11 = 0.75 self._EE_THRESH12 = 3.25 self._EE_THRESH21 = 0.01 self._EE_THRESH22 = 0.35 - self._POSE_PITCH_THRESHOLD = 0.3237 - self._POSE_PITCH_THRESHOLD_SLACK = 0.3657 + self._POSE_PITCH_THRESHOLD = 0.3133 + self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD - self._POSE_YAW_THRESHOLD = 0.3109 - self._POSE_YAW_THRESHOLD_SLACK = 0.4294 + self._POSE_YAW_THRESHOLD = 0.4020 + self._POSE_YAW_THRESHOLD_SLACK = 0.5042 self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD - self._PITCH_NATURAL_OFFSET = 0.057 # initial value before offset is learned - self._YAW_NATURAL_OFFSET = 0.11 # initial value before offset is learned + self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned + self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 self._YAW_MIN_OFFSET = -0.0246 - self._POSESTD_THRESHOLD = 0.315 + self._POSESTD_THRESHOLD = 0.3 self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz @@ -59,6 +57,9 @@ def __init__(self): self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative self._POSE_OFFSET_MAX_COUNT = int(360 / self._DT_DMON) # stop deweighting new data after 6 min, aka "short term memory" + self._WHEELPOS_THRESHOLD = 0.5 + self._WHEELPOS_FILTER_MIN_COUNT = int(5 / self._DT_DMON) + self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change @@ -66,9 +67,9 @@ def __init__(self): self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts -# model output refers to center of cropped image, so need to apply the x displacement offset -RESIZED_FOCAL = 320.0 -H, W, FULL_W = 320, 160, 426 +# model output refers to center of undistorted+leveled image +EFL = 598.0 # focal length in K +W, H = tici_d_frame_size # corrected image has same size as raw class DistractedType: NOT_DISTRACTED = 0 @@ -76,22 +77,22 @@ class DistractedType: DISTRACTED_BLINK = 2 DISTRACTED_E2E = 4 -def face_orientation_from_net(angles_desc, pos_desc, rpy_calib, is_rhd): +def face_orientation_from_net(angles_desc, pos_desc, rpy_calib): # the output of these angles are in device frame # so from driver's perspective, pitch is up and yaw is right pitch_net, yaw_net, roll_net = angles_desc - face_pixel_position = ((pos_desc[0] + .5)*W - W + FULL_W, (pos_desc[1]+.5)*H) - yaw_focal_angle = atan2(face_pixel_position[0] - FULL_W//2, RESIZED_FOCAL) - pitch_focal_angle = atan2(face_pixel_position[1] - H//2, RESIZED_FOCAL) + face_pixel_position = ((pos_desc[0]+0.5)*W, (pos_desc[1]+0.5)*H) + yaw_focal_angle = atan2(face_pixel_position[0] - W//2, EFL) + pitch_focal_angle = atan2(face_pixel_position[1] - H//2, EFL) pitch = pitch_net + pitch_focal_angle yaw = -yaw_net + yaw_focal_angle # no calib for roll pitch -= rpy_calib[1] - yaw -= rpy_calib[2] * (1 - 2 * int(is_rhd)) # lhd -> -=, rhd -> += + yaw -= rpy_calib[2] return roll_net, pitch, yaw class DriverPose(): @@ -112,7 +113,6 @@ class DriverBlink(): def __init__(self): self.left_blink = 0. self.right_blink = 0. - self.cfactor = 1. class DriverStatus(): def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): @@ -120,7 +120,7 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.settings = settings # init driver status - self.is_rhd_region = rhd + # self.wheelpos_learner = RunningStatFilter() self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) self.pose_calibrated = False self.blink = DriverBlink() @@ -137,8 +137,8 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.distracted_types = [] self.driver_distracted = False self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON) + self.wheel_on_right = rhd self.face_detected = False - self.face_partial = False self.terminal_alert_cnt = 0 self.terminal_time = 0 self.step_change = 0. @@ -197,7 +197,7 @@ def _get_distracted_types(self): yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw: distracted_types.append(DistractedType.DISTRACTED_POSE) - if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD*self.blink.cfactor: + if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD: distracted_types.append(DistractedType.DISTRACTED_BLINK) if self.ee1_calibrated: @@ -214,13 +214,7 @@ def _get_distracted_types(self): return distracted_types def set_policy(self, model_data, car_speed): - ep = min(model_data.meta.engagedProb, 0.8) / 0.8 # engaged prob bp = model_data.meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s - # TODO: retune adaptive blink - self.blink.cfactor = interp(ep, [0, 0.5, 1], - [self.settings._BLINK_THRESHOLD_STRICT, - self.settings._BLINK_THRESHOLD, - self.settings._BLINK_THRESHOLD_SLACK]) / self.settings._BLINK_THRESHOLD k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2) bp_normal = max(min(bp / k1, 0.5),0) self.pose.cfactor_pitch = interp(bp_normal, [0, 0.5], @@ -231,28 +225,34 @@ def set_policy(self, model_data, car_speed): self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD def update_states(self, driver_state, cal_rpy, car_speed, op_engaged): - if not all(len(x) > 0 for x in (driver_state.faceOrientation, driver_state.facePosition, - driver_state.faceOrientationStd, driver_state.facePositionStd, - driver_state.readyProb, driver_state.notReadyProb)): + # rhd_pred = driver_state.wheelOnRightProb + # if car_speed > 0.01: + # self.wheelpos_learner.push_and_update(rhd_pred) + # if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT: + # self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD + # else: + # self.wheel_on_right = rhd_pred > self.settings._WHEELPOS_THRESHOLD + driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData + if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition, + driver_data.faceOrientationStd, driver_data.facePositionStd, + driver_data.readyProb, driver_data.notReadyProb)): return - self.face_partial = driver_state.partialFace > self.settings._PARTIAL_FACE_THRESHOLD - self.face_detected = driver_state.faceProb > self.settings._FACE_THRESHOLD or self.face_partial - self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_state.faceOrientation, driver_state.facePosition, cal_rpy, self.is_rhd_region) - self.pose.pitch_std = driver_state.faceOrientationStd[0] - self.pose.yaw_std = driver_state.faceOrientationStd[1] - # self.pose.roll_std = driver_state.faceOrientationStd[2] + self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD + self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy) + self.pose.pitch_std = driver_data.faceOrientationStd[0] + self.pose.yaw_std = driver_data.faceOrientationStd[1] model_std_max = max(self.pose.pitch_std, self.pose.yaw_std) - self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD and not self.face_partial - self.blink.left_blink = driver_state.leftBlinkProb * (driver_state.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) - self.blink.right_blink = driver_state.rightBlinkProb * (driver_state.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) - self.eev1 = driver_state.notReadyProb[1] - self.eev2 = driver_state.readyProb[0] + self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD + self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) + self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) + self.eev1 = driver_data.notReadyProb[1] + self.eev2 = driver_data.readyProb[0] self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_POSE in self.distracted_types or DistractedType.DISTRACTED_BLINK in self.distracted_types) and \ - driver_state.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std + driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std self.driver_distraction_filter.update(self.driver_distracted) # update offseter diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index a84ed242ba0a68..43b5e7747e66ba 100755 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -17,19 +17,19 @@ INVISIBLE_SECONDS_TO_RED = dm_settings._AWARENESS_TIME + 1 def make_msg(face_detected, distracted=False, model_uncertain=False): - ds = log.DriverState.new_message() - ds.faceOrientation = [0., 0., 0.] - ds.facePosition = [0., 0.] - ds.faceProb = 1. * face_detected - ds.leftEyeProb = 1. - ds.rightEyeProb = 1. - ds.leftBlinkProb = 1. * distracted - ds.rightBlinkProb = 1. * distracted - ds.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] - ds.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] + ds = log.DriverStateV2.new_message() + ds.leftDriverData.faceOrientation = [0., 0., 0.] + ds.leftDriverData.facePosition = [0., 0.] + ds.leftDriverData.faceProb = 1. * face_detected + ds.leftDriverData.leftEyeProb = 1. + ds.leftDriverData.rightEyeProb = 1. + ds.leftDriverData.leftBlinkProb = 1. * distracted + ds.leftDriverData.rightBlinkProb = 1. * distracted + ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] + ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] # TODO: test both separately when e2e is used - ds.readyProb = [0., 0., 0., 0.] - ds.notReadyProb = [0., 0.] + ds.leftDriverData.readyProb = [0., 0., 0., 0.] + ds.leftDriverData.notReadyProb = [0., 0.] return ds diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index b83629c76a8347..032b50487978e8 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -52,7 +52,7 @@ def model_replay(lr, frs): vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, False, *(tici_f_frame_size)) vipc_server.start_listener() - sm = messaging.SubMaster(['modelV2', 'driverState']) + sm = messaging.SubMaster(['modelV2', 'driverStateV2']) pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'driverCameraState', 'liveCalibration', 'lateralPlan']) try: @@ -112,7 +112,7 @@ def model_replay(lr, frs): if min(frame_idxs['roadCameraState'], frame_idxs['wideRoadCameraState']) > recv_cnt['modelV2']: recv = "modelV2" elif msg.which() == 'driverCameraState': - recv = "driverState" + recv = "driverStateV2" # wait for a response with Timeout(15, f"timed out waiting for {recv}"): @@ -170,8 +170,8 @@ def model_replay(lr, frs): 'logMonoTime', 'modelV2.frameDropPerc', 'modelV2.modelExecutionTime', - 'driverState.modelExecutionTime', - 'driverState.dspExecutionTime' + 'driverStateV2.modelExecutionTime', + 'driverStateV2.dspExecutionTime' ] # TODO this tolerence is absurdly large tolerance = 5e-1 if PC else None diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 90520f2619e4d4..626d54a8f0e860 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -f74ab97371be93fdc28333e5ea12bbb78c3a32d0 +1d0979ca59dbc89bc5890656e9501e83f0556d50 diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 1eda9bc7f59c89..89885ef3974aac 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -293,7 +293,7 @@ def ublox_rcv_callback(msg): ProcessConfig( proc_name="dmonitoringd", pub_sub={ - "driverState": ["driverMonitoringState"], + "driverStateV2": ["driverMonitoringState"], "liveCalibration": [], "carState": [], "modelV2": [], "controlsState": [], }, ignore=["logMonoTime", "valid"], diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 6cf53a046e340b..3beeaff3b23708 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -33,7 +33,7 @@ "selfdrive.controls.radard": 4.5, "./_modeld": 4.48, "./boardd": 3.63, - "./_dmonitoringmodeld": 10.0, + "./_dmonitoringmodeld": 5.0, "selfdrive.thermald.thermald": 3.87, "selfdrive.locationd.calibrationd": 2.0, "./_soundd": 1.0, @@ -60,7 +60,7 @@ "roadCameraState": [2.5, 0.35], "driverCameraState": [2.5, 0.35], "modelV2": [2.5, 0.35], - "driverState": [2.5, 0.40], + "driverStateV2": [2.5, 0.40], "liveLocationKalman": [2.5, 0.35], "wideRoadCameraState": [1.5, 0.35], } @@ -221,7 +221,7 @@ def test_model_execution_timings(self): # TODO: this went up when plannerd cpu usage increased, why? cfgs = [ ("modelV2", 0.050, 0.036), - ("driverState", 0.050, 0.026), + ("driverStateV2", 0.050, 0.026), ] for (s, instant_max, avg_max) in cfgs: ts = [getattr(getattr(m, s), "modelExecutionTime") for m in self.lr if m.which() == s] diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc index 06578b455a7ca3..868a97f6041145 100644 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -25,7 +25,7 @@ void DriverViewWindow::mouseReleaseEvent(QMouseEvent* e) { emit done(); } -DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverState"}), QWidget(parent) { +DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverStateV2"}), QWidget(parent) { face_img = loadPixmap("../assets/img_driver_face.png", {FACE_IMG_SIZE, FACE_IMG_SIZE}); } @@ -57,43 +57,36 @@ void DriverViewScene::paintEvent(QPaintEvent* event) { return; } - const int width = 4 * height() / 3; - const QRect rect2 = {rect().center().x() - width / 2, rect().top(), width, rect().height()}; - const QRect valid_rect = {is_rhd ? rect2.right() - rect2.height() / 2 : rect2.left(), rect2.top(), rect2.height() / 2, rect2.height()}; + cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); + cereal::DriverStateV2::DriverData::Reader driver_data; - // blackout - const QColor bg(0, 0, 0, 140); - const QRect& blackout_rect = rect(); - p.fillRect(blackout_rect.adjusted(0, 0, valid_rect.left() - blackout_rect.right(), 0), bg); - p.fillRect(blackout_rect.adjusted(valid_rect.right() - blackout_rect.left(), 0, 0, 0), bg); - p.fillRect(blackout_rect.adjusted(valid_rect.left()-blackout_rect.left()+1, 0, valid_rect.right()-blackout_rect.right()-1, -valid_rect.height()*7/10), bg); // top dz + // is_rhd = driver_state.getWheelOnRightProb() > 0.5; + driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); - // face bounding box - cereal::DriverState::Reader driver_state = sm["driverState"].getDriverState(); - bool face_detected = driver_state.getFaceProb() > 0.5; + bool face_detected = driver_data.getFaceProb() > 0.5; if (face_detected) { - auto fxy_list = driver_state.getFacePosition(); - auto std_list = driver_state.getFaceOrientationStd(); + auto fxy_list = driver_data.getFacePosition(); + auto std_list = driver_data.getFaceOrientationStd(); float face_x = fxy_list[0]; float face_y = fxy_list[1]; float face_std = std::max(std_list[0], std_list[1]); float alpha = 0.7; - if (face_std > 0.08) { - alpha = std::max(0.7 - (face_std-0.08)*7, 0.0); + if (face_std > 0.15) { + alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); } - const int box_size = 0.6 * rect2.height() / 2; - const float rhd_offset = 0.05; // lhd is shifted, so rhd is not mirrored - int fbox_x = valid_rect.center().x() + (is_rhd ? (face_x + rhd_offset) : -face_x) * valid_rect.width(); - int fbox_y = valid_rect.center().y() + face_y * valid_rect.height(); + const int box_size = 220; + // use approx instead of distort_points + int fbox_x = 1080.0 - 1714.0 * face_x; + int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); } // icon - const int img_offset = 30; - const int img_x = is_rhd ? rect2.right() - FACE_IMG_SIZE - img_offset : rect2.left() + img_offset; - const int img_y = rect2.bottom() - FACE_IMG_SIZE - img_offset; - p.setOpacity(face_detected ? 1.0 : 0.3); + const int img_offset = 60; + const int img_x = rect().left() + img_offset; + const int img_y = rect().bottom() - FACE_IMG_SIZE - img_offset; + p.setOpacity(face_detected ? 1.0 : 0.2); p.drawPixmap(img_x, img_y, face_img); } diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index f16e8e4e0def08..0a3b9762e56826 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -54,16 +54,15 @@ const mat4 device_transform = {{ }}; mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) { - const float driver_view_ratio = 1.333; - const float yscale = stream_height * driver_view_ratio / tici_dm_crop::width; + const float driver_view_ratio = 2.0; + const float yscale = stream_height * driver_view_ratio / stream_width; const float xscale = yscale*screen_height/screen_width*stream_width/stream_height; mat4 transform = (mat4){{ - xscale, 0.0, 0.0, xscale*tici_dm_crop::x_offset/stream_width*2, - 0.0, yscale, 0.0, yscale*tici_dm_crop::y_offset/stream_height*2, + xscale, 0.0, 0.0, 0.0, + 0.0, yscale, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, }}; - return transform; } From 19af336f89807a3a621132c5054f68de2b49d43c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 8 Jun 2022 21:07:49 -0700 Subject: [PATCH 020/436] bump opendbc (#24790) --- opendbc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc b/opendbc index 7701277d266611..30aacafa10e72d 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 7701277d2666119bc7fcaca9f8cfefd50cd5b071 +Subproject commit 30aacafa10e72db33ecf5e3b7a6eacffa9390b8b From 384e08e2ee299e8e0bc558024361ab92bedb1bbd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 8 Jun 2022 22:38:07 -0700 Subject: [PATCH 021/436] Honda Bosch: fix openpilot longitudinal controls mismatch (#24788) * fix bosch controls mismatch * bump * bump panda --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 475a9a31241090..42772b49e31368 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 475a9a312410908abcaa32f2e48140fcbfc2362f +Subproject commit 42772b49e313688f8f96114806a72784f978e990 From b0f6a8bdf78e3be3bd698ccda3477c6adb2cd270 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Thu, 9 Jun 2022 12:08:23 -0500 Subject: [PATCH 022/436] VW MQB: Add FW for 2020 Volkswagen Tiguan (#24795) --- selfdrive/car/volkswagen/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 8cba9988bdd32f..9b6f6a16c31723 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -564,6 +564,7 @@ class VWCarInfo(CarInfo): (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820529A6060603', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A60604A1', + b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A6000600', b'\xf1\x875QF909144B \xf1\x895582\xf1\x82\00571A60634A1', b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A60604A1', b'\xf1\x875QM909144C \xf1\x891082\xf1\x82\x0521A60604A1', From 5bb1554ec49f4c3e0e29d614c6e8de2b5f71e163 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 9 Jun 2022 10:55:05 -0700 Subject: [PATCH 023/436] build checks do not rely on each other (#24783) --- .github/workflows/prebuilt.yaml | 3 ++- .github/workflows/release.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index a6808a0276dcfa..7acc8a2254f1e7 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -25,11 +25,12 @@ jobs: IMAGE_NAME: openpilot-prebuilt steps: - name: Wait for green check mark - uses: lewagon/wait-on-check-action@v0.2 + uses: commaai/wait-on-check-action@f16fc3bb6cd4886520b4e9328db1d42104d5cadc with: ref: master wait-interval: 30 running-workflow-name: 'build prebuilt' + check-regexp: ^((?!.*(build master-ci).*).)*$ - uses: actions/checkout@v3 with: submodules: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 14555f1e7d7169..fb5a37eeef23dc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,11 +12,12 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - name: Wait for green check mark - uses: lewagon/wait-on-check-action@v0.2 + uses: commaai/wait-on-check-action@f16fc3bb6cd4886520b4e9328db1d42104d5cadc with: ref: master wait-interval: 30 running-workflow-name: 'build master-ci' + check-regexp: ^((?!.*(build prebuilt).*).)*$ - uses: actions/checkout@v3 with: submodules: true From 2d0a72a0e837457e5e93dda143e5909889cb03f5 Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Thu, 9 Jun 2022 13:07:17 -0500 Subject: [PATCH 024/436] Add missing CHR engine f/w (#24797) `@papifrio#5178` 2019 Toyota C-HR ICE ("nodsu" TSS-P) DongleID/route ade67da77ee74b16|2022-06-09--00-36-15 --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index a9517d26bf9633..70a94be167c8e9 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -558,6 +558,7 @@ class ToyotaCarInfo(CarInfo): b'\x033F401100\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203102\x00\x00\x00\x00', b'\x033F401200\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00', b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00', + b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203302\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'8821F0W01000 ', From 074f614f880fa85a56596cd2917c07d8f39961f1 Mon Sep 17 00:00:00 2001 From: AlexandreSato <66435071+AlexandreSato@users.noreply.github.com> Date: Thu, 9 Jun 2022 16:01:31 -0300 Subject: [PATCH 025/436] Lexus: NX Hybrid 2020 support (#24796) * LEXUS: Missing esp and engine FW versions in values.py DongleId: 09ae96064ed85a14 | testRoute: 09ae96064ed85a14|2022-01-10--01-57-37 | * Make a new platform for NX Hybrid TSS2 * Update releases Co-authored-by: Shane Smiskol --- RELEASES.md | 1 + docs/CARS.md | 1 + selfdrive/car/tests/routes.py | 1 + selfdrive/car/toyota/interface.py | 2 +- selfdrive/car/toyota/values.py | 24 ++++++++++++++++++++++-- 5 files changed, 26 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index de57d520d3f2fe..87e5adb584e8cf 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,7 @@ Version 0.8.15 (2022-XX-XX) * Effective feedforward that uses road roll * Simplified tuning, all car-specific parameters can be derived from data * AGNOS 5 +* Lexus NX Hybrid 2020 support thanks to AlexandreSato! Version 0.8.14 (2022-06-01) ======================== diff --git a/docs/CARS.md b/docs/CARS.md index 2aa624d8495cf6..9a0788d8758e24 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -51,6 +51,7 @@ How We Rate The Cars |Lexus|ES 2019-21|All|||||| |Lexus|ES Hybrid 2019-22|All|||||| |Lexus|NX 2020|All|||||| +|Lexus|NX Hybrid 2020|All|||||| |Lexus|UX Hybrid 2019-21|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index d2ec6b68d72fa2..e7942f517c1896 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -144,6 +144,7 @@ TestRoute("ec429c0f37564e3c|2020-02-01--17-28-12", TOYOTA.LEXUS_NXH), TestRoute("964c09eb11ca8089|2020-11-03--22-04-00", TOYOTA.LEXUS_NX), TestRoute("3fd5305f8b6ca765|2021-04-28--19-26-49", TOYOTA.LEXUS_NX_TSS2), + TestRoute("09ae96064ed85a14|2022-06-09--12-22-31", TOYOTA.LEXUS_NXH_TSS2), TestRoute("0a302ffddbb3e3d3|2020-02-08--16-19-08", TOYOTA.HIGHLANDER_TSS2), TestRoute("437e4d2402abf524|2021-05-25--07-58-50", TOYOTA.HIGHLANDERH_TSS2), TestRoute("3183cd9b021e89ce|2021-05-25--10-34-44", TOYOTA.HIGHLANDER), diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index a1ce8c4980cd73..9440c73f5d8bf1 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -186,7 +186,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 3108 * CV.LB_TO_KG + STD_CARGO_KG # mean between min and max set_lat_tune(ret.lateralTuning, LatTunes.PID_M) - elif candidate in (CAR.LEXUS_NXH, CAR.LEXUS_NX, CAR.LEXUS_NX_TSS2): + elif candidate in (CAR.LEXUS_NX, CAR.LEXUS_NXH, CAR.LEXUS_NX_TSS2, CAR.LEXUS_NXH_TSS2): stop_and_go = True ret.wheelbase = 2.66 ret.steerRatio = 14.7 diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 70a94be167c8e9..412a9c93905f2e 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -77,6 +77,7 @@ class CAR: LEXUS_NX = "LEXUS NX 2018" LEXUS_NXH = "LEXUS NX HYBRID 2018" LEXUS_NX_TSS2 = "LEXUS NX 2020" + LEXUS_NXH_TSS2 = "LEXUS NX HYBRID 2020" LEXUS_RC = "LEXUS RC 2020" LEXUS_RX = "LEXUS RX 2016" LEXUS_RXH = "LEXUS RX HYBRID 2017" @@ -158,6 +159,7 @@ class ToyotaCarInfo(CarInfo): CAR.LEXUS_NX: ToyotaCarInfo("Lexus NX 2018-19", footnotes=[Footnote.DSU]), CAR.LEXUS_NXH: ToyotaCarInfo("Lexus NX Hybrid 2018-19", footnotes=[Footnote.DSU]), CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020"), + CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020"), CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2020"), CAR.LEXUS_RX: ToyotaCarInfo("Lexus RX 2016-18", footnotes=[Footnote.DSU]), CAR.LEXUS_RXH: ToyotaCarInfo("Lexus RX Hybrid 2016-19", footnotes=[Footnote.DSU]), @@ -1584,6 +1586,23 @@ class ToyotaCarInfo(CarInfo): b'\x028646F7803100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', ], }, + CAR.LEXUS_NXH_TSS2: { + (Ecu.engine, 0x700, None): [ + b'\x0237887000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.esp, 0x7b0, None): [ + b'F152678210\x00\x00\x00\x00\x00\x00', + ], + (Ecu.eps, 0x7a1, None): [ + b'8965B78120\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x750, 0xf): [ + b'\x018821F3301400\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x750, 0x6d): [ + b'\x028646F78030A0\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', + ], + }, CAR.LEXUS_NXH: { (Ecu.engine, 0x7e0, None): [ b'\x0237841000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -1914,6 +1933,7 @@ class ToyotaCarInfo(CarInfo): CAR.LEXUS_NXH: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_NX: dbc_dict('toyota_tnga_k_pt_generated', 'toyota_adas'), CAR.LEXUS_NX_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), + CAR.LEXUS_NXH_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.PRIUS_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.MIRAI: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.ALPHARD_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), @@ -1926,7 +1946,7 @@ class ToyotaCarInfo(CarInfo): # Toyota/Lexus Safety Sense 2.0 and 2.5 TSS2_CAR = {CAR.RAV4_TSS2, CAR.RAV4_TSS2_2022, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.RAV4H_TSS2_2022, CAR.LEXUS_RX_TSS2, CAR.LEXUS_RXH_TSS2, CAR.HIGHLANDER_TSS2, CAR.HIGHLANDERH_TSS2, CAR.PRIUS_TSS2, CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2, - CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2, CAR.AVALONH_TSS2, CAR.ALPHARDH_TSS2} + CAR.MIRAI, CAR.LEXUS_NX_TSS2, CAR.LEXUS_NXH_TSS2, CAR.ALPHARD_TSS2, CAR.AVALON_TSS2, CAR.AVALONH_TSS2, CAR.ALPHARDH_TSS2} NO_DSU_CAR = TSS2_CAR | {CAR.CHR, CAR.CHRH, CAR.CAMRY, CAR.CAMRYH} @@ -1935,7 +1955,7 @@ class ToyotaCarInfo(CarInfo): EV_HYBRID_CAR = {CAR.AVALONH_2019, CAR.AVALONH_TSS2, CAR.CAMRYH, CAR.CAMRYH_TSS2, CAR.CHRH, CAR.COROLLAH_TSS2, CAR.HIGHLANDERH, CAR.HIGHLANDERH_TSS2, CAR.PRIUS, CAR.PRIUS_V, CAR.RAV4H, CAR.RAV4H_TSS2, CAR.RAV4H_TSS2_2022, CAR.LEXUS_CTH, CAR.MIRAI, CAR.LEXUS_ESH, CAR.LEXUS_ESH_TSS2, CAR.LEXUS_NXH, CAR.LEXUS_RXH, - CAR.LEXUS_RXH_TSS2, CAR.PRIUS_TSS2, CAR.ALPHARDH_TSS2} + CAR.LEXUS_RXH_TSS2, CAR.LEXUS_NXH_TSS2, CAR.PRIUS_TSS2, CAR.ALPHARDH_TSS2} # no resume button press required NO_STOP_TIMER_CAR = TSS2_CAR | {CAR.PRIUS_V, CAR.RAV4H, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.SIENNA, CAR.LEXUS_ESH} From ab8a6352b02b4240f9888af9f3b430c6d6bc4be8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 9 Jun 2022 15:02:41 -0700 Subject: [PATCH 026/436] some release notes --- RELEASES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 87e5adb584e8cf..f6d40e6c769528 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,6 +5,12 @@ Version 0.8.15 (2022-XX-XX) * Much smoother control, consistent across the speed range * Effective feedforward that uses road roll * Simplified tuning, all car-specific parameters can be derived from data + * Significantly improved control on TSS-P Prius +* New driver monitoring model + * takes a larger input frame + * outputs a driver state for both driver and passenger + * automatically determines which side the driver is on (soon) +* Reduced power usage: device runs cooler and fan spins less * AGNOS 5 * Lexus NX Hybrid 2020 support thanks to AlexandreSato! From 8c101c61a82f4b72939bcae7cc7a651ac8fc6fed Mon Sep 17 00:00:00 2001 From: George Hotz <72895+geohot@users.noreply.github.com> Date: Thu, 9 Jun 2022 15:11:32 -0700 Subject: [PATCH 027/436] EyeSight standard on all 2019+ Ascent and Forester (#24799) * on all 2019+ Ascent Forester * update docs Co-authored-by: Adeeb Shihadeh --- docs/CARS.md | 4 ++-- selfdrive/car/subaru/values.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 9a0788d8758e24..20c29e3cb5127e 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -104,7 +104,7 @@ How We Rate The Cars |Mazda|CX-5 2022|All|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| -|Subaru|Forester 2019-21|EyeSight|||||| +|Subaru|Forester 2019-21|All|||||| |Škoda|Kamiq 2021[6](#footnotes)|Driver Assistance|||||| |Škoda|Karoq 2019|Driver Assistance|||||| |Škoda|Kodiaq 2018-19|Driver Assistance|||||| @@ -187,7 +187,7 @@ How We Rate The Cars |Nissan|Leaf 2018-22|ProPILOT|||||| |Nissan|Rogue 2018-20|ProPILOT|||||| |Nissan|X-Trail 2017|ProPILOT|||||| -|Subaru|Ascent 2019-20|EyeSight|||||| +|Subaru|Ascent 2019-20|All|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| |Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 412333edcd0e67..45358eb3a43cdd 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -41,7 +41,7 @@ class SubaruCarInfo(CarInfo): CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { - CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20"), + CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All"), CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), @@ -50,7 +50,7 @@ class SubaruCarInfo(CarInfo): SubaruCarInfo("Subaru Impreza 2020-21"), SubaruCarInfo("Subaru Crosstrek 2020-21"), ], - CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21"), + CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), CAR.LEGACY_PREGLOBAL: SubaruCarInfo("Subaru Legacy 2015-18"), CAR.OUTBACK_PREGLOBAL: SubaruCarInfo("Subaru Outback 2015-17"), From 2ac693100359d9df34d5b2b450a0058f2dd6b7c4 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Thu, 9 Jun 2022 15:23:29 -0700 Subject: [PATCH 028/436] fullframe DM: flip RHD yaw to use matching thresholds --- selfdrive/monitoring/driver_monitor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index 4affa1e79660a1..e7ed175846ad0a 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -240,6 +240,8 @@ def update_states(self, driver_state, cal_rpy, car_speed, op_engaged): self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy) + if self.wheel_on_right: + self.pose.yaw *= -1 self.pose.pitch_std = driver_data.faceOrientationStd[0] self.pose.yaw_std = driver_data.faceOrientationStd[1] model_std_max = max(self.pose.pitch_std, self.pose.yaw_std) From 1fe582686f5446631fa2c87a1c45dcb8f24fdc5e Mon Sep 17 00:00:00 2001 From: Griffin Christenson <36981741+griff12@users.noreply.github.com> Date: Thu, 9 Jun 2022 18:34:05 -0500 Subject: [PATCH 029/436] Toyota: Add missing esp fw version for RAV4_TSS2 (#24793) add fwVersion for RAV4_TSS2 add b'\x01F15260R302\x00\x00\x00\x00\x00\x00' to list of Ecu.esp FwVersions for RAV4_TSS2 --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 412a9c93905f2e..91a7fa5d10dac6 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1254,6 +1254,7 @@ class ToyotaCarInfo(CarInfo): b'\x01F15260R220\x00\x00\x00\x00\x00\x00', b'\x01F15260R290\x00\x00\x00\x00\x00\x00', b'\x01F15260R300\x00\x00\x00\x00\x00\x00', + b'\x01F15260R302\x00\x00\x00\x00\x00\x00', b'\x01F152642551\x00\x00\x00\x00\x00\x00', b'\x01F152642561\x00\x00\x00\x00\x00\x00', b'\x01F152642700\x00\x00\x00\x00\x00\x00', From 86ce2f8d4def65d3adb1136b24b75700e953a62f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 9 Jun 2022 17:39:39 -0700 Subject: [PATCH 030/436] Revert "UI: remove DM icon (#24771)" This reverts commit 4d836c6edd6e0f2639a3e2500794990da339eddc. --- selfdrive/ui/qt/onroad.cc | 10 +++++++++- selfdrive/ui/qt/onroad.h | 5 +++++ selfdrive/ui/ui.cc | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index e549f62a232253..ce61c094bd6bd7 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -166,6 +166,7 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { NvgWindow::NvgWindow(VisionStreamType type, QWidget* parent) : fps_filter(UI_FREQ, 3, 1. / UI_FREQ), CameraViewWidget("camerad", type, true, parent) { engage_img = loadPixmap("../assets/img_chffr_wheel.png", {img_size, img_size}); + dm_img = loadPixmap("../assets/img_driver_face.png", {img_size, img_size}); } void NvgWindow::updateState(const UIState &s) { @@ -185,11 +186,13 @@ void NvgWindow::updateState(const UIState &s) { setProperty("speed", QString::number(std::nearbyint(cur_speed))); setProperty("maxSpeed", maxspeed_str); setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); + setProperty("hideDM", cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE); setProperty("status", s.status); - // update engageability at 2Hz + // update engageability and DM icons at 2Hz if (sm.frame % (UI_FREQ / 2) == 0) { setProperty("engageable", cs.getEngageable() || cs.getEnabled()); + setProperty("dmActive", sm["driverMonitoringState"].getDriverMonitoringState().getIsActiveMode()); } } @@ -231,6 +234,11 @@ void NvgWindow::drawHud(QPainter &p) { engage_img, bg_colors[status], 1.0); } + // dm icon + if (!hideDM) { + drawIcon(p, radius / 2 + (bdr_s * 2), rect().bottom() - footer_h / 2, + dm_img, QColor(0, 0, 0, 70), dmActive ? 1.0 : 0.2); + } p.restore(); } diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index e1f665149e005e..6ca2b3c738506c 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -32,6 +32,8 @@ class NvgWindow : public CameraViewWidget { Q_PROPERTY(QString maxSpeed MEMBER maxSpeed); Q_PROPERTY(bool is_cruise_set MEMBER is_cruise_set); Q_PROPERTY(bool engageable MEMBER engageable); + Q_PROPERTY(bool dmActive MEMBER dmActive); + Q_PROPERTY(bool hideDM MEMBER hideDM); Q_PROPERTY(int status MEMBER status); public: @@ -43,6 +45,7 @@ class NvgWindow : public CameraViewWidget { void drawText(QPainter &p, int x, int y, const QString &text, int alpha = 255); QPixmap engage_img; + QPixmap dm_img; const int radius = 192; const int img_size = (radius / 2) * 1.5; QString speed; @@ -50,6 +53,8 @@ class NvgWindow : public CameraViewWidget { QString maxSpeed; bool is_cruise_set = false; bool engageable = false; + bool dmActive = false; + bool hideDM = false; int status = STATUS_DISENGAGED; protected: diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index f7c2c90437f690..c1af4ed6f42253 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -231,7 +231,7 @@ void UIState::updateStatus() { UIState::UIState(QObject *parent) : QObject(parent) { sm = std::make_unique>({ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "roadCameraState", - "pandaStates", "carParams", "sensorEvents", "carState", "liveLocationKalman", + "pandaStates", "carParams", "driverMonitoringState", "sensorEvents", "carState", "liveLocationKalman", "wideRoadCameraState", "managerState", "navInstruction", "navRoute", }); From 47f80e7add8635e2d80a395f72906cd40c956459 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 9 Jun 2022 17:44:58 -0700 Subject: [PATCH 031/436] Hyundai: Tucson 2021 support (#24791) * add support for 2021 Hyundai Tucson base configuration and fingerprints * fix fcw again and don't use mando radar for tucson * last fixes for working tucson * Apply suggestions from code review * add to cars * increase steerRatio * missing car info * one platform * rename * add min enable speed * update releases Co-authored-by: bluesforte Co-authored-by: Adeeb Shihadeh --- RELEASES.md | 1 + docs/CARS.md | 1 + selfdrive/car/hyundai/interface.py | 4 ++-- selfdrive/car/hyundai/values.py | 19 +++++++++++++------ selfdrive/car/tests/routes.py | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index f6d40e6c769528..acbafd5f5b5a20 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,7 @@ Version 0.8.15 (2022-XX-XX) * automatically determines which side the driver is on (soon) * Reduced power usage: device runs cooler and fan spins less * AGNOS 5 +* Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! Version 0.8.14 (2022-06-01) diff --git a/docs/CARS.md b/docs/CARS.md index 20c29e3cb5127e..c8614c80371e40 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -172,6 +172,7 @@ How We Rate The Cars |Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|||||| |Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|||||| |Hyundai|Sonata 2018-19|SCC + LKAS|||||| +|Hyundai|Tucson 2021|SCC + LKAS|||||| |Hyundai|Veloster 2019-20|SCC + LKAS|||||| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| |Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index e051079dc4e2e2..37f29d23c393c8 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -170,9 +170,9 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl tire_stiffness_factor = 0.5 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] - elif candidate == CAR.TUCSON_DIESEL_2019: + elif candidate == CAR.TUCSON: ret.lateralTuning.pid.kf = 0.00005 - ret.mass = 3633. * CV.LB_TO_KG + ret.mass = 3520. * CV.LB_TO_KG ret.wheelbase = 2.67 ret.steerRatio = 14.00 * 1.15 tire_stiffness_factor = 0.385 diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 54bbd80475330a..7d3ab93cdee778 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -52,7 +52,7 @@ class CAR: SANTA_FE_PHEV_2022 = "HYUNDAI SANTA FE PlUG-IN HYBRID 2022" SONATA = "HYUNDAI SONATA 2020" SONATA_LF = "HYUNDAI SONATA 2019" - TUCSON_DIESEL_2019 = "HYUNDAI TUCSON DIESEL 2019" + TUCSON = "HYUNDAI TUCSON 2019" PALISADE = "HYUNDAI PALISADE 2020" VELOSTER = "HYUNDAI VELOSTER 2019" SONATA_HYBRID = "HYUNDAI SONATA HYBRID 2021" @@ -105,7 +105,10 @@ class HyundaiCarInfo(CarInfo): CAR.SANTA_FE_PHEV_2022: HyundaiCarInfo("Hyundai Santa Fe Plug-in Hybrid 2022", "All", harness=Harness.hyundai_l), CAR.SONATA: HyundaiCarInfo("Hyundai Sonata 2020-22", "All", video_link="https://www.youtube.com/watch?v=ix63r9kE3Fw", harness=Harness.hyundai_a), CAR.SONATA_LF: HyundaiCarInfo("Hyundai Sonata 2018-19", harness=Harness.hyundai_e), - CAR.TUCSON_DIESEL_2019: HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness=Harness.hyundai_l), + CAR.TUCSON: [ + HyundaiCarInfo("Hyundai Tucson 2021", min_enable_speed=19 * CV.MPH_TO_MS, harness=Harness.hyundai_l), + HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness=Harness.hyundai_l), + ], CAR.PALISADE: [ HyundaiCarInfo("Hyundai Palisade 2020-21", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), HyundaiCarInfo("Kia Telluride 2020", harness=Harness.hyundai_h), @@ -508,18 +511,22 @@ class Buttons: b'\xf1\x87LAHSGN012918KF10\x98\x88x\x87\x88\x88x\x87\x88\x88\x98\x88\x87w\x88w\x88\x88\x98\x886o\xf6\xff\x98w\x7f\xff3\x00\xf1\x816W3B1051\x00\x00\xf1\x006W351_C2\x00\x006W3B1051\x00\x00TLF0T20NL2H\r\xbdm', ], }, - CAR.TUCSON_DIESEL_2019: { + CAR.TUCSON: { (Ecu.fwdRadar, 0x7d0, None): [ b'\xf1\x00TL__ FCA F-CUP 1.00 1.01 99110-D3500 ', + b'\xf1\x00TL__ FCA F-CUP 1.00 1.02 99110-D3510 ', ], (Ecu.engine, 0x7e0, None): [ b'\xf1\x8971TLC2NAIDDIR002\xf1\x8271TLC2NAIDDIR002', + b'\xf1\x81606G3051\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00TL MFC AT KOR LHD 1.00 1.02 95895-D3800 180719', + b'\xf1\x00TL MFC AT USA LHD 1.00 1.06 95895-D3800 190107', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x87LBJXAN202299KF22\x87x\x87\x88ww\x87xx\x88\x97\x88\x87\x88\x98x\x88\x99\x98\x89\x87o\xf6\xff\x87w\x7f\xff\x12\x9a\xf1\x81U083\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U083\x00\x00\x00\x00\x00\x00TTL2V20KL1\x8fRn\x8a', + b'\xf1\x87KMLDCU585233TJ20wx\x87\x88x\x88\x98\x89vfwfwwww\x87f\x9f\xff\x98\xff\x7f\xf9\xf7s\xf1\x816T6G4051\x00\x00\xf1\x006T6J0_C2\x00\x006T6G4051\x00\x00TTL4G24NH2\x00\x00\x00\x00', ], }, CAR.SANTA_FE: { @@ -1198,11 +1205,11 @@ class Buttons: FEATURES = { # which message has the gear "use_cluster_gears": {CAR.ELANTRA, CAR.ELANTRA_GT_I30, CAR.KONA}, - "use_tcu_gears": {CAR.KIA_OPTIMA, CAR.SONATA_LF, CAR.VELOSTER, CAR.TUCSON_DIESEL_2019}, + "use_tcu_gears": {CAR.KIA_OPTIMA, CAR.SONATA_LF, CAR.VELOSTER, CAR.TUCSON}, "use_elect_gears": {CAR.KIA_NIRO_EV, CAR.KIA_NIRO_HEV, CAR.KIA_NIRO_HEV_2021, CAR.KIA_OPTIMA_H, CAR.IONIQ_EV_LTD, CAR.KONA_EV, CAR.IONIQ, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.ELANTRA_HEV_2021, CAR.SONATA_HYBRID, CAR.KONA_HEV, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.IONIQ_PHEV_2019}, # these cars use the FCA11 message for the AEB and FCW signals, all others use SCC12 - "use_fca": {CAR.SONATA, CAR.SONATA_HYBRID, CAR.ELANTRA, CAR.ELANTRA_2021, CAR.ELANTRA_HEV_2021, CAR.ELANTRA_GT_I30, CAR.KIA_STINGER, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KONA_EV, CAR.KIA_FORTE, CAR.KIA_NIRO_EV, CAR.PALISADE, CAR.GENESIS_G70, CAR.GENESIS_G70_2020, CAR.KONA, CAR.SANTA_FE, CAR.KIA_SELTOS, CAR.KONA_HEV, CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.TUCSON_DIESEL_2019}, + "use_fca": {CAR.SONATA, CAR.SONATA_HYBRID, CAR.ELANTRA, CAR.ELANTRA_2021, CAR.ELANTRA_HEV_2021, CAR.ELANTRA_GT_I30, CAR.KIA_STINGER, CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.KONA_EV, CAR.KIA_FORTE, CAR.KIA_NIRO_EV, CAR.PALISADE, CAR.GENESIS_G70, CAR.GENESIS_G70_2020, CAR.KONA, CAR.SANTA_FE, CAR.KIA_SELTOS, CAR.KONA_HEV, CAR.SANTA_FE_2022, CAR.KIA_K5_2021, CAR.IONIQ_HEV_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022, CAR.TUCSON}, } HDA2_CAR = {CAR.KIA_EV6, } @@ -1250,7 +1257,7 @@ class Buttons: CAR.SANTA_FE_PHEV_2022: dbc_dict('hyundai_kia_generic', None), CAR.SONATA: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar'), CAR.SONATA_LF: dbc_dict('hyundai_kia_generic', None), # Has 0x5XX messages, but different format - CAR.TUCSON_DIESEL_2019: dbc_dict('hyundai_kia_generic', None), + CAR.TUCSON: dbc_dict('hyundai_kia_generic', None), CAR.PALISADE: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar'), CAR.VELOSTER: dbc_dict('hyundai_kia_generic', None), CAR.KIA_CEED: dbc_dict('hyundai_kia_generic', None), diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index e7942f517c1896..a9d36f9eabd636 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -84,7 +84,7 @@ TestRoute("c75a59efa0ecd502|2021-03-11--20-52-55", HYUNDAI.KIA_SELTOS), TestRoute("5b7c365c50084530|2020-04-15--16-13-24", HYUNDAI.SONATA), TestRoute("b2a38c712dcf90bd|2020-05-18--18-12-48", HYUNDAI.SONATA_LF), - TestRoute("fb3fd42f0baaa2f8|2022-03-30--15-25-05", HYUNDAI.TUCSON_DIESEL_2019), + TestRoute("fb3fd42f0baaa2f8|2022-03-30--15-25-05", HYUNDAI.TUCSON), TestRoute("5875672fc1d4bf57|2020-07-23--21-33-28", HYUNDAI.KIA_SORENTO), TestRoute("9c917ba0d42ffe78|2020-04-17--12-43-19", HYUNDAI.PALISADE), TestRoute("3f29334d6134fcd4|2022-03-30--22-00-50", HYUNDAI.IONIQ_PHEV_2019), From 1b402687e4bb7146a9aa0c92d8b5062ce53aeb22 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 9 Jun 2022 18:51:34 -0700 Subject: [PATCH 032/436] docs: adjust steering torque thresholds (#24798) * Prius has good steering control now * 1.5 is the threshold for good torque * get back down there, hondas * half stars for 1.0-1.5 * show number of cars * try bigger * emphasize tiers balanced * Add half star * Update ref_commit --- docs/CARS.md | 148 +++++++++++------------ selfdrive/car/CARS_template.md | 5 +- selfdrive/car/docs_definitions.py | 31 ++--- selfdrive/car/honda/interface.py | 4 +- selfdrive/car/toyota/values.py | 9 +- selfdrive/test/process_replay/ref_commit | 2 +- 6 files changed, 100 insertions(+), 99 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index c8614c80371e40..e9b9e947f3e4e1 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -25,7 +25,8 @@ How We Rate The Cars - - No steering control below certain speeds. ### Steering Torque -- - Car has enough steering torque for comfortable highway driving. +- - Car has enough steering torque to take tighter turns. +- - Car has enough steering torque for comfortable highway driving. - - Limited ability to make turns. ### Actively Maintained @@ -34,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -## Gold Cars +# Gold - 29 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -53,6 +54,8 @@ How We Rate The Cars |Lexus|NX 2020|All|||||| |Lexus|NX Hybrid 2020|All|||||| |Lexus|UX Hybrid 2019-21|All|||||| +|Toyota|Avalon 2022|All|||||| +|Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| |Toyota|Corolla 2020-22|All|||||| @@ -66,11 +69,17 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -## Silver Cars +# Silver - 78 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| +|Audi|A3 2014-19|ACC + Lane Assist|||||| +|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|||||| |Audi|Q2 2018|ACC + Lane Assist|||||| +|Audi|Q3 2020-21|ACC + Lane Assist|||||| +|Audi|RS3 2018|ACC + Lane Assist|||||| +|Audi|S3 2015-17|ACC + Lane Assist|||||| +|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| |Genesis|G70 2018|All|||||| |Genesis|G80 2018|All|||||| |Hyundai|Elantra 2021-22|SCC + LKAS|||||| @@ -84,6 +93,7 @@ How We Rate The Cars |Hyundai|Santa Fe 2021-22|All|||||| |Hyundai|Santa Fe Hybrid 2022|All|||||| |Hyundai|Santa Fe Plug-in Hybrid 2022|All|||||| +|Hyundai|Sonata 2018-19|SCC + LKAS|||||| |Hyundai|Tucson Diesel 2019|SCC + LKAS|||||| |Kia|Ceed 2019|SCC + LKAS|||||| |Kia|Forte 2018|SCC + LKAS|||||| @@ -99,13 +109,13 @@ How We Rate The Cars |Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|NX 2018-19|All|[3](#footnotes)||||| |Lexus|NX Hybrid 2018-19|All|[3](#footnotes)||||| -|Lexus|RX 2020-22|All|||||| -|Lexus|RX Hybrid 2020-21|All|||||| +|Lexus|RX 2020-22|All|||||| +|Lexus|RX Hybrid 2020-21|All|||||| |Mazda|CX-5 2022|All|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| |Subaru|Forester 2019-21|All|||||| -|Škoda|Kamiq 2021[6](#footnotes)|Driver Assistance|||||| +|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| |Škoda|Karoq 2019|Driver Assistance|||||| |Škoda|Kodiaq 2018-19|Driver Assistance|||||| |Škoda|Octavia 2015, 2018-19|Driver Assistance|||||| @@ -114,75 +124,82 @@ How We Rate The Cars |Škoda|Superb 2015-18|Driver Assistance|||||| |Toyota|Alphard 2019-20|All|||||| |Toyota|Alphard Hybrid 2021|All|||||| -|Toyota|Avalon 2022|All|||||| -|Toyota|Avalon Hybrid 2022|All|||||| +|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| +|Toyota|Highlander 2017-19|All|[3](#footnotes)||||| +|Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| +|Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| +|Toyota|Prius Prime 2017-20|All|[3](#footnotes)||||| |Toyota|RAV4 2022|All|||||| +|Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 Hybrid 2022|All|||||| -|Volkswagen|Arteon 2018, 2021[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|Passat 2015-19[7](#footnotes)|Driver Assistance|||||| +|Toyota|Sienna 2018-20|All|[3](#footnotes)||||| +|Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|e-Golf 2014, 2018-20|Driver Assistance|||||| +|Volkswagen|Golf 2015-20|Driver Assistance|||||| +|Volkswagen|Golf Alltrack 2017-18|Driver Assistance|||||| +|Volkswagen|Golf GTE 2016|Driver Assistance|||||| +|Volkswagen|Golf GTI 2018-21|Driver Assistance|||||| +|Volkswagen|Golf R 2016-19|Driver Assistance|||||| +|Volkswagen|Golf SportsVan 2016|Driver Assistance|||||| +|Volkswagen|Golf SportWagen 2015|Driver Assistance|||||| +|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Polo 2020|Driver Assistance|||||| -|Volkswagen|T-Cross 2021[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|T-Roc 2021[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|Taos 2022[8](#footnotes)|Driver Assistance|||||| +|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -## Bronze Cars +# Bronze - 66 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-19|AcuraWatch Plus|||||| |Acura|RDX 2016-18|AcuraWatch Plus|||||| -|Acura|RDX 2019-21|All|||||| -|Audi|A3 2014-19|ACC + Lane Assist|||||| -|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|||||| -|Audi|Q3 2020-21|ACC + Lane Assist|||||| -|Audi|RS3 2018|ACC + Lane Assist|||||| -|Audi|S3 2015-17|ACC + Lane Assist|||||| +|Acura|RDX 2019-21|All|||||| |Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| -|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica 2020|Adaptive Cruise|||||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica Hybrid 2019-21|Adaptive Cruise|||||| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| +|Chrysler|Pacifica Hybrid 2019-21|Adaptive Cruise|||||| |Genesis|G90 2018|All|||||| -|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| -|Honda|Accord 2018-21|All|||||| -|Honda|Accord Hybrid 2018-21|All|||||| +|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| +|Honda|Accord 2018-21|All|||||| +|Honda|Accord Hybrid 2018-21|All|||||| |Honda|Civic 2016-18|Honda Sensing|||||| -|Honda|Civic 2019-20|All|||[2](#footnotes)||| -|Honda|Civic Hatchback 2017-21|Honda Sensing|||||| +|Honda|Civic 2019-20|All|||[2](#footnotes)||| +|Honda|Civic Hatchback 2017-21|Honda Sensing|||||| |Honda|CR-V 2015-16|Touring|||||| -|Honda|CR-V 2017-21|Honda Sensing|||||| +|Honda|CR-V 2017-21|Honda Sensing|||||| |Honda|CR-V Hybrid 2017-19|Honda Sensing|||||| |Honda|e 2020|All|||||| -|Honda|Fit 2018-19|Honda Sensing|||||| +|Honda|Fit 2018-19|Honda Sensing|||||| |Honda|Freed 2020|Honda Sensing|||||| |Honda|HR-V 2019-20|Honda Sensing|||||| -|Honda|Insight 2019-21|All|||||| -|Honda|Inspire 2018|All|||||| +|Honda|Insight 2019-21|All|||||| +|Honda|Inspire 2018|All|||||| |Honda|Odyssey 2018-20|Honda Sensing|||||| -|Honda|Passport 2019-21|All|||||| -|Honda|Pilot 2016-21|Honda Sensing|||||| -|Honda|Ridgeline 2017-22|Honda Sensing|||||| +|Honda|Passport 2019-21|All|||||| +|Honda|Pilot 2016-21|Honda Sensing|||||| +|Honda|Ridgeline 2017-22|Honda Sensing|||||| |Hyundai|Elantra 2017-19|SCC + LKAS|||||| |Hyundai|Genesis 2015-16|SCC + LKAS|||||| |Hyundai|Ioniq Electric 2019|SCC + LKAS|||||| |Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|||||| |Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|||||| -|Hyundai|Sonata 2018-19|SCC + LKAS|||||| |Hyundai|Tucson 2021|SCC + LKAS|||||| |Hyundai|Veloster 2019-20|SCC + LKAS|||||| -|Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| -|Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| +|Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| +|Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| |Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| |Kia|Optima 2017|SCC + LKAS|||||| |Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|IS 2017-19|All|||||| |Lexus|RC 2020|All|||||| -|Lexus|RX 2016-18|All|[3](#footnotes)||||| -|Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| +|Lexus|RX 2016-18|All|[3](#footnotes)||||| +|Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| |Mazda|CX-9 2021|All|||||| |Nissan|Altima 2019-20|ProPILOT|||||| |Nissan|Leaf 2018-22|ProPILOT|||||| @@ -190,37 +207,21 @@ How We Rate The Cars |Nissan|X-Trail 2017|ProPILOT|||||| |Subaru|Ascent 2019-20|All|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| -|Subaru|Crosstrek 2020-21|EyeSight|||||| +|Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| -|Subaru|Impreza 2020-21|EyeSight|||||| -|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|C-HR 2017-21|All|||||| -|Toyota|C-HR Hybrid 2017-19|All|||||| +|Subaru|Impreza 2020-21|EyeSight|||||| +|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| +|Toyota|C-HR 2017-21|All|||||| +|Toyota|C-HR Hybrid 2017-19|All|||||| |Toyota|Corolla 2017-19|All|[3](#footnotes)||||| -|Toyota|Highlander 2017-19|All|[3](#footnotes)||||| -|Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| -|Toyota|Prius 2016-20|TSS-P|[3](#footnotes)|||[5](#footnotes)|| -|Toyota|Prius Prime 2017-20|All|[3](#footnotes)|||[5](#footnotes)|| -|Toyota|Prius v 2017|TSS-P|[3](#footnotes)|||[5](#footnotes)|| -|Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|Sienna 2018-20|All|[3](#footnotes)||||| -|Volkswagen|Atlas 2018-19, 2022[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|California 2021[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|Caravelle 2020[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|e-Golf 2014, 2018-20|Driver Assistance|||||| -|Volkswagen|Golf 2015-20|Driver Assistance|||||| -|Volkswagen|Golf Alltrack 2017-18|Driver Assistance|||||| -|Volkswagen|Golf GTE 2016|Driver Assistance|||||| -|Volkswagen|Golf GTI 2018-21|Driver Assistance|||||| -|Volkswagen|Golf R 2016-19|Driver Assistance|||||| -|Volkswagen|Golf SportsVan 2016|Driver Assistance|||||| -|Volkswagen|Golf SportWagen 2015|Driver Assistance|||||| -|Volkswagen|Jetta 2018-21|Driver Assistance|||||| -|Volkswagen|Jetta GLI 2021|Driver Assistance|||||| -|Volkswagen|Tiguan 2019-22[8](#footnotes)|Driver Assistance|||||| +|Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| +|Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| +|Volkswagen|Atlas 2018-19, 2022[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Jetta 2018-21|Driver Assistance|||||| +|Volkswagen|Jetta GLI 2021|Driver Assistance|||||| +|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|||||| @@ -228,10 +229,9 @@ How We Rate The Cars 22019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
3When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
428mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
-5An inaccurate steering wheel angle sensor makes precise control difficult.
-6Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
-7Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.
-8Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.
+5Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
+6Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.
+7Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). \ No newline at end of file diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index cd9b7bc6eef7d8..f93dc64b06c4f2 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -28,7 +28,8 @@ How We Rate The Cars - {{star_icon.format(Star.EMPTY.value)}} - No steering control below certain speeds. ### Steering Torque -- {{star_icon.format(Star.FULL.value)}} - Car has enough steering torque for comfortable highway driving. +- {{star_icon.format(Star.FULL.value)}} - Car has enough steering torque to take tighter turns. +- {{star_icon.format(Star.HALF.value)}} - Car has enough steering torque for comfortable highway driving. - {{star_icon.format(Star.EMPTY.value)}} - Limited ability to make turns. ### Actively Maintained @@ -38,7 +39,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** {% for tier, cars in tiers.items() %} -## {{tier.name.title()}} Cars +# {{tier.name.title()}} - {{cars | length}} cars |{{Column | map(attribute='value') | join('|')}}| |---|---|---|:---:|:---:|:---:|:---:|:---:| diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 95561872accfce..e3b46c29e4da60 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -6,7 +6,9 @@ from enum import Enum from typing import Dict, List, Optional, Union, no_type_check -STEERING_TORQUE_THRESHOLD = 2.0 # m/s^2 +TACO_TORQUE_THRESHOLD = 2.5 # m/s^2 +GREAT_TORQUE_THRESHOLD = 1.5 # m/s^2 +GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2 class Tier(Enum): @@ -70,11 +72,6 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic if self.min_enable_speed is not None: min_enable_speed = self.min_enable_speed - # TODO: remove hardcoded good torque and just use maxLateralAccel - good_torque = self.good_torque - if not math.isnan(CP.maxLateralAccel): - good_torque = CP.maxLateralAccel >= STEERING_TORQUE_THRESHOLD - self.car_name = CP.carName self.make, self.model = self.name.split(' ', 1) self.row = { @@ -82,21 +79,27 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic Column.MODEL: self.model, Column.PACKAGE: self.package, # StarColumns - Column.LONGITUDINAL: CP.openpilotLongitudinalControl and not CP.radarOffCan, - Column.FSR_LONGITUDINAL: min_enable_speed <= 0., - Column.FSR_STEERING: min_steer_speed <= 0., - Column.STEERING_TORQUE: good_torque, - Column.MAINTAINED: CP.carFingerprint not in non_tested_cars and self.harness is not Harness.none, + Column.LONGITUDINAL: Star.FULL if CP.openpilotLongitudinalControl and not CP.radarOffCan else Star.EMPTY, + Column.FSR_LONGITUDINAL: Star.FULL if min_enable_speed <= 0. else Star.EMPTY, + Column.FSR_STEERING: Star.FULL if min_steer_speed <= 0. else Star.EMPTY, + Column.STEERING_TORQUE: Star.FULL if self.good_torque else Star.EMPTY, # TODO: remove hardcoding and use maxLateralAccel + Column.MAINTAINED: Star.FULL if CP.carFingerprint not in non_tested_cars and self.harness is not Harness.none else Star.EMPTY, } + if not math.isnan(CP.maxLateralAccel): + if CP.maxLateralAccel >= GREAT_TORQUE_THRESHOLD: + self.row[Column.STEERING_TORQUE] = Star.FULL + elif CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: + self.row[Column.STEERING_TORQUE] = Star.HALF + else: + self.row[Column.STEERING_TORQUE] = Star.EMPTY + if CP.notCar: for col in StarColumns: - self.row[col] = True + self.row[col] = Star.FULL self.all_footnotes = all_footnotes for column in StarColumns: - self.row[column] = Star.FULL if self.row[column] else Star.EMPTY - # Demote if footnote specifies a star footnote = get_footnote(self.footnotes, column) if footnote is not None and footnote.value.star is not None: diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index ad98f8863f6237..de5db98ea67dd3 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -100,7 +100,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 8000], [0, 2560, 3840]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.1]] else: - ret.maxLateralAccel = 1.7 + ret.maxLateralAccel = 0.4 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]] tire_stiffness_factor = 1. @@ -245,7 +245,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.35 # as spec ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.82 - ret.maxLateralAccel = 1.5 + ret.maxLateralAccel = 0.8 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]] elif candidate == CAR.ODYSSEY_CHN: diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 91a7fa5d10dac6..7e6cfa0d09f1ca 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -93,9 +93,6 @@ class Footnote(Enum): CAMRY = CarFootnote( "28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.", Column.FSR_LONGITUDINAL) - ANGLE_SENSOR = CarFootnote( - "An inaccurate steering wheel angle sensor makes precise control difficult.", - Column.STEERING_TORQUE, star=Star.HALF) @dataclass @@ -133,10 +130,10 @@ class ToyotaCarInfo(CarInfo): CAR.HIGHLANDERH: ToyotaCarInfo("Toyota Highlander Hybrid 2017-19", footnotes=[Footnote.DSU]), CAR.HIGHLANDERH_TSS2: ToyotaCarInfo("Toyota Highlander Hybrid 2020-22"), CAR.PRIUS: [ - ToyotaCarInfo("Toyota Prius 2016-20", "TSS-P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU, Footnote.ANGLE_SENSOR]), - ToyotaCarInfo("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU, Footnote.ANGLE_SENSOR]), + ToyotaCarInfo("Toyota Prius 2016-20", "TSS-P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), ], - CAR.PRIUS_V: ToyotaCarInfo("Toyota Prius v 2017", "TSS-P", min_enable_speed=MIN_ACC_SPEED, footnotes=[Footnote.DSU, Footnote.ANGLE_SENSOR]), + CAR.PRIUS_V: ToyotaCarInfo("Toyota Prius v 2017", "TSS-P", min_enable_speed=MIN_ACC_SPEED, footnotes=[Footnote.DSU]), CAR.PRIUS_TSS2: [ ToyotaCarInfo("Toyota Prius 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"), ToyotaCarInfo("Toyota Prius Prime 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"), diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 3a9b1d94797b26..2f425709341d58 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -0e18bb3317437f2cad6d0a5782a9222eda655d58 +f301bdf52b271c8c4ef0f2bce3be9c8f90037652 From 53724e8b6cbcfc6c9d49ebd0834624fa23d0f6e9 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 9 Jun 2022 19:29:54 -0700 Subject: [PATCH 033/436] bypass HDA2 dashcam with file (#24803) Co-authored-by: Comma Device --- selfdrive/car/hyundai/interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 37f29d23c393c8..831b318d556208 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os from cereal import car from panda import Panda from common.conversions import Conversions as CV @@ -37,7 +38,9 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl # These cars have been put into dashcam only due to both a lack of users and test coverage. # These cars likely still work fine. Once a user confirms each car works and a test route is # added to selfdrive/car/tests/routes.py, we can remove it from this list. - ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} or candidate in HDA2_CAR + ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} + if candidate in HDA2_CAR: + ret.dashcamOnly = not os.path.exists('/data/enable-ev6') ret.steerActuatorDelay = 0.1 # Default delay ret.steerRateCost = 0.5 From 60aa553b8c0d8323f276b102ce359262e639861b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 9 Jun 2022 20:56:05 -0700 Subject: [PATCH 034/436] Honda: use common imperial unit message (#24786) * CAR_SPEED should be on all cars * bump opendbc * clean this up too * it's an or * clean up * comment --- opendbc | 2 +- selfdrive/car/honda/carstate.py | 49 ++++++++++----------------------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/opendbc b/opendbc index 30aacafa10e72d..ec0e1f20bae4c3 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 30aacafa10e72db33ecf5e3b7a6eacffa9390b8b +Subproject commit ec0e1f20bae4c39326036c0061418404ac15ae9e diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 5314fe375ebe44..f5cdc838c4cc8c 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -22,9 +22,9 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): ("STEER_ANGLE_RATE", "STEERING_SENSORS"), ("MOTOR_TORQUE", "STEER_MOTOR_TORQUE"), ("STEER_TORQUE_SENSOR", "STEER_STATUS"), + ("IMPERIAL_UNIT", "CAR_SPEED"), ("LEFT_BLINKER", "SCM_FEEDBACK"), ("RIGHT_BLINKER", "SCM_FEEDBACK"), - ("GEAR", gearbox_msg), ("SEATBELT_DRIVER_LAMP", "SEATBELT_STATUS"), ("SEATBELT_DRIVER_LATCHED", "SEATBELT_STATUS"), ("BRAKE_PRESSED", "POWERTRAIN_DATA"), @@ -35,6 +35,7 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): ("BRAKE_HOLD_ACTIVE", "VSA_STATUS"), ("STEER_STATUS", "STEER_STATUS"), ("GEAR_SHIFTER", gearbox_msg), + ("GEAR", gearbox_msg), ("PEDAL_GAS", "POWERTRAIN_DATA"), ("CRUISE_SETTING", "SCM_BUTTONS"), ("ACC_STATUS", "POWERTRAIN_DATA"), @@ -48,9 +49,10 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): ("SEATBELT_STATUS", 10), ("CRUISE", 10), ("POWERTRAIN_DATA", 100), + ("CAR_SPEED", 10), ("VSA_STATUS", 50), ("STEER_STATUS", 100), - ("STEER_MOTOR_TORQUE", 0), # TODO: not on every car + ("STEER_MOTOR_TORQUE", 0), # TODO: not on every car ] if CP.carFingerprint == CAR.ODYSSEY_CHN: @@ -73,16 +75,11 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): signals.append(("BRAKE_PRESSED", "BRAKE_MODULE")) checks.append(("BRAKE_MODULE", 50)) - if CP.carFingerprint in HONDA_BOSCH: - signals += [ - ("EPB_STATE", "EPB_STATUS"), - ("IMPERIAL_UNIT", "CAR_SPEED"), - ] - checks += [ - ("EPB_STATUS", 50), - ("CAR_SPEED", 10), - ] + if CP.carFingerprint in (HONDA_BOSCH | {CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN}): + signals.append(("EPB_STATE", "EPB_STATUS")) + checks.append(("EPB_STATUS", 50)) + if CP.carFingerprint in HONDA_BOSCH: if not CP.openpilotLongitudinalControl: signals += [ ("CRUISE_CONTROL_LABEL", "ACC_HUD"), @@ -121,17 +118,6 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): ("STANDSTILL", 50), ] - if CP.carFingerprint == CAR.CIVIC: - signals += [("IMPERIAL_UNIT", "HUD_SETTING"), - ("EPB_STATE", "EPB_STATUS")] - checks += [ - ("HUD_SETTING", 50), - ("EPB_STATUS", 50), - ] - elif CP.carFingerprint in (CAR.ODYSSEY, CAR.ODYSSEY_CHN): - signals.append(("EPB_STATE", "EPB_STATUS")) - checks.append(("EPB_STATUS", 50)) - # add gas interceptor reading if we are using it if CP.enableGasInterceptor: signals.append(("INTERCEPTOR_GAS", "GAS_SENSOR")) @@ -179,6 +165,11 @@ def update(self, cp, cp_cam, cp_body): # update prevs, update must run once per loop self.prev_cruise_buttons = self.cruise_buttons self.prev_cruise_setting = self.cruise_setting + self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"] + self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"] + + # used for car hud message + self.is_metric = not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"] # ******************* parse out can ******************* # STANDSTILL->WHEELS_MOVING bit can be noisy around zero, so use XMISSION_SPEED @@ -219,16 +210,12 @@ def update(self, cp, cp_cam, cp_body): ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE"] ret.steeringRateDeg = cp.vl["STEERING_SENSORS"]["STEER_ANGLE_RATE"] - self.cruise_setting = cp.vl["SCM_BUTTONS"]["CRUISE_SETTING"] - self.cruise_buttons = cp.vl["SCM_BUTTONS"]["CRUISE_BUTTONS"] - ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_stalk( 250, cp.vl["SCM_FEEDBACK"]["LEFT_BLINKER"], cp.vl["SCM_FEEDBACK"]["RIGHT_BLINKER"]) ret.brakeHoldActive = cp.vl["VSA_STATUS"]["BRAKE_HOLD_ACTIVE"] == 1 # TODO: set for all cars - if self.CP.carFingerprint in (CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN, CAR.CRV_5G, CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, - CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E): + if self.CP.carFingerprint in (HONDA_BOSCH | {CAR.CIVIC, CAR.ODYSSEY, CAR.ODYSSEY_CHN}): ret.parkingBrake = cp.vl["EPB_STATUS"]["EPB_STATE"] != 0 gear = int(cp.vl[self.gearbox_msg]["GEAR_SHIFTER"]) @@ -281,14 +268,6 @@ def update(self, cp, cp_cam, cp_body): if ret.brake > 0.1: ret.brakePressed = True - # TODO: discover the CAN msg that has the imperial unit bit for all other cars - if self.CP.carFingerprint in (CAR.CIVIC, ): - self.is_metric = not cp.vl["HUD_SETTING"]["IMPERIAL_UNIT"] - elif self.CP.carFingerprint in HONDA_BOSCH: - self.is_metric = not cp.vl["CAR_SPEED"]["IMPERIAL_UNIT"] - else: - self.is_metric = False - if self.CP.carFingerprint in HONDA_BOSCH: ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5) else: From aeb2c67c904d9ed3a579e6ee40b05606b92713af Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 10 Jun 2022 15:39:18 -0700 Subject: [PATCH 035/436] add PyQt (#24811) --- Pipfile | 2 + Pipfile.lock | 448 ++++++++++++++++++++++++++++----------------------- 2 files changed, 248 insertions(+), 202 deletions(-) diff --git a/Pipfile b/Pipfile index 1e526a5ec0d506..3958c890fec1f0 100644 --- a/Pipfile +++ b/Pipfile @@ -84,6 +84,8 @@ urllib3 = "*" utm = "*" websocket_client = "*" hatanaka = "==2.4" +PyQt5 = "==5.15.4" +PyQt5-sip = "==12.9.0" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index b0ec993b2a766f..df2b3bb8532678 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "50d3486820d05997c1e084e70362fa080e0a294faa01c7f1d43219e44f94a80b" + "sha256": "4dbfaf9d7a532e0e9a126f828512a5383a817722a1580ef64b99e2427f366a72" }, "pipfile-spec": 6, "requires": { @@ -146,7 +146,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==2.0.12" }, "click": { @@ -347,7 +347,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.3" }, "importlib-metadata": { @@ -371,7 +371,7 @@ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "markers": "python_version < '4' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "itsdangerous": { @@ -687,33 +687,23 @@ }, "protobuf": { "hashes": [ - "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf", - "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f", - "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f", - "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7", - "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996", - "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067", - "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c", - "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7", - "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9", - "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c", - "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739", - "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91", - "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c", - "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153", - "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9", - "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388", - "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e", - "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab", - "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde", - "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531", - "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8", - "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7", - "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20", - "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3" + "sha256:0d4719e724472e296062ba8e82a36d64693fcfdb550d9dff98af70ca79eafe3d", + "sha256:2b35602cb65d53c168c104469e714bf68670335044c38eee3c899d6a8af03ffc", + "sha256:32fff501b6df3050936d1839b80ea5899bf34db24792d223d7640611f67de15a", + "sha256:34400fd76f85bdae9a2e9c1444ea4699c0280962423eff4418765deceebd81b5", + "sha256:3767c64593a49c7ac0accd08ed39ce42744405f0989d468f0097a17496fdbe84", + "sha256:3f2ed842e8ca43b790cb4a101bcf577226e0ded98a6a6ba2d5e12095a08dc4da", + "sha256:52c1e44e25f2949be7ffa7c66acbfea940b0945dd416920231f7cb30ea5ac6db", + "sha256:5d9b5c8270461706973c3871c6fbdd236b51dfe9dab652f1fb6a36aa88287e47", + "sha256:72d357cc4d834cc85bd957e8b8e1f4b64c2eac9ca1a942efeb8eb2e723fca852", + "sha256:79cd8d0a269b714f6b32641f86928c718e8d234466919b3f552bfb069dbb159b", + "sha256:a4c0c6f2f95a559e59a0258d3e4b186f340cbdc5adec5ce1bc06d01972527c88", + "sha256:b309fda192850ac4184ca1777aab9655564bc8d10a9cc98f10e1c8bf11295c22", + "sha256:b3d7d4b4945fe3c001403b6c24442901a5e58c0a3059290f5a63523ed4435f82", + "sha256:c8829092c5aeb61619161269b2f8a2e36fd7cb26abbd9282d3bc453f02769146" ], "markers": "python_version >= '3.7'", - "version": "==3.20.1" + "version": "==4.21.1" }, "psutil": { "hashes": [ @@ -841,11 +831,11 @@ }, "pylint": { "hashes": [ - "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731", - "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526" + "sha256:549261e0762c3466cc001024c4419c08252cb8c8d40f5c2c6966fea690e7fe2a", + "sha256:bb71e6d169506de585edea997e48d9ff20c0dc0e2fbc1d166bad6b640120326b" ], "index": "pypi", - "version": "==2.13.9" + "version": "==2.14.1" }, "pyopencl": { "hashes": [ @@ -890,6 +880,53 @@ "index": "pypi", "version": "==2022.1.5" }, + "pyqt5": { + "hashes": [ + "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025", + "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be", + "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3", + "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98", + "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7" + ], + "index": "pypi", + "version": "==5.15.4" + }, + "pyqt5-qt5": { + "hashes": [ + "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", + "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962", + "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", + "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327" + ], + "version": "==5.15.2" + }, + "pyqt5-sip": { + "hashes": [ + "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93", + "sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7", + "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206", + "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d", + "sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271", + "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121", + "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96", + "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055", + "sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a", + "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a", + "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e", + "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f", + "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a", + "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644", + "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7", + "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32", + "sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067", + "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a", + "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197", + "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850", + "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5" + ], + "index": "pypi", + "version": "==12.9.0" + }, "pyserial": { "hashes": [ "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", @@ -954,74 +991,74 @@ }, "pyzmq": { "hashes": [ - "sha256:011a45c846ec69a3671ed15893b74b6ad608800c89ac6d0f0411e2137c6b313d", - "sha256:011f26841dd56ed87e464c98023dbbd4c0b3ab8802a045de3ea83e0187eb8145", - "sha256:0258563bf69f6ca305204354f171e0627a9bf8fe78c9d4f63a5e2447035cbb4b", - "sha256:07d2008e51718fba60641e5d1a0646b222b7929f16f6e7cf0834b8439f42c9e8", - "sha256:0a787f7870cba38d655c68ea7ae14bb6c3e9e19bb618d0c2412513321eeaeb80", - "sha256:0a89b9860d2171bcf674648dc8186db9cf3b773ad3c0610a2c7bf189cf3560b6", - "sha256:0de8a7e13ffacfe33c89acc0d7bfa2f5bde94e3f74b7f1e4d43c97ce17864d77", - "sha256:12a53f5c13edf12547ce495afebdd5ab11c1b67ea078a941b21e13161783741a", - "sha256:12eac2294d48ee27d1eaef7e214acedb394e4c95e3a1f6e4467099b82d58ef73", - "sha256:176be6c348dbec04e8e0d41e810743b7084b73e50954a6fedeeafc65d7fa9290", - "sha256:1d480d48253f61ff90115b8069ed32f51a0907eb19101c4a5ae0b9a5973e40ad", - "sha256:21792f4d0fcc5040978ee211c033e915d8b6608ea8a5b33fe197a04f0d43e991", - "sha256:277b3ebc684b369a57a186a9acf629c1b01247eb04d1105536ef2dae5f61168a", - "sha256:2f227150148e7c3db7ecd8a58500439979f556e15455841a30b6d121755b14bc", - "sha256:34b143751e9b2b89cf9b656081f1b2842a563c4c9ffc8465531875daf546e772", - "sha256:3f3807e81bf51d4c63eb12a21920614e0e840645418e9f2e3b5ffdd5991b3415", - "sha256:3fa7126d532effee452c0ab395ab3cbef1c06fd6870ab7e681f812ba9e685cfa", - "sha256:434044eec7f9df08fc5ca5c9bdd1a4bb08663679d43ebe7b9849713956f4d85f", - "sha256:4d861ae20040afc17adef33053c328667da78d4d3676b2936788fd031665e3a8", - "sha256:536491ad640448f14d8aa2dc497c354a348f216eb23513bf5aa0ac40e2b02577", - "sha256:5619f6598d6fd30778053ae2daa48a7c54029816648b908270b751411fd52e74", - "sha256:591b455546d34bb96aa453dd9666bddb8c81314e23dbf2606f9614acf7e73d9f", - "sha256:5a13171268f05d127e31b4c369b753733f67dbb0d765901ef625a115feb5c7de", - "sha256:5eaf7e0841d3d8d1d92838c8b56f98cb9bf35b14bcbe4efa281e4812ef4be728", - "sha256:6c09e6e5c4baf0959287943dc8170624d739ae555d334e896a94d9de01c7bb21", - "sha256:6e2093a97bf3f6008a4be6b5bae8ae3fc409f18373593bef19dd7b381ab8030c", - "sha256:7b518ad9cdbaaeb1a9da3444797698871ae2eeae34ff9a656d5150d37e1e42a1", - "sha256:7ca7d77f24644298cbe53bc279eb7ca05f3b8637473d392f0c9f34b37f08b49a", - "sha256:7eca5902ff41575d9a26f91fc750018b7eb129600ea600fe69ce852fbdfab4e2", - "sha256:8951830d6a00636b3af478091f9668ecc486f1dad01b975527957fd1d8c31bfd", - "sha256:8c234aefeef034c5d6de452e2af5173a95ea06315b685db703091e6f937a6e60", - "sha256:9273f6d1da1018822f41630fb0f3fe208e8e70e5d5e780795326900cfa22d8b6", - "sha256:9622d9560a6fd8d589816cdcec6946642cb4e070b3f68be1d3779b52cf240f73", - "sha256:9ef2d1476cea927ba33a29f59aa128ce3b174e81083cbd091dd3149af741c85d", - "sha256:9feb7ccd426ff2158ce79f4c87a8a1600ed4f77e65e2fffda2b42638b2bc73e4", - "sha256:a0b8528aefceb787f41ad429f3210a3c6b52e99f85413416e3d0c9e6d035f8ac", - "sha256:a2e4d70d34112997a32c8193fae2579aec854745f8730031e5d84cc579fd98ff", - "sha256:a37f0ec88e220326803084208d80229218b309d728954ab747ab21cca33424aa", - "sha256:a45f5c0477d12df05ef2e2922b49b7c0ae9d0f4ff9b6bb0d666558df0ef37122", - "sha256:a64b9cce166396df5f33894074d6515778d48c63aae5ee1abd86d8bbc5a711d8", - "sha256:a89285fedbeca483a855a77285453e21e4fc86ef0944bc018ef4b3033aa04ad2", - "sha256:a8f40604437ec8010f77f7053fd135ccb202d6ca18329903831087cab8dbdab1", - "sha256:b2a4af5e6fa85ee1743c725b46579f8de0b97024eb5ae1a0b5c5711adc436665", - "sha256:b97dc1273f16f85a38cff6668a07b636ef14e30591039efbfd21f5f91efae964", - "sha256:bdd008629293a0d4f00b516841ac0df89f17a64bc2d83bcfa48212d3f3b3ca1a", - "sha256:be3425dfdb9c46dc62d490fc1a6142a5f3dc6605ebb9048ae675056ef621413c", - "sha256:c2394bb857607494c3750b5040f852a1ad7831d7a7907b6106f0af2c70860cef", - "sha256:cb45b7ea577283b547b907a3389d62ca2eaddaf725fbb79cd360802440fa9c91", - "sha256:cd3f563b98e2a8730c93bdc550f119ae766b2d3da1f0d6a3c7735b59adfa1642", - "sha256:cda55ff0a7566405fb29ca38db1829fecb4c041b8dc3f91754f337bb7b27cbd8", - "sha256:df0b05fa4321b090abe5601dea9b1c8933c06f496588ccb397a0b1f9dfe32ebe", - "sha256:e464e7b1be2216eba54b47256c15bf307ae4a656aa0f73becea7b3e7283c5ac2", - "sha256:e730d490b1421e52b43b1b9f5e1f8c3973499206e188f29b582577531e11033b", - "sha256:e7ae3e520bd182a0cbfff3cc69dda3a2c26f69847e81bd3f090ed04471fc1282", - "sha256:e9631c6a339843e4f95efb80ff9a1bfaaf3d611ba9677a7a5cc61ffb923b4e06", - "sha256:f3daabbe42ca31712e29d906dfa4bf1890341d2fd5178de118bc9977a8d2b23b", - "sha256:fe8807d67456e7cf0e9a33b85e0d05bb9d2977dbdb23977e4cc2b843633618fd" - ], - "index": "pypi", - "version": "==23.0.0" + "sha256:057176dd3f5ccf5aad4abd662d76b6a39bbf799baaf2f39cd4fdaf2eab326e43", + "sha256:05ec90a8da618f2398f9d1aa20b18a9ef332992c6ac23e8c866099faad6ef0d6", + "sha256:154de02b15422af28b53d29a02de72121ba503634955017255573fc1f995143d", + "sha256:16b832adb5d8716f46051da5533c480250bf126984ce86804db6137a3a7f931b", + "sha256:1df26aa854bdd3a8341bf199064dd6aa6e240f2eaa3c9fa8d217e5d8b868c73e", + "sha256:28f9164fb2658b7b414fa0894c75b1a9c61375774cdc1bdb7298beb042a2cd87", + "sha256:2951c29b8649f3672af9dca8ff61d86310d3664d9629788b1c66422fb13b1239", + "sha256:2b08774057ae7ce8a2eb4e7d54db05358234440706ce43a85814500c5d7bd22e", + "sha256:2e2ac40f7a91c740ec68d6db07ae19ea9259c959333c68bee56ab2c799a67d66", + "sha256:312e56799410c34797417a4060a8bd37d4db1f06d1ec0c54f7c8fd81e0d90376", + "sha256:38f778a74e3889392e949326cfd0e9b2eb37dcbb2980d98fad2c51703d523db2", + "sha256:3955dd5bbbe02f454655296ee36a66c334c7102a29b8458223d168c0380edfd5", + "sha256:425ba851a6f9892bde1da2024d82e2fe6796bd77e3391fb96665c50fe9d4c6a5", + "sha256:48bbc2db041ab28eeee4a3e8ada0ed336640946dd5a8e53dbd3805f9dbdcf0dc", + "sha256:4fbcd657cda75574fd1315a4c44bd322bc2e219039fb09f146bbe6f8aef039e9", + "sha256:523ba7fd4d8fe75ad09c1e574a648892b75a97d0cfc8005727681053ac19555b", + "sha256:53b2c1326c2e484d450932d2be739f064b7cb572faabec38386098a28516a529", + "sha256:540d7146c3cdc9bbffab039ea067f494eba24d1abe5bd33eb9f963c01e3305d4", + "sha256:563d4281c4dbdf647d93114420151d33f895afc4c46b7115a67a0aa5347e6624", + "sha256:67a049bcf967a39993858beed873ed3405536019820922d4efacfe35ab3da51a", + "sha256:67ec63ae3c9c1fa2e077fcb42e77035e2121a04f987464bdf9945a28535d30ad", + "sha256:68e22c5d3be451e87d47f956b397a7823bfbde2176341bc902fba30f96831d7e", + "sha256:6ab4b6108e69f63c917cd7ef7217c5727955b1ac90600e44a13ed5312019a014", + "sha256:6bd7f18bd4cf51ea8d7e54825902cf36f9d2f35cc51ef618373988d5398b8dd0", + "sha256:6cd53e861bccc0bdc4620f68fb4a91d5bcfe9f4213cf8e200fa498044d33a6dc", + "sha256:6d346e551fa64b89d57a4ac74b9bc66703413f02f50093e089e861999ec5cccc", + "sha256:6ff8708fabc9f9bc2949f457d39b4088c9656c4c9ac15fbbbbaafce8f6d07833", + "sha256:7626e8384275a7dea6f3d1f749fb5e00299042e9c895fc3dbe24cb154909c242", + "sha256:7e7346b2b33dcd4a2171dd8a9870ae283eec8f6231dcbcf237a0f41e74751a50", + "sha256:81623c67cb71b93b5f7e06c9107f3781738ae86866db830c950223d87af2a235", + "sha256:83f1c76068faf62c32a36dd62dc4db642c2027bbbd960f8f6345b59e9d4dc472", + "sha256:8679bb1dd723ecbea03b1f96c98972815775fd8ec756c440a14f289c436c472e", + "sha256:86fb683cb9a9c0bb7476988b7957393ecdd22777d87d804442c66e62c99197f9", + "sha256:8757c62f7960cd26122f7aaaf86eda1e016fa85734c3777b8054dd334d7dea4d", + "sha256:894be7d17228e7328cc188096c0162697211ec91761f6812fff12790cbe11c66", + "sha256:8a0f240bf43c29be1bd82d77e602a61c798e9de02e5f8bb7bb414cb814f43236", + "sha256:8c3abf7eab5b76ae162c4fbb16d514a947fc57fd995b64e5ea8ef8ba3b888a69", + "sha256:93332c6972e4c91522c4810e907f3aea067424338071161b39cacded022559df", + "sha256:97d6c676dc97d593625d9fc48154f2ffeabb619a1e6fe8d2a5b53f97e3e9bdee", + "sha256:99dd85f0ca1db8d17a01a25c2bbb7784d25a2d39497c6beddbe96bff74194e04", + "sha256:9c7fb691fb07ec7ab99fd173bb0e7e0248d31bf83d484a87b917a342f63812c9", + "sha256:b3bc3cf200aab74f3d758586ac50295214eda496ac6a6636e0c881c5958d9123", + "sha256:bba54f97578943f48f621b4a7afb8eb022370da26a88b88ccc9fee9f3ef7ce45", + "sha256:bd2a13a0f8367e50347cbac87ae230ae1953935443240238f956bf10668bead6", + "sha256:cbc1184349ca6e5112898aa7fc3efa1b1bbae24ab1edc774cfd09cbfd3b091d7", + "sha256:cd82cca9c489e441574804dbda2dd8e114cf3be7935b03de11dade2c9478aea6", + "sha256:ce8ba5ed8b0a7a203922d61cff45ee6001a41a9359f04f00d055a4e988755569", + "sha256:cfee22e072a382b92ee0709dbb8203dabd52d54258051e770d9d2a81b162530b", + "sha256:d977df6f7c4109ed1d96ffb6795f6af77114be606ae4556efbfc9cac725db65d", + "sha256:da72a384a1d7e87490ca71182f3ab469ed21d847adc16b70c34faac5a3b12801", + "sha256:ddf4ad1d651e6c9234945061e1a31fe27a4be0dea21c498b87b186fadf8f5919", + "sha256:eb0ae5dfda83bbce660179d7b41c1c38fd833a54d2e6d9b258c644f3b75ef94d", + "sha256:f4c7d370badc60ac94a554bc571a46d03e39d8aacfba8006b334512e184aed59", + "sha256:f6c378b435a26fda8996579c0e324b108d2ca0d01b4661503a75634e5155559f", + "sha256:f6c9d30888503f2f5f87d6d41f016301352dd98da4a861bd10663c3a2d99d3b5", + "sha256:fab8a7877275060f7b303e1f91c218069a2814a616b6a5ee2d8a3737deb15915", + "sha256:fc32e7d7f98cac3d8d5153ed2cb583158ae3d446a6efb8e28ccb1c54a09f4169" + ], + "index": "pypi", + "version": "==23.1.0" }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", + "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.0" }, "scons": { "hashes": [ @@ -1118,11 +1155,11 @@ }, "setuptools": { "hashes": [ - "sha256:68e45d17c9281ba25dc0104eadd2647172b3472d9e01f911efa57965e8d51a36", - "sha256:a43bdedf853c670e5fed28e5623403bad2f73cf02f9a2774e91def6bda8265a7" + "sha256:d1746e7fd520e83bbe210d02fff1aa1a425ad671c7a9da7d246ec2401a087198", + "sha256:e7d11f3db616cda0751372244c2ba798e8e56a28e096ec4529010b803485f3fe" ], "markers": "python_version >= '3.7'", - "version": "==62.3.2" + "version": "==62.3.3" }, "six": { "hashes": [ @@ -1134,11 +1171,11 @@ }, "smbus2": { "hashes": [ - "sha256:6276eb599b76c4e74372f2582d2282f03b4398f0da16bc996608e4f21557ca9b", - "sha256:8b1e70cda011b6fb3caf8377a1084f73a5aa99392b78529f140b0a3f06468f6c" + "sha256:50f3c78e436b42a9583948be06961a8104cf020ebad5edfaaf2657528bef0818", + "sha256:634541ed794068a822fe7499f1577468b9d4641b68dd9bfb6d0eb7270f4d2a32" ], "index": "pypi", - "version": "==0.4.1" + "version": "==0.4.2" }, "sympy": { "hashes": [ @@ -1150,11 +1187,11 @@ }, "timezonefinder": { "hashes": [ - "sha256:90228f7bcf60388868ac95d3d6a8c9f710c766990909cb89dc39a893da7132ad", - "sha256:ce16831bc6349a82ca25ffc0e4c89df5bc5df29b286e5201701bce852f8e49f2" + "sha256:0471463083b5fef7a656c7e31a7376fa1941bf6a41a0750c6e0ca151e43646e7", + "sha256:2e8e958814f21fa809b16e98bd2f99082b69a0f88b864a4a1f2944d5a7e61827" ], "index": "pypi", - "version": "==6.0.0" + "version": "==6.0.1" }, "tomli": { "hashes": [ @@ -1164,6 +1201,14 @@ "markers": "python_version < '3.11'", "version": "==2.0.1" }, + "tomlkit": { + "hashes": [ + "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1", + "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd" + ], + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==0.11.0" + }, "tqdm": { "hashes": [ "sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d", @@ -1496,62 +1541,62 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==2.0.12" }, "control": { "hashes": [ - "sha256:8c9084bf386eafcf5d74008f780fae6dec68d243d18a380c866ac10a3549f8d3" + "sha256:0891d2d32d6006ac1faa4e238ed8223ca342a4721d202dfeccae24fb02563183" ], "index": "pypi", - "version": "==0.9.1" + "version": "==0.9.2" }, "coverage": { "hashes": [ - "sha256:00c8544510f3c98476bbd58201ac2b150ffbcce46a8c3e4fb89ebf01998f806a", - "sha256:016d7f5cf1c8c84f533a3c1f8f36126fbe00b2ec0ccca47cc5731c3723d327c6", - "sha256:03014a74023abaf5a591eeeaf1ac66a73d54eba178ff4cb1fa0c0a44aae70383", - "sha256:033ebec282793bd9eb988d0271c211e58442c31077976c19c442e24d827d356f", - "sha256:21e6686a95025927775ac501e74f5940cdf6fe052292f3a3f7349b0abae6d00f", - "sha256:26f8f92699756cb7af2b30720de0c5bb8d028e923a95b6d0c891088025a1ac8f", - "sha256:2e76bd16f0e31bc2b07e0fb1379551fcd40daf8cdf7e24f31a29e442878a827c", - "sha256:341e9c2008c481c5c72d0e0dbf64980a4b2238631a7f9780b0fe2e95755fb018", - "sha256:3cfd07c5889ddb96a401449109a8b97a165be9d67077df6802f59708bfb07720", - "sha256:4002f9e8c1f286e986fe96ec58742b93484195defc01d5cc7809b8f7acb5ece3", - "sha256:50ed480b798febce113709846b11f5d5ed1e529c88d8ae92f707806c50297abf", - "sha256:543e172ce4c0de533fa892034cce260467b213c0ea8e39da2f65f9a477425211", - "sha256:5a78cf2c43b13aa6b56003707c5203f28585944c277c1f3f109c7b041b16bd39", - "sha256:5cd698341626f3c77784858427bad0cdd54a713115b423d22ac83a28303d1d95", - "sha256:60c2147921da7f4d2d04f570e1838db32b95c5509d248f3fe6417e91437eaf41", - "sha256:62d382f7d77eeeaff14b30516b17bcbe80f645f5cf02bb755baac376591c653c", - "sha256:69432946f154c6add0e9ede03cc43b96e2ef2733110a77444823c053b1ff5166", - "sha256:727dafd7f67a6e1cad808dc884bd9c5a2f6ef1f8f6d2f22b37b96cb0080d4f49", - "sha256:742fb8b43835078dd7496c3c25a1ec8d15351df49fb0037bffb4754291ef30ce", - "sha256:750e13834b597eeb8ae6e72aa58d1d831b96beec5ad1d04479ae3772373a8088", - "sha256:7b546cf2b1974ddc2cb222a109b37c6ed1778b9be7e6b0c0bc0cf0438d9e45a6", - "sha256:83bd142cdec5e4a5c4ca1d4ff6fa807d28460f9db919f9f6a31babaaa8b88426", - "sha256:8d2e80dd3438e93b19e1223a9850fa65425e77f2607a364b6fd134fcd52dc9df", - "sha256:9229d074e097f21dfe0643d9d0140ee7433814b3f0fc3706b4abffd1e3038632", - "sha256:968ed5407f9460bd5a591cefd1388cc00a8f5099de9e76234655ae48cfdbe2c3", - "sha256:9c82f2cd69c71698152e943f4a5a6b83a3ab1db73b88f6e769fabc86074c3b08", - "sha256:a00441f5ea4504f5abbc047589d09e0dc33eb447dc45a1a527c8b74bfdd32c65", - "sha256:a022394996419142b33a0cf7274cb444c01d2bb123727c4bb0b9acabcb515dea", - "sha256:af5b9ee0fc146e907aa0f5fb858c3b3da9199d78b7bb2c9973d95550bd40f701", - "sha256:b5578efe4038be02d76c344007b13119b2b20acd009a88dde8adec2de4f630b5", - "sha256:b84ab65444dcc68d761e95d4d70f3cfd347ceca5a029f2ffec37d4f124f61311", - "sha256:c53ad261dfc8695062fc8811ac7c162bd6096a05a19f26097f411bdf5747aee7", - "sha256:cc173f1ce9ffb16b299f51c9ce53f66a62f4d975abe5640e976904066f3c835d", - "sha256:d548edacbf16a8276af13063a2b0669d58bbcfca7c55a255f84aac2870786a61", - "sha256:d55fae115ef9f67934e9f1103c9ba826b4c690e4c5bcf94482b8b2398311bf9c", - "sha256:d8099ea680201c2221f8468c372198ceba9338a5fec0e940111962b03b3f716a", - "sha256:e35217031e4b534b09f9b9a5841b9344a30a6357627761d4218818b865d45055", - "sha256:e4f52c272fdc82e7c65ff3f17a7179bc5f710ebc8ce8a5cadac81215e8326740", - "sha256:e637ae0b7b481905358624ef2e81d7fb0b1af55f5ff99f9ba05442a444b11e45", - "sha256:eef5292b60b6de753d6e7f2d128d5841c7915fb1e3321c3a1fe6acfe76c38052", - "sha256:fb45fe08e1abc64eb836d187b20a59172053999823f7f6ef4f18a819c44ba16f" - ], - "index": "pypi", - "version": "==6.4" + "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749", + "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982", + "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3", + "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9", + "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428", + "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e", + "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c", + "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9", + "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264", + "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605", + "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397", + "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d", + "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c", + "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815", + "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068", + "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b", + "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4", + "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4", + "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3", + "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84", + "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83", + "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4", + "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8", + "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb", + "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d", + "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df", + "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6", + "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b", + "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72", + "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13", + "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df", + "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc", + "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6", + "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28", + "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b", + "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4", + "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad", + "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46", + "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3", + "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9", + "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54" + ], + "index": "pypi", + "version": "==6.4.1" }, "cryptography": { "hashes": [ @@ -1665,11 +1710,11 @@ }, "filelock": { "hashes": [ - "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20", - "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6" + "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404", + "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04" ], "markers": "python_version >= '3.7'", - "version": "==3.7.0" + "version": "==3.7.1" }, "fonttools": { "hashes": [ @@ -1688,11 +1733,11 @@ }, "hypothesis": { "hashes": [ - "sha256:2696cdb9005946bf1d2b215cc91d3fc01625e3342eb8743ddd04b667b2f1882b", - "sha256:967009fa561b3a3f8363a73d71923357271c37dc7fa27b30c2d21a1b6092b240" + "sha256:4ad26c5d434171ffc02aba569dd52255573d615554c062bc30734dbe6f318c61", + "sha256:69978811f1d9c19710c7d2bf8233dc43c80efa964251b72efbe8274044e073b4" ], "index": "pypi", - "version": "==6.46.7" + "version": "==6.47.1" }, "identify": { "hashes": [ @@ -1707,7 +1752,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3'", + "markers": "python_version >= '3.5'", "version": "==3.3" }, "imagesize": { @@ -1918,41 +1963,40 @@ }, "mpld3": { "hashes": [ - "sha256:3626d37da7ac11d0fce4dd5ceb37d04ba618bf951129bf6ca3f94bf48da87d6f", - "sha256:b1290f6cca2d9515e32ee21a8b0c18b2ea361cefce58ba0f4df881e9eeb3f64c", - "sha256:c589db8b661aee25c93e198e2e18ed47b9a96951de41d96241345acec5f819ab" + "sha256:1a167dbef836dd7c66d8aa71c06a32d50bffa18725f304d93cb74fdb3545043b", + "sha256:41938e65de4ba41a1b53d92e7c8609e7172e09b33ef5db42bb6f73701106c8b7" ], "index": "pypi", - "version": "==0.5.7" + "version": "==0.5.8" }, "mypy": { "hashes": [ - "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d", - "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8", - "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de", - "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038", - "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed", - "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334", - "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff", - "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2", - "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22", - "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2", - "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2", - "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605", - "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb", - "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519", - "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0", - "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc", - "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b", - "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f", - "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075", - "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef", - "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb", - "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a", - "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b" - ], - "index": "pypi", - "version": "==0.950" + "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5", + "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66", + "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e", + "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56", + "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e", + "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d", + "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813", + "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932", + "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569", + "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b", + "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0", + "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648", + "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6", + "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950", + "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15", + "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723", + "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a", + "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3", + "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6", + "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24", + "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b", + "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d", + "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492" + ], + "index": "pypi", + "version": "==0.961" }, "mypy-extensions": { "hashes": [ @@ -1963,11 +2007,11 @@ }, "myst-parser": { "hashes": [ - "sha256:1635ce3c18965a528d6de980f989ff64d6a1effb482e1f611b1bfb79e38f3d98", - "sha256:4c076d649e066f9f5c7c661bae2658be1ca06e76b002bb97f02a09398707686c" + "sha256:4965e51918837c13bf1c6f6fe2c6bddddf193148360fbdaefe743a4981358f6a", + "sha256:739a4d96773a8e55a2cacd3941ce46a446ee23dcd6b37e06f73f551ad7821d86" ], "index": "pypi", - "version": "==0.17.2" + "version": "==0.18.0" }, "natsort": { "hashes": [ @@ -2014,16 +2058,16 @@ }, "opencv-python-headless": { "hashes": [ - "sha256:3f330468c29882dbbec5af25695c5e575572c6b855cb0f9fe53e14116fd46bfc", - "sha256:4bdf982574bf2fefc5f82c86df7cb42e56ad627874c7c0f4d94ecf4ae8885304", - "sha256:567a54c1919bcf5b3d20a9830e3c511e57134de8def286ce137c3544a892f98c", - "sha256:62e31878641a8f96e773118d1eea9f34bdda87c9990a0faab04ebaafb5ae015c", - "sha256:a60e9ff48854ec37be391e19dd634883cc26c2f0f814e5325b3deca33420912c", - "sha256:c3c2dda44d601757a508b07d628537c49f31223ad4edd0f747a70d4c852a7b98", - "sha256:ca4f013fa958f60fb2327fe87e6127c1ac0ab536890b1d4b00847f417b7af1ba" + "sha256:21e70f8b0c04098cdf466d27184fe6c3820aaef944a22548db95099959c95889", + "sha256:2c032c373e447c3fc2a670bca20e2918a1205a6e72854df60425fd3f82c78c32", + "sha256:3bacd806cce1f1988e58f3d6f761538e0215d6621d316de94c009dc0acbd6ad3", + "sha256:d5291d7e10aa2c19cab6fd86f0d61af8617290ecd2d7ffcb051e446868d04cc5", + "sha256:e6c069bc963d7e8fcec21b3e33e594d35948badd63eccb2e80f88b0a12102c03", + "sha256:eec6281054346103d6af93f173b7c6a206beb2663d3adc04aa3ddc66e85093df", + "sha256:ffbf26fcd697af996408440a93bc69c49c05a36845771f984156dfbeaa95d497" ], "index": "pypi", - "version": "==4.5.5.64" + "version": "==4.6.0.66" }, "packaging": { "hashes": [ @@ -2329,11 +2373,11 @@ }, "requests": { "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", + "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" ], "index": "pypi", - "version": "==2.27.1" + "version": "==2.28.0" }, "reverse-geocoder": { "hashes": [ From 66bc246210b4471e0a5fa3ab792c4d9fe0316406 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 10 Jun 2022 15:23:42 -0700 Subject: [PATCH 036/436] count events: add simple camera debugging --- selfdrive/debug/count_events.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/selfdrive/debug/count_events.py b/selfdrive/debug/count_events.py index 8b32ce9d21f79c..c3870e0f9ef662 100755 --- a/selfdrive/debug/count_events.py +++ b/selfdrive/debug/count_events.py @@ -4,6 +4,7 @@ from pprint import pprint from tqdm import tqdm +from cereal.services import service_list from tools.lib.route import Route from tools.lib.logreader import LogReader @@ -13,6 +14,9 @@ cnt_valid: Counter = Counter() cnt_events: Counter = Counter() + cams = [s for s in service_list if s.endswith('CameraState')] + cnt_cameras = dict.fromkeys(cams, 0) + for q in tqdm(r.qlog_paths()): if q is None: continue @@ -21,12 +25,21 @@ if msg.which() == 'carEvents': for e in msg.carEvents: cnt_events[e.name] += 1 + elif msg.which() in cams: + cnt_cameras[msg.which()] += 1 + if not msg.valid: cnt_valid[msg.which()] += 1 + print("Events") pprint(cnt_events) - print("\n\n") + print("\n") print("Not valid") pprint(cnt_valid) + + print("\n") + print("Cameras") + for k, v in cnt_cameras.items(): + print(" ", k.ljust(20), v) From c646eeee0ac54925db5afc51b95c5d869d6dba68 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 10 Jun 2022 16:16:46 -0700 Subject: [PATCH 037/436] Revert fullframe DM model (#24812) * Revert "fullframe DM: flip RHD yaw to use matching thresholds" This reverts commit 2ac693100359d9df34d5b2b450a0058f2dd6b7c4. * Revert "fullframe DM model (#24762)" This reverts commit d6c07a6b158595c1ab10256dc6eee4fbdf902379. * revert cereal --- cereal | 2 +- common/modeldata.h | 6 + selfdrive/camerad/cameras/camera_common.cc | 2 +- selfdrive/camerad/cameras/camera_qcom2.cc | 2 +- selfdrive/hardware/tici/test_power_draw.py | 2 +- selfdrive/modeld/dmonitoringmodeld.cc | 6 +- selfdrive/modeld/models/dmonitoring.cc | 234 ++++++++++++------ selfdrive/modeld/models/dmonitoring.h | 33 +-- .../modeld/models/dmonitoring_model.current | 4 +- .../modeld/models/dmonitoring_model.onnx | 4 +- .../modeld/models/dmonitoring_model_q.dlc | 4 +- selfdrive/modeld/runners/onnx_runner.py | 21 +- selfdrive/modeld/runners/onnxmodel.cc | 6 +- selfdrive/modeld/runners/onnxmodel.h | 3 +- selfdrive/modeld/runners/snpemodel.cc | 12 +- selfdrive/modeld/runners/snpemodel.h | 3 +- selfdrive/monitoring/dmonitoringd.py | 7 +- selfdrive/monitoring/driver_monitor.py | 90 ++++--- selfdrive/monitoring/test_monitoring.py | 24 +- selfdrive/test/process_replay/model_replay.py | 8 +- .../process_replay/model_replay_ref_commit | 2 +- .../test/process_replay/process_replay.py | 2 +- selfdrive/test/test_onroad.py | 6 +- selfdrive/ui/qt/offroad/driverview.cc | 43 ++-- selfdrive/ui/qt/widgets/cameraview.cc | 9 +- 25 files changed, 308 insertions(+), 227 deletions(-) diff --git a/cereal b/cereal index 96cbed052ab1d6..e9d3597d23311a 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 96cbed052ab1d69ea15da94dc2b882d59a786347 +Subproject commit e9d3597d23311a3e33a2def74ceec839e5ff4bf5 diff --git a/common/modeldata.h b/common/modeldata.h index 410a69ea654cb5..06d9398031f1c1 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -24,6 +24,12 @@ constexpr auto T_IDXS_FLOAT = build_idxs(10.0); constexpr auto X_IDXS = build_idxs(192.0); constexpr auto X_IDXS_FLOAT = build_idxs(192.0); +namespace tici_dm_crop { + const int x_offset = -72; + const int y_offset = -144; + const int width = 954; +}; + const mat3 fcam_intrinsic_matrix = (mat3){{2648.0, 0.0, 1928.0 / 2, 0.0, 2648.0, 1208.0 / 2, 0.0, 0.0, 1.0}}; diff --git a/selfdrive/camerad/cameras/camera_common.cc b/selfdrive/camerad/cameras/camera_common.cc index 21e225d5380ff0..049324f0856a8b 100644 --- a/selfdrive/camerad/cameras/camera_common.cc +++ b/selfdrive/camerad/cameras/camera_common.cc @@ -239,7 +239,7 @@ static kj::Array yuv420_to_jpeg(const CameraBuf *b, int thumbnail_w int in_stride = b->cur_yuv_buf->stride; // make the buffer big enough. jpeg_write_raw_data requires 16-pixels aligned height to be used. - std::unique_ptr buf(new uint8_t[(thumbnail_width * ((thumbnail_height + 15) & ~15) * 3) / 2]); + std::unique_ptr buf(new uint8_t[(thumbnail_width * ((thumbnail_height + 15) & ~15) * 3) / 2]); uint8_t *y_plane = buf.get(); uint8_t *u_plane = y_plane + thumbnail_width * thumbnail_height; uint8_t *v_plane = u_plane + (thumbnail_width * thumbnail_height) / 4; diff --git a/selfdrive/camerad/cameras/camera_qcom2.cc b/selfdrive/camerad/cameras/camera_qcom2.cc index b949dfb80fa049..d43beb0921e0f0 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.cc +++ b/selfdrive/camerad/cameras/camera_qcom2.cc @@ -837,7 +837,7 @@ void cameras_init(VisionIpcServer *v, MultiCameraState *s, cl_device_id device_i s->road_cam.camera_init(s, v, CAMERA_ID_AR0231, 1, 20, device_id, ctx, VISION_STREAM_RGB_ROAD, VISION_STREAM_ROAD, !env_disable_road); s->wide_road_cam.camera_init(s, v, CAMERA_ID_AR0231, 0, 20, device_id, ctx, VISION_STREAM_RGB_WIDE_ROAD, VISION_STREAM_WIDE_ROAD, !env_disable_wide_road); - s->sm = new SubMaster({"driverStateV2"}); + s->sm = new SubMaster({"driverState"}); s->pm = new PubMaster({"roadCameraState", "driverCameraState", "wideRoadCameraState", "thumbnail"}); } diff --git a/selfdrive/hardware/tici/test_power_draw.py b/selfdrive/hardware/tici/test_power_draw.py index 5eb84514e5ea74..ab2d691a09217d 100755 --- a/selfdrive/hardware/tici/test_power_draw.py +++ b/selfdrive/hardware/tici/test_power_draw.py @@ -21,7 +21,7 @@ class Proc: PROCS = [ Proc('camerad', 2.15), Proc('modeld', 1.0), - Proc('dmonitoringmodeld', 0.35), + Proc('dmonitoringmodeld', 0.25), Proc('encoderd', 0.23), ] diff --git a/selfdrive/modeld/dmonitoringmodeld.cc b/selfdrive/modeld/dmonitoringmodeld.cc index cde13a9beeb6ba..68c49572e66696 100644 --- a/selfdrive/modeld/dmonitoringmodeld.cc +++ b/selfdrive/modeld/dmonitoringmodeld.cc @@ -12,7 +12,7 @@ ExitHandler do_exit; void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { - PubMaster pm({"driverStateV2"}); + PubMaster pm({"driverState"}); SubMaster sm({"liveCalibration"}); float calib[CALIB_LEN] = {0}; double last = 0; @@ -31,11 +31,11 @@ void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { } double t1 = millis_since_boot(); - DMonitoringModelResult model_res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); + DMonitoringResult res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); double t2 = millis_since_boot(); // send dm packet - dmonitoring_publish(pm, extra.frame_id, model_res, (t2 - t1) / 1000.0, model.output); + dmonitoring_publish(pm, extra.frame_id, res, (t2 - t1) / 1000.0, model.output); //printf("dmonitoring process: %.2fms, from last %.2fms\n", t2 - t1, t1 - last); last = t1; diff --git a/selfdrive/modeld/models/dmonitoring.cc b/selfdrive/modeld/models/dmonitoring.cc index 20294c3f3ced62..e134ad3a5af5c4 100644 --- a/selfdrive/modeld/models/dmonitoring.cc +++ b/selfdrive/modeld/models/dmonitoring.cc @@ -10,8 +10,8 @@ #include "selfdrive/modeld/models/dmonitoring.h" -constexpr int MODEL_WIDTH = 1440; -constexpr int MODEL_HEIGHT = 960; +constexpr int MODEL_WIDTH = 320; +constexpr int MODEL_HEIGHT = 640; template static inline T *get_buffer(std::vector &buf, const size_t size) { @@ -19,115 +19,199 @@ static inline T *get_buffer(std::vector &buf, const size_t size) { return buf.data(); } +static inline void init_yuv_buf(std::vector &buf, const int width, int height) { + uint8_t *y = get_buffer(buf, width * height * 3 / 2); + uint8_t *u = y + width * height; + uint8_t *v = u + (width / 2) * (height / 2); + + // needed on comma two to make the padded border black + // equivalent to RGB(0,0,0) in YUV space + memset(y, 16, width * height); + memset(u, 128, (width / 2) * (height / 2)); + memset(v, 128, (width / 2) * (height / 2)); +} + void dmonitoring_init(DMonitoringModelState* s) { s->is_rhd = Params().getBool("IsRHD"); + for (int x = 0; x < std::size(s->tensor); ++x) { + s->tensor[x] = (x - 128.f) * 0.0078125f; + } + init_yuv_buf(s->resized_buf, MODEL_WIDTH, MODEL_HEIGHT); #ifdef USE_ONNX_MODEL - s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); + s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); #else - s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); + s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); #endif s->m->addCalib(s->calib, CALIB_LEN); } -void parse_driver_data(DriverStateResult &ds_res, const DMonitoringModelState* s, int out_idx_offset) { - for (int i = 0; i < 3; ++i) { - ds_res.face_orientation[i] = s->output[out_idx_offset+i] * REG_SCALE; - ds_res.face_orientation_std[i] = exp(s->output[out_idx_offset+6+i]); - } - for (int i = 0; i < 2; ++i) { - ds_res.face_position[i] = s->output[out_idx_offset+3+i] * REG_SCALE; - ds_res.face_position_std[i] = exp(s->output[out_idx_offset+9+i]); - } - for (int i = 0; i < 4; ++i) { - ds_res.ready_prob[i] = sigmoid(s->output[out_idx_offset+35+i]); - } - for (int i = 0; i < 2; ++i) { - ds_res.not_ready_prob[i] = sigmoid(s->output[out_idx_offset+39+i]); - } - ds_res.face_prob = sigmoid(s->output[out_idx_offset+12]); - ds_res.left_eye_prob = sigmoid(s->output[out_idx_offset+21]); - ds_res.right_eye_prob = sigmoid(s->output[out_idx_offset+30]); - ds_res.left_blink_prob = sigmoid(s->output[out_idx_offset+31]); - ds_res.right_blink_prob = sigmoid(s->output[out_idx_offset+32]); - ds_res.sunglasses_prob = sigmoid(s->output[out_idx_offset+33]); - ds_res.occluded_prob = sigmoid(s->output[out_idx_offset+34]); +static inline auto get_yuv_buf(std::vector &buf, const int width, int height) { + uint8_t *y = get_buffer(buf, width * height * 3 / 2); + uint8_t *u = y + width * height; + uint8_t *v = u + (width /2) * (height / 2); + return std::make_tuple(y, u, v); } -void fill_driver_data(cereal::DriverStateV2::DriverData::Builder ddata, const DriverStateResult &ds_res) { - ddata.setFaceOrientation(ds_res.face_orientation); - ddata.setFaceOrientationStd(ds_res.face_orientation_std); - ddata.setFacePosition(ds_res.face_position); - ddata.setFacePositionStd(ds_res.face_position_std); - ddata.setFaceProb(ds_res.face_prob); - ddata.setLeftEyeProb(ds_res.left_eye_prob); - ddata.setRightEyeProb(ds_res.right_eye_prob); - ddata.setLeftBlinkProb(ds_res.left_blink_prob); - ddata.setRightBlinkProb(ds_res.right_blink_prob); - ddata.setSunglassesProb(ds_res.sunglasses_prob); - ddata.setOccludedProb(ds_res.occluded_prob); - ddata.setReadyProb(ds_res.ready_prob); - ddata.setNotReadyProb(ds_res.not_ready_prob); +struct Rect {int x, y, w, h;}; +void crop_nv12_to_yuv(uint8_t *raw, int stride, int uv_offset, uint8_t *y, uint8_t *u, uint8_t *v, const Rect &rect) { + uint8_t *raw_y = raw; + uint8_t *raw_uv = raw_y + uv_offset; + for (int r = 0; r < rect.h / 2; r++) { + memcpy(y + 2 * r * rect.w, raw_y + (2 * r + rect.y) * stride + rect.x, rect.w); + memcpy(y + (2 * r + 1) * rect.w, raw_y + (2 * r + rect.y + 1) * stride + rect.x, rect.w); + for (int h = 0; h < rect.w / 2; h++) { + u[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2]; + v[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2 + 1]; + } + } } -DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { - int v_off = height - MODEL_HEIGHT; - int h_off = (width - MODEL_WIDTH) / 2; - int yuv_buf_len = MODEL_WIDTH * MODEL_HEIGHT; - - uint8_t *raw_buf = (uint8_t *) stream_buf; - // vertical crop free - uint8_t *raw_y_start = raw_buf + stride * v_off; +DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { + const int cropped_height = tici_dm_crop::width / 1.33; + Rect crop_rect = {width / 2 - tici_dm_crop::width / 2 + tici_dm_crop::x_offset, + height / 2 - cropped_height / 2 + tici_dm_crop::y_offset, + cropped_height / 2, + cropped_height}; + if (!s->is_rhd) { + crop_rect.x += tici_dm_crop::width - crop_rect.w; + } - uint8_t *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); + int resized_width = MODEL_WIDTH; + int resized_height = MODEL_HEIGHT; + + auto [cropped_y, cropped_u, cropped_v] = get_yuv_buf(s->cropped_buf, crop_rect.w, crop_rect.h); + if (!s->is_rhd) { + crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, cropped_y, cropped_u, cropped_v, crop_rect); + } else { + auto [mirror_y, mirror_u, mirror_v] = get_yuv_buf(s->premirror_cropped_buf, crop_rect.w, crop_rect.h); + crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, mirror_y, mirror_u, mirror_v, crop_rect); + libyuv::I420Mirror(mirror_y, crop_rect.w, + mirror_u, crop_rect.w / 2, + mirror_v, crop_rect.w / 2, + cropped_y, crop_rect.w, + cropped_u, crop_rect.w / 2, + cropped_v, crop_rect.w / 2, + crop_rect.w, crop_rect.h); + } - // here makes a uint8 copy - for (int r = 0; r < MODEL_HEIGHT; ++r) { - memcpy(net_input_buf + r * MODEL_WIDTH, raw_y_start + r * stride + h_off, MODEL_WIDTH); + auto [resized_buf, resized_u, resized_v] = get_yuv_buf(s->resized_buf, resized_width, resized_height); + uint8_t *resized_y = resized_buf; + libyuv::FilterMode mode = libyuv::FilterModeEnum::kFilterBilinear; + libyuv::I420Scale(cropped_y, crop_rect.w, + cropped_u, crop_rect.w / 2, + cropped_v, crop_rect.w / 2, + crop_rect.w, crop_rect.h, + resized_y, resized_width, + resized_u, resized_width / 2, + resized_v, resized_width / 2, + resized_width, resized_height, + mode); + + + int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v + float *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); + // one shot conversion, O(n) anyway + // yuvframe2tensor, normalize + for (int r = 0; r < MODEL_HEIGHT/2; r++) { + for (int c = 0; c < MODEL_WIDTH/2; c++) { + // Y_ul + net_input_buf[(r*MODEL_WIDTH/2) + c + (0*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c]]; + // Y_dl + net_input_buf[(r*MODEL_WIDTH/2) + c + (1*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c]]; + // Y_ur + net_input_buf[(r*MODEL_WIDTH/2) + c + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c+1]]; + // Y_dr + net_input_buf[(r*MODEL_WIDTH/2) + c + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c+1]]; + // U + net_input_buf[(r*MODEL_WIDTH/2) + c + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_u[r*resized_width/2 + c]]; + // V + net_input_buf[(r*MODEL_WIDTH/2) + c + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_v[r*resized_width/2 + c]]; + } } - // printf("preprocess completed. %d \n", yuv_buf_len); - // FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); - // fwrite(net_input_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); - // fclose(dump_yuv_file); + //printf("preprocess completed. %d \n", yuv_buf_len); + //FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); + //fwrite(resized_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); + //fclose(dump_yuv_file); + + // *** testing *** + // idat = np.frombuffer(open("/tmp/inputdump.yuv", "rb").read(), np.float32).reshape(6, 160, 320) + // imshow(cv2.cvtColor(tensor_to_frames(idat[None]/0.0078125+128)[0], cv2.COLOR_YUV2RGB_I420)) + + //FILE *dump_yuv_file2 = fopen("/tmp/inputdump.yuv", "wb"); + //fwrite(net_input_buf, MODEL_HEIGHT*MODEL_WIDTH*3/2, sizeof(float), dump_yuv_file2); + //fclose(dump_yuv_file2); double t1 = millis_since_boot(); - s->m->addImage((float*)net_input_buf, yuv_buf_len / 4); + s->m->addImage(net_input_buf, yuv_buf_len); for (int i = 0; i < CALIB_LEN; i++) { s->calib[i] = calib[i]; } s->m->execute(); double t2 = millis_since_boot(); - DMonitoringModelResult model_res = {0}; - parse_driver_data(model_res.driver_state_lhd, s, 0); - parse_driver_data(model_res.driver_state_rhd, s, 41); - model_res.poor_vision_prob = sigmoid(s->output[82]); - model_res.wheel_on_right_prob = sigmoid(s->output[83]); - model_res.dsp_execution_time = (t2 - t1) / 1000.; - - return model_res; + DMonitoringResult ret = {0}; + for (int i = 0; i < 3; ++i) { + ret.face_orientation[i] = s->output[i] * REG_SCALE; + ret.face_orientation_meta[i] = exp(s->output[6 + i]); + } + for (int i = 0; i < 2; ++i) { + ret.face_position[i] = s->output[3 + i] * REG_SCALE; + ret.face_position_meta[i] = exp(s->output[9 + i]); + } + for (int i = 0; i < 4; ++i) { + ret.ready_prob[i] = sigmoid(s->output[39 + i]); + } + for (int i = 0; i < 2; ++i) { + ret.not_ready_prob[i] = sigmoid(s->output[43 + i]); + } + ret.face_prob = sigmoid(s->output[12]); + ret.left_eye_prob = sigmoid(s->output[21]); + ret.right_eye_prob = sigmoid(s->output[30]); + ret.left_blink_prob = sigmoid(s->output[31]); + ret.right_blink_prob = sigmoid(s->output[32]); + ret.sg_prob = sigmoid(s->output[33]); + ret.poor_vision = sigmoid(s->output[34]); + ret.partial_face = sigmoid(s->output[35]); + ret.distracted_pose = sigmoid(s->output[36]); + ret.distracted_eyes = sigmoid(s->output[37]); + ret.occluded_prob = sigmoid(s->output[38]); + ret.dsp_execution_time = (t2 - t1) / 1000.; + return ret; } -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred) { +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred) { // make msg MessageBuilder msg; - auto framed = msg.initEvent().initDriverStateV2(); + auto framed = msg.initEvent().initDriverState(); framed.setFrameId(frame_id); framed.setModelExecutionTime(execution_time); - framed.setDspExecutionTime(model_res.dsp_execution_time); - - framed.setPoorVisionProb(model_res.poor_vision_prob); - framed.setWheelOnRightProb(model_res.wheel_on_right_prob); - fill_driver_data(framed.initLeftDriverData(), model_res.driver_state_lhd); - fill_driver_data(framed.initRightDriverData(), model_res.driver_state_rhd); - + framed.setDspExecutionTime(res.dsp_execution_time); + + framed.setFaceOrientation(res.face_orientation); + framed.setFaceOrientationStd(res.face_orientation_meta); + framed.setFacePosition(res.face_position); + framed.setFacePositionStd(res.face_position_meta); + framed.setFaceProb(res.face_prob); + framed.setLeftEyeProb(res.left_eye_prob); + framed.setRightEyeProb(res.right_eye_prob); + framed.setLeftBlinkProb(res.left_blink_prob); + framed.setRightBlinkProb(res.right_blink_prob); + framed.setSunglassesProb(res.sg_prob); + framed.setPoorVision(res.poor_vision); + framed.setPartialFace(res.partial_face); + framed.setDistractedPose(res.distracted_pose); + framed.setDistractedEyes(res.distracted_eyes); + framed.setOccludedProb(res.occluded_prob); + framed.setReadyProb(res.ready_prob); + framed.setNotReadyProb(res.not_ready_prob); if (send_raw_pred) { framed.setRawPredictions(raw_pred.asBytes()); } - pm.send("driverStateV2", msg); + pm.send("driverState", msg); } void dmonitoring_free(DMonitoringModelState* s) { diff --git a/selfdrive/modeld/models/dmonitoring.h b/selfdrive/modeld/models/dmonitoring.h index 874722cd93fb88..a1be91e3bb20a8 100644 --- a/selfdrive/modeld/models/dmonitoring.h +++ b/selfdrive/modeld/models/dmonitoring.h @@ -9,43 +9,44 @@ #define CALIB_LEN 3 -#define OUTPUT_SIZE 84 +#define OUTPUT_SIZE 45 #define REG_SCALE 0.25f -typedef struct DriverStateResult { +typedef struct DMonitoringResult { float face_orientation[3]; - float face_orientation_std[3]; + float face_orientation_meta[3]; float face_position[2]; - float face_position_std[2]; + float face_position_meta[2]; float face_prob; float left_eye_prob; float right_eye_prob; float left_blink_prob; float right_blink_prob; - float sunglasses_prob; + float sg_prob; + float poor_vision; + float partial_face; + float distracted_pose; + float distracted_eyes; float occluded_prob; float ready_prob[4]; float not_ready_prob[2]; -} DriverStateResult; - -typedef struct DMonitoringModelResult { - DriverStateResult driver_state_lhd; - DriverStateResult driver_state_rhd; - float poor_vision_prob; - float wheel_on_right_prob; float dsp_execution_time; -} DMonitoringModelResult; +} DMonitoringResult; typedef struct DMonitoringModelState { RunModel *m; bool is_rhd; float output[OUTPUT_SIZE]; - std::vector net_input_buf; + std::vector resized_buf; + std::vector cropped_buf; + std::vector premirror_cropped_buf; + std::vector net_input_buf; float calib[CALIB_LEN]; + float tensor[UINT8_MAX + 1]; } DMonitoringModelState; void dmonitoring_init(DMonitoringModelState* s); -DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred); +DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred); void dmonitoring_free(DMonitoringModelState* s); diff --git a/selfdrive/modeld/models/dmonitoring_model.current b/selfdrive/modeld/models/dmonitoring_model.current index ba2865429e6452..74bcfe17a4ebe4 100644 --- a/selfdrive/modeld/models/dmonitoring_model.current +++ b/selfdrive/modeld/models/dmonitoring_model.current @@ -1,2 +1,2 @@ -5b02cff5-2b29-431d-b186-372e9c6fd0c7 -bf33cc569076984626ac7e027f927aa593261fa7 \ No newline at end of file +a8236e30-5bee-4689-8ea0-fc102e2770e5 +d508c79bae1c1c451f3af3e2bc231ce33678cb43 \ No newline at end of file diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 1fca1317d5ca44..51b0d1ed760980 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aca1dd411b5f488bea605dc360656e631fc4301968a589ea072e2220c8092600 -size 9157561 +oid sha256:00731ebd06fcff7e5837607b91bc56cad3bed5d7ee89052c911c981e8f665308 +size 3679940 diff --git a/selfdrive/modeld/models/dmonitoring_model_q.dlc b/selfdrive/modeld/models/dmonitoring_model_q.dlc index c1cba151764929..2e54f7ee4b098e 100644 --- a/selfdrive/modeld/models/dmonitoring_model_q.dlc +++ b/selfdrive/modeld/models/dmonitoring_model_q.dlc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d146b1d1fd9d40d57d058e51d285f83676866e26d9e5aff9fa27623ce343b58a -size 2636941 +oid sha256:667df5e925570a0f6a33dfb890e186a1f13f101885b46db47ec45305737dffb6 +size 1145921 diff --git a/selfdrive/modeld/runners/onnx_runner.py b/selfdrive/modeld/runners/onnx_runner.py index ac7cc68814df22..e282a66b66a8d0 100755 --- a/selfdrive/modeld/runners/onnx_runner.py +++ b/selfdrive/modeld/runners/onnx_runner.py @@ -9,24 +9,20 @@ import onnxruntime as ort # pylint: disable=import-error -def read(sz, tf8=False): +def read(sz): dd = [] gt = 0 - szof = 1 if tf8 else 4 - while gt < sz * szof: - st = os.read(0, sz * szof - gt) + while gt < sz * 4: + st = os.read(0, sz * 4 - gt) assert(len(st) > 0) dd.append(st) gt += len(st) - r = np.frombuffer(b''.join(dd), dtype=np.uint8 if tf8 else np.float32).astype(np.float32) - if tf8: - r = r / 255. - return r + return np.frombuffer(b''.join(dd), dtype=np.float32) def write(d): os.write(1, d.tobytes()) -def run_loop(m, tf8_input=False): +def run_loop(m): ishapes = [[1]+ii.shape[1:] for ii in m.get_inputs()] keys = [x.name for x in m.get_inputs()] @@ -37,10 +33,10 @@ def run_loop(m, tf8_input=False): print("ready to run onnx model", keys, ishapes, file=sys.stderr) while 1: inputs = [] - for k, shp in zip(keys, ishapes): + for shp in ishapes: ts = np.product(shp) #print("reshaping %s with offset %d" % (str(shp), offset), file=sys.stderr) - inputs.append(read(ts, (k=='input_img' and tf8_input)).reshape(shp)) + inputs.append(read(ts).reshape(shp)) ret = m.run(None, dict(zip(keys, inputs))) #print(ret, file=sys.stderr) for r in ret: @@ -48,7 +44,6 @@ def run_loop(m, tf8_input=False): if __name__ == "__main__": - print(sys.argv, file=sys.stderr) print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) options = ort.SessionOptions() options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL @@ -68,6 +63,6 @@ def run_loop(m, tf8_input=False): print("Onnx selected provider: ", [provider], file=sys.stderr) ort_session = ort.InferenceSession(sys.argv[1], options, providers=[provider]) print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - run_loop(ort_session, tf8_input=("--use_tf8" in sys.argv)) + run_loop(ort_session) except KeyboardInterrupt: pass diff --git a/selfdrive/modeld/runners/onnxmodel.cc b/selfdrive/modeld/runners/onnxmodel.cc index 1f9f551abc5351..9b4d6fd0152b95 100644 --- a/selfdrive/modeld/runners/onnxmodel.cc +++ b/selfdrive/modeld/runners/onnxmodel.cc @@ -14,13 +14,12 @@ #include "common/swaglog.h" #include "common/util.h" -ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra, bool _use_tf8) { +ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra) { LOGD("loading model %s", path); output = _output; output_size = _output_size; use_extra = _use_extra; - use_tf8 = _use_tf8; int err = pipe(pipein); assert(err == 0); @@ -29,12 +28,11 @@ ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int std::string exe_dir = util::dir_name(util::readlink("/proc/self/exe")); std::string onnx_runner = exe_dir + "/runners/onnx_runner.py"; - std::string tf8_arg = use_tf8 ? "--use_tf8" : ""; proc_pid = fork(); if (proc_pid == 0) { LOGD("spawning onnx process %s", onnx_runner.c_str()); - char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, (char*)tf8_arg.c_str(), nullptr}; + char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, nullptr}; dup2(pipein[0], 0); dup2(pipeout[1], 1); close(pipein[0]); diff --git a/selfdrive/modeld/runners/onnxmodel.h b/selfdrive/modeld/runners/onnxmodel.h index 4ac599e2afbbf0..567d81d29ef8f7 100644 --- a/selfdrive/modeld/runners/onnxmodel.h +++ b/selfdrive/modeld/runners/onnxmodel.h @@ -6,7 +6,7 @@ class ONNXModel : public RunModel { public: - ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false, bool _use_tf8 = false); + ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false); ~ONNXModel(); void addRecurrent(float *state, int state_size); void addDesire(float *state, int state_size); @@ -31,7 +31,6 @@ class ONNXModel : public RunModel { int calib_size; float *image_input_buf = NULL; int image_buf_size; - bool use_tf8; float *extra_input_buf = NULL; int extra_buf_size; bool use_extra; diff --git a/selfdrive/modeld/runners/snpemodel.cc b/selfdrive/modeld/runners/snpemodel.cc index 4d6917e894c539..1861494d591e3b 100644 --- a/selfdrive/modeld/runners/snpemodel.cc +++ b/selfdrive/modeld/runners/snpemodel.cc @@ -14,11 +14,10 @@ void PrintErrorStringAndExit() { std::exit(EXIT_FAILURE); } -SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra, bool luse_tf8) { +SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra) { output = loutput; output_size = loutput_size; use_extra = luse_extra; - use_tf8 = luse_tf8; #ifdef QCOM2 if (runtime==USE_GPU_RUNTIME) { Runtime = zdl::DlSystem::Runtime_t::GPU; @@ -71,16 +70,14 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int printf("model: %s -> %s\n", input_tensor_name, output_tensor_name); zdl::DlSystem::UserBufferEncodingFloat userBufferEncodingFloat; - zdl::DlSystem::UserBufferEncodingTf8 userBufferEncodingTf8(0, 1./255); // network takes 0-1 zdl::DlSystem::IUserBufferFactory& ubFactory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); - size_t size_of_input = use_tf8 ? sizeof(uint8_t) : sizeof(float); // create input buffer { const auto &inputDims_opt = snpe->getInputDimensions(input_tensor_name); const zdl::DlSystem::TensorShape& bufferShape = *inputDims_opt; std::vector strides(bufferShape.rank()); - strides[strides.size() - 1] = size_of_input; + strides[strides.size() - 1] = sizeof(float); size_t product = 1; for (size_t i = 0; i < bufferShape.rank(); i++) product *= bufferShape[i]; size_t stride = strides[strides.size() - 1]; @@ -89,10 +86,7 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int strides[i-1] = stride; } printf("input product is %lu\n", product); - inputBuffer = ubFactory.createUserBuffer(NULL, - product*size_of_input, - strides, - use_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingTf8 : (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingFloat); + inputBuffer = ubFactory.createUserBuffer(NULL, product*sizeof(float), strides, &userBufferEncodingFloat); inputMap.add(input_tensor_name, inputBuffer.get()); } diff --git a/selfdrive/modeld/runners/snpemodel.h b/selfdrive/modeld/runners/snpemodel.h index ed9d58d1e11335..ba51fdced079f6 100644 --- a/selfdrive/modeld/runners/snpemodel.h +++ b/selfdrive/modeld/runners/snpemodel.h @@ -23,7 +23,7 @@ class SNPEModel : public RunModel { public: - SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false, bool use_tf8 = false); + SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false); void addRecurrent(float *state, int state_size); void addTrafficConvention(float *state, int state_size); void addCalib(float *state, int state_size); @@ -52,7 +52,6 @@ class SNPEModel : public RunModel { std::unique_ptr inputBuffer; float *input; size_t input_size; - bool use_tf8; // snpe output stuff zdl::DlSystem::UserBufferMap outputMap; diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index c31e9da43b9f20..9a3196030ba643 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -18,7 +18,7 @@ def dmonitoringd_thread(sm=None, pm=None): pm = messaging.PubMaster(['driverMonitoringState']) if sm is None: - sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverStateV2']) + sm = messaging.SubMaster(['driverState', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverState']) driver_status = DriverStatus(rhd=Params().get_bool("IsRHD")) @@ -34,7 +34,7 @@ def dmonitoringd_thread(sm=None, pm=None): while True: sm.update() - if not sm.updated['driverStateV2']: + if not sm.updated['driverState']: continue # Get interaction @@ -51,7 +51,7 @@ def dmonitoringd_thread(sm=None, pm=None): # Get data from dmonitoringmodeld events = Events() - driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) + driver_status.update_states(sm['driverState'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) # Block engaging after max number of distrations if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \ @@ -79,7 +79,6 @@ def dmonitoringd_thread(sm=None, pm=None): "isLowStd": driver_status.pose.low_std, "hiStdCount": driver_status.hi_stds, "isActiveMode": driver_status.active_monitoring_mode, - "isRHD": driver_status.wheel_on_right, } pm.send('driverMonitoringState', dat) diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index e7ed175846ad0a..662e0d76ce7167 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -5,7 +5,6 @@ from common.realtime import DT_DMON from common.filter_simple import FirstOrderFilter from common.stat_live import RunningStatFilter -from common.transformations.camera import tici_d_frame_size EventName = car.CarEvent.EventName @@ -27,29 +26,32 @@ def __init__(self): self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6. self._FACE_THRESHOLD = 0.5 + self._PARTIAL_FACE_THRESHOLD = 0.8 self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.925 - self._BLINK_THRESHOLD = 0.861 + self._BLINK_THRESHOLD = 0.8 + self._BLINK_THRESHOLD_SLACK = 0.9 + self._BLINK_THRESHOLD_STRICT = self._BLINK_THRESHOLD self._EE_THRESH11 = 0.75 self._EE_THRESH12 = 3.25 self._EE_THRESH21 = 0.01 self._EE_THRESH22 = 0.35 - self._POSE_PITCH_THRESHOLD = 0.3133 - self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 + self._POSE_PITCH_THRESHOLD = 0.3237 + self._POSE_PITCH_THRESHOLD_SLACK = 0.3657 self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD - self._POSE_YAW_THRESHOLD = 0.4020 - self._POSE_YAW_THRESHOLD_SLACK = 0.5042 + self._POSE_YAW_THRESHOLD = 0.3109 + self._POSE_YAW_THRESHOLD_SLACK = 0.4294 self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD - self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned - self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned + self._PITCH_NATURAL_OFFSET = 0.057 # initial value before offset is learned + self._YAW_NATURAL_OFFSET = 0.11 # initial value before offset is learned self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 self._YAW_MIN_OFFSET = -0.0246 - self._POSESTD_THRESHOLD = 0.3 + self._POSESTD_THRESHOLD = 0.315 self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz @@ -57,9 +59,6 @@ def __init__(self): self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative self._POSE_OFFSET_MAX_COUNT = int(360 / self._DT_DMON) # stop deweighting new data after 6 min, aka "short term memory" - self._WHEELPOS_THRESHOLD = 0.5 - self._WHEELPOS_FILTER_MIN_COUNT = int(5 / self._DT_DMON) - self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change @@ -67,9 +66,9 @@ def __init__(self): self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts -# model output refers to center of undistorted+leveled image -EFL = 598.0 # focal length in K -W, H = tici_d_frame_size # corrected image has same size as raw +# model output refers to center of cropped image, so need to apply the x displacement offset +RESIZED_FOCAL = 320.0 +H, W, FULL_W = 320, 160, 426 class DistractedType: NOT_DISTRACTED = 0 @@ -77,22 +76,22 @@ class DistractedType: DISTRACTED_BLINK = 2 DISTRACTED_E2E = 4 -def face_orientation_from_net(angles_desc, pos_desc, rpy_calib): +def face_orientation_from_net(angles_desc, pos_desc, rpy_calib, is_rhd): # the output of these angles are in device frame # so from driver's perspective, pitch is up and yaw is right pitch_net, yaw_net, roll_net = angles_desc - face_pixel_position = ((pos_desc[0]+0.5)*W, (pos_desc[1]+0.5)*H) - yaw_focal_angle = atan2(face_pixel_position[0] - W//2, EFL) - pitch_focal_angle = atan2(face_pixel_position[1] - H//2, EFL) + face_pixel_position = ((pos_desc[0] + .5)*W - W + FULL_W, (pos_desc[1]+.5)*H) + yaw_focal_angle = atan2(face_pixel_position[0] - FULL_W//2, RESIZED_FOCAL) + pitch_focal_angle = atan2(face_pixel_position[1] - H//2, RESIZED_FOCAL) pitch = pitch_net + pitch_focal_angle yaw = -yaw_net + yaw_focal_angle # no calib for roll pitch -= rpy_calib[1] - yaw -= rpy_calib[2] + yaw -= rpy_calib[2] * (1 - 2 * int(is_rhd)) # lhd -> -=, rhd -> += return roll_net, pitch, yaw class DriverPose(): @@ -113,6 +112,7 @@ class DriverBlink(): def __init__(self): self.left_blink = 0. self.right_blink = 0. + self.cfactor = 1. class DriverStatus(): def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): @@ -120,7 +120,7 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.settings = settings # init driver status - # self.wheelpos_learner = RunningStatFilter() + self.is_rhd_region = rhd self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) self.pose_calibrated = False self.blink = DriverBlink() @@ -137,8 +137,8 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.distracted_types = [] self.driver_distracted = False self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON) - self.wheel_on_right = rhd self.face_detected = False + self.face_partial = False self.terminal_alert_cnt = 0 self.terminal_time = 0 self.step_change = 0. @@ -197,7 +197,7 @@ def _get_distracted_types(self): yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw: distracted_types.append(DistractedType.DISTRACTED_POSE) - if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD: + if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD*self.blink.cfactor: distracted_types.append(DistractedType.DISTRACTED_BLINK) if self.ee1_calibrated: @@ -214,7 +214,13 @@ def _get_distracted_types(self): return distracted_types def set_policy(self, model_data, car_speed): + ep = min(model_data.meta.engagedProb, 0.8) / 0.8 # engaged prob bp = model_data.meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s + # TODO: retune adaptive blink + self.blink.cfactor = interp(ep, [0, 0.5, 1], + [self.settings._BLINK_THRESHOLD_STRICT, + self.settings._BLINK_THRESHOLD, + self.settings._BLINK_THRESHOLD_SLACK]) / self.settings._BLINK_THRESHOLD k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2) bp_normal = max(min(bp / k1, 0.5),0) self.pose.cfactor_pitch = interp(bp_normal, [0, 0.5], @@ -225,36 +231,28 @@ def set_policy(self, model_data, car_speed): self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD def update_states(self, driver_state, cal_rpy, car_speed, op_engaged): - # rhd_pred = driver_state.wheelOnRightProb - # if car_speed > 0.01: - # self.wheelpos_learner.push_and_update(rhd_pred) - # if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT: - # self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD - # else: - # self.wheel_on_right = rhd_pred > self.settings._WHEELPOS_THRESHOLD - driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData - if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition, - driver_data.faceOrientationStd, driver_data.facePositionStd, - driver_data.readyProb, driver_data.notReadyProb)): + if not all(len(x) > 0 for x in (driver_state.faceOrientation, driver_state.facePosition, + driver_state.faceOrientationStd, driver_state.facePositionStd, + driver_state.readyProb, driver_state.notReadyProb)): return - self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD - self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy) - if self.wheel_on_right: - self.pose.yaw *= -1 - self.pose.pitch_std = driver_data.faceOrientationStd[0] - self.pose.yaw_std = driver_data.faceOrientationStd[1] + self.face_partial = driver_state.partialFace > self.settings._PARTIAL_FACE_THRESHOLD + self.face_detected = driver_state.faceProb > self.settings._FACE_THRESHOLD or self.face_partial + self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_state.faceOrientation, driver_state.facePosition, cal_rpy, self.is_rhd_region) + self.pose.pitch_std = driver_state.faceOrientationStd[0] + self.pose.yaw_std = driver_state.faceOrientationStd[1] + # self.pose.roll_std = driver_state.faceOrientationStd[2] model_std_max = max(self.pose.pitch_std, self.pose.yaw_std) - self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD - self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) - self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) - self.eev1 = driver_data.notReadyProb[1] - self.eev2 = driver_data.readyProb[0] + self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD and not self.face_partial + self.blink.left_blink = driver_state.leftBlinkProb * (driver_state.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) + self.blink.right_blink = driver_state.rightBlinkProb * (driver_state.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) + self.eev1 = driver_state.notReadyProb[1] + self.eev2 = driver_state.readyProb[0] self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_POSE in self.distracted_types or DistractedType.DISTRACTED_BLINK in self.distracted_types) and \ - driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std + driver_state.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std self.driver_distraction_filter.update(self.driver_distracted) # update offseter diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index 43b5e7747e66ba..a84ed242ba0a68 100755 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -17,19 +17,19 @@ INVISIBLE_SECONDS_TO_RED = dm_settings._AWARENESS_TIME + 1 def make_msg(face_detected, distracted=False, model_uncertain=False): - ds = log.DriverStateV2.new_message() - ds.leftDriverData.faceOrientation = [0., 0., 0.] - ds.leftDriverData.facePosition = [0., 0.] - ds.leftDriverData.faceProb = 1. * face_detected - ds.leftDriverData.leftEyeProb = 1. - ds.leftDriverData.rightEyeProb = 1. - ds.leftDriverData.leftBlinkProb = 1. * distracted - ds.leftDriverData.rightBlinkProb = 1. * distracted - ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] - ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] + ds = log.DriverState.new_message() + ds.faceOrientation = [0., 0., 0.] + ds.facePosition = [0., 0.] + ds.faceProb = 1. * face_detected + ds.leftEyeProb = 1. + ds.rightEyeProb = 1. + ds.leftBlinkProb = 1. * distracted + ds.rightBlinkProb = 1. * distracted + ds.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] + ds.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] # TODO: test both separately when e2e is used - ds.leftDriverData.readyProb = [0., 0., 0., 0.] - ds.leftDriverData.notReadyProb = [0., 0.] + ds.readyProb = [0., 0., 0., 0.] + ds.notReadyProb = [0., 0.] return ds diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index 032b50487978e8..b83629c76a8347 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -52,7 +52,7 @@ def model_replay(lr, frs): vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, False, *(tici_f_frame_size)) vipc_server.start_listener() - sm = messaging.SubMaster(['modelV2', 'driverStateV2']) + sm = messaging.SubMaster(['modelV2', 'driverState']) pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'driverCameraState', 'liveCalibration', 'lateralPlan']) try: @@ -112,7 +112,7 @@ def model_replay(lr, frs): if min(frame_idxs['roadCameraState'], frame_idxs['wideRoadCameraState']) > recv_cnt['modelV2']: recv = "modelV2" elif msg.which() == 'driverCameraState': - recv = "driverStateV2" + recv = "driverState" # wait for a response with Timeout(15, f"timed out waiting for {recv}"): @@ -170,8 +170,8 @@ def model_replay(lr, frs): 'logMonoTime', 'modelV2.frameDropPerc', 'modelV2.modelExecutionTime', - 'driverStateV2.modelExecutionTime', - 'driverStateV2.dspExecutionTime' + 'driverState.modelExecutionTime', + 'driverState.dspExecutionTime' ] # TODO this tolerence is absurdly large tolerance = 5e-1 if PC else None diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 626d54a8f0e860..90520f2619e4d4 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -1d0979ca59dbc89bc5890656e9501e83f0556d50 +f74ab97371be93fdc28333e5ea12bbb78c3a32d0 diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 89885ef3974aac..1eda9bc7f59c89 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -293,7 +293,7 @@ def ublox_rcv_callback(msg): ProcessConfig( proc_name="dmonitoringd", pub_sub={ - "driverStateV2": ["driverMonitoringState"], + "driverState": ["driverMonitoringState"], "liveCalibration": [], "carState": [], "modelV2": [], "controlsState": [], }, ignore=["logMonoTime", "valid"], diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 3beeaff3b23708..6cf53a046e340b 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -33,7 +33,7 @@ "selfdrive.controls.radard": 4.5, "./_modeld": 4.48, "./boardd": 3.63, - "./_dmonitoringmodeld": 5.0, + "./_dmonitoringmodeld": 10.0, "selfdrive.thermald.thermald": 3.87, "selfdrive.locationd.calibrationd": 2.0, "./_soundd": 1.0, @@ -60,7 +60,7 @@ "roadCameraState": [2.5, 0.35], "driverCameraState": [2.5, 0.35], "modelV2": [2.5, 0.35], - "driverStateV2": [2.5, 0.40], + "driverState": [2.5, 0.40], "liveLocationKalman": [2.5, 0.35], "wideRoadCameraState": [1.5, 0.35], } @@ -221,7 +221,7 @@ def test_model_execution_timings(self): # TODO: this went up when plannerd cpu usage increased, why? cfgs = [ ("modelV2", 0.050, 0.036), - ("driverStateV2", 0.050, 0.026), + ("driverState", 0.050, 0.026), ] for (s, instant_max, avg_max) in cfgs: ts = [getattr(getattr(m, s), "modelExecutionTime") for m in self.lr if m.which() == s] diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc index 868a97f6041145..06578b455a7ca3 100644 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -25,7 +25,7 @@ void DriverViewWindow::mouseReleaseEvent(QMouseEvent* e) { emit done(); } -DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverStateV2"}), QWidget(parent) { +DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverState"}), QWidget(parent) { face_img = loadPixmap("../assets/img_driver_face.png", {FACE_IMG_SIZE, FACE_IMG_SIZE}); } @@ -57,36 +57,43 @@ void DriverViewScene::paintEvent(QPaintEvent* event) { return; } - cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); - cereal::DriverStateV2::DriverData::Reader driver_data; + const int width = 4 * height() / 3; + const QRect rect2 = {rect().center().x() - width / 2, rect().top(), width, rect().height()}; + const QRect valid_rect = {is_rhd ? rect2.right() - rect2.height() / 2 : rect2.left(), rect2.top(), rect2.height() / 2, rect2.height()}; - // is_rhd = driver_state.getWheelOnRightProb() > 0.5; - driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); + // blackout + const QColor bg(0, 0, 0, 140); + const QRect& blackout_rect = rect(); + p.fillRect(blackout_rect.adjusted(0, 0, valid_rect.left() - blackout_rect.right(), 0), bg); + p.fillRect(blackout_rect.adjusted(valid_rect.right() - blackout_rect.left(), 0, 0, 0), bg); + p.fillRect(blackout_rect.adjusted(valid_rect.left()-blackout_rect.left()+1, 0, valid_rect.right()-blackout_rect.right()-1, -valid_rect.height()*7/10), bg); // top dz - bool face_detected = driver_data.getFaceProb() > 0.5; + // face bounding box + cereal::DriverState::Reader driver_state = sm["driverState"].getDriverState(); + bool face_detected = driver_state.getFaceProb() > 0.5; if (face_detected) { - auto fxy_list = driver_data.getFacePosition(); - auto std_list = driver_data.getFaceOrientationStd(); + auto fxy_list = driver_state.getFacePosition(); + auto std_list = driver_state.getFaceOrientationStd(); float face_x = fxy_list[0]; float face_y = fxy_list[1]; float face_std = std::max(std_list[0], std_list[1]); float alpha = 0.7; - if (face_std > 0.15) { - alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); + if (face_std > 0.08) { + alpha = std::max(0.7 - (face_std-0.08)*7, 0.0); } - const int box_size = 220; - // use approx instead of distort_points - int fbox_x = 1080.0 - 1714.0 * face_x; - int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; + const int box_size = 0.6 * rect2.height() / 2; + const float rhd_offset = 0.05; // lhd is shifted, so rhd is not mirrored + int fbox_x = valid_rect.center().x() + (is_rhd ? (face_x + rhd_offset) : -face_x) * valid_rect.width(); + int fbox_y = valid_rect.center().y() + face_y * valid_rect.height(); p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); } // icon - const int img_offset = 60; - const int img_x = rect().left() + img_offset; - const int img_y = rect().bottom() - FACE_IMG_SIZE - img_offset; - p.setOpacity(face_detected ? 1.0 : 0.2); + const int img_offset = 30; + const int img_x = is_rhd ? rect2.right() - FACE_IMG_SIZE - img_offset : rect2.left() + img_offset; + const int img_y = rect2.bottom() - FACE_IMG_SIZE - img_offset; + p.setOpacity(face_detected ? 1.0 : 0.3); p.drawPixmap(img_x, img_y, face_img); } diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index 0a3b9762e56826..f16e8e4e0def08 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -54,15 +54,16 @@ const mat4 device_transform = {{ }}; mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) { - const float driver_view_ratio = 2.0; - const float yscale = stream_height * driver_view_ratio / stream_width; + const float driver_view_ratio = 1.333; + const float yscale = stream_height * driver_view_ratio / tici_dm_crop::width; const float xscale = yscale*screen_height/screen_width*stream_width/stream_height; mat4 transform = (mat4){{ - xscale, 0.0, 0.0, 0.0, - 0.0, yscale, 0.0, 0.0, + xscale, 0.0, 0.0, xscale*tici_dm_crop::x_offset/stream_width*2, + 0.0, yscale, 0.0, yscale*tici_dm_crop::y_offset/stream_height*2, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, }}; + return transform; } From 6618d2bebee9a1fc5a0ee5d5a9b2764403865281 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 10 Jun 2022 16:18:31 -0700 Subject: [PATCH 038/436] Add missing fw versions for 2019 Sonata (#24814) --- selfdrive/car/hyundai/values.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 7d3ab93cdee778..4ca81fdb403db8 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -501,6 +501,7 @@ class Buttons: ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00LFF LKAS AT USA LHD 1.00 1.01 95740-C1000 E51', + b'\xf1\x00LFF LKAS AT USA LHD 1.01 1.02 95740-C1000 E52', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x006T6H0_C2\x00\x006T6B4051\x00\x00TLF0G24NL1\xb0\x9f\xee\xf5', @@ -509,6 +510,7 @@ class Buttons: b'\xf1\x87\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xf1\x816T6B4051\x00\x00\xf1\x006T6H0_C2\x00\x006T6B4051\x00\x00TLF0G24SL2n\x8d\xbe\xd8', b'\xf1\x87LAHSGN012918KF10\x98\x88x\x87\x88\x88x\x87\x88\x88\x98\x88\x87w\x88w\x88\x88\x98\x886o\xf6\xff\x98w\x7f\xff3\x00\xf1\x816W3B1051\x00\x00\xf1\x006W351_C2\x00\x006W3B1051\x00\x00TLF0T20NL2\x00\x00\x00\x00', b'\xf1\x87LAHSGN012918KF10\x98\x88x\x87\x88\x88x\x87\x88\x88\x98\x88\x87w\x88w\x88\x88\x98\x886o\xf6\xff\x98w\x7f\xff3\x00\xf1\x816W3B1051\x00\x00\xf1\x006W351_C2\x00\x006W3B1051\x00\x00TLF0T20NL2H\r\xbdm', + b'\xf1\x87LAJSG49645724HF0\x87x\x87\x88\x87www\x88\x99\xa8\x89\x88\x99\xa8\x89\x88\x99\xa8\x89S_\xfb\xff\x87f\x7f\xff^2\xf1\x816W3B1051\x00\x00\xf1\x006W351_C2\x00\x006W3B1051\x00\x00TLF0T20NL2H\r\xbdm', ], }, CAR.TUCSON: { From a8ccd8f838aff958a60546de04bf42220567b288 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 10 Jun 2022 16:26:06 -0700 Subject: [PATCH 039/436] put cereal on master --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index e9d3597d23311a..ff49c8e126ea04 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit e9d3597d23311a3e33a2def74ceec839e5ff4bf5 +Subproject commit ff49c8e126ea04889af13d690ceaf520ce7084d2 From 3066ad81a8b2f89a22ddfc86a879395c573ce0f5 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Fri, 10 Jun 2022 17:57:23 -0700 Subject: [PATCH 040/436] Car interface: set max lateral torque from table (#24789) * json * better naem * Read from table * formatting and default to nan * Generate docs * Read from table * this should be the same * Prius v is full * test we always set the tunes correctly add to release files * Set for all cars Set for all cars * Revert tuning changes Revert tuning changes * remove that * fixes * update ref commit for new maxLateralAccels Co-authored-by: Shane Smiskol --- docs/CARS.md | 68 +++++++++++----------- release/files_common | 1 + selfdrive/car/chrysler/interface.py | 8 --- selfdrive/car/docs_definitions.py | 2 +- selfdrive/car/gm/interface.py | 2 - selfdrive/car/honda/interface.py | 11 ---- selfdrive/car/hyundai/interface.py | 8 --- selfdrive/car/interfaces.py | 10 +++- selfdrive/car/subaru/interface.py | 2 - selfdrive/car/tests/test_car_interfaces.py | 5 +- selfdrive/car/tests/test_docs.py | 6 ++ selfdrive/car/torque_data.json | 1 + selfdrive/car/toyota/interface.py | 12 ---- selfdrive/car/volkswagen/interface.py | 6 -- selfdrive/test/process_replay/ref_commit | 2 +- 15 files changed, 57 insertions(+), 87 deletions(-) create mode 100644 selfdrive/car/torque_data.json diff --git a/docs/CARS.md b/docs/CARS.md index e9b9e947f3e4e1..479c519dabca51 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -35,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -# Gold - 29 cars +# Gold - 28 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -52,10 +52,9 @@ How We Rate The Cars |Lexus|ES 2019-21|All|||||| |Lexus|ES Hybrid 2019-22|All|||||| |Lexus|NX 2020|All|||||| -|Lexus|NX Hybrid 2020|All|||||| +|Lexus|RX 2020-22|All|||||| |Lexus|UX Hybrid 2019-21|All|||||| |Toyota|Avalon 2022|All|||||| -|Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| |Toyota|Corolla 2020-22|All|||||| @@ -69,14 +68,13 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 78 cars +# Silver - 75 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| |Audi|A3 2014-19|ACC + Lane Assist|||||| |Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|||||| |Audi|Q2 2018|ACC + Lane Assist|||||| -|Audi|Q3 2020-21|ACC + Lane Assist|||||| |Audi|RS3 2018|ACC + Lane Assist|||||| |Audi|S3 2015-17|ACC + Lane Assist|||||| |Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| @@ -93,7 +91,6 @@ How We Rate The Cars |Hyundai|Santa Fe 2021-22|All|||||| |Hyundai|Santa Fe Hybrid 2022|All|||||| |Hyundai|Santa Fe Plug-in Hybrid 2022|All|||||| -|Hyundai|Sonata 2018-19|SCC + LKAS|||||| |Hyundai|Tucson Diesel 2019|SCC + LKAS|||||| |Kia|Ceed 2019|SCC + LKAS|||||| |Kia|Forte 2018|SCC + LKAS|||||| @@ -106,15 +103,16 @@ How We Rate The Cars |Kia|Sorento 2018|SCC + LKAS|||||| |Kia|Sorento 2019|SCC + LKAS|||||| |Kia|Stinger 2018|SCC + LKAS|||||| -|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|NX 2018-19|All|[3](#footnotes)||||| |Lexus|NX Hybrid 2018-19|All|[3](#footnotes)||||| -|Lexus|RX 2020-22|All|||||| +|Lexus|NX Hybrid 2020|All|||||| |Lexus|RX Hybrid 2020-21|All|||||| |Mazda|CX-5 2022|All|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| +|Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Forester 2019-21|All|||||| +|Subaru|Impreza 2020-21|EyeSight|||||| |Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| |Škoda|Karoq 2019|Driver Assistance|||||| |Škoda|Kodiaq 2018-19|Driver Assistance|||||| @@ -125,18 +123,17 @@ How We Rate The Cars |Toyota|Alphard 2019-20|All|||||| |Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| |Toyota|Highlander 2017-19|All|[3](#footnotes)||||| |Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| |Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| |Toyota|Prius Prime 2017-20|All|[3](#footnotes)||||| -|Toyota|RAV4 2022|All|||||| |Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 Hybrid 2022|All|||||| |Toyota|Sienna 2018-20|All|[3](#footnotes)||||| -|Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Atlas 2018-19, 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|e-Golf 2014, 2018-20|Driver Assistance|||||| |Volkswagen|Golf 2015-20|Driver Assistance|||||| |Volkswagen|Golf Alltrack 2017-18|Driver Assistance|||||| @@ -145,57 +142,59 @@ How We Rate The Cars |Volkswagen|Golf R 2016-19|Driver Assistance|||||| |Volkswagen|Golf SportsVan 2016|Driver Assistance|||||| |Volkswagen|Golf SportWagen 2015|Driver Assistance|||||| -|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Polo 2020|Driver Assistance|||||| |Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 66 cars +# Bronze - 70 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-19|AcuraWatch Plus|||||| |Acura|RDX 2016-18|AcuraWatch Plus|||||| -|Acura|RDX 2019-21|All|||||| +|Acura|RDX 2019-21|All|||||| +|Audi|Q3 2020-21|ACC + Lane Assist|||||| |Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica 2020|Adaptive Cruise|||||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| +|Chrysler|Pacifica 2020|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica Hybrid 2019-21|Adaptive Cruise|||||| +|Chrysler|Pacifica Hybrid 2019-21|Adaptive Cruise|||||| |Genesis|G90 2018|All|||||| |GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| -|Honda|Accord 2018-21|All|||||| -|Honda|Accord Hybrid 2018-21|All|||||| +|Honda|Accord 2018-21|All|||||| +|Honda|Accord Hybrid 2018-21|All|||||| |Honda|Civic 2016-18|Honda Sensing|||||| -|Honda|Civic 2019-20|All|||[2](#footnotes)||| -|Honda|Civic Hatchback 2017-21|Honda Sensing|||||| +|Honda|Civic 2019-20|All|||[2](#footnotes)||| +|Honda|Civic Hatchback 2017-21|Honda Sensing|||||| |Honda|CR-V 2015-16|Touring|||||| -|Honda|CR-V 2017-21|Honda Sensing|||||| +|Honda|CR-V 2017-21|Honda Sensing|||||| |Honda|CR-V Hybrid 2017-19|Honda Sensing|||||| |Honda|e 2020|All|||||| -|Honda|Fit 2018-19|Honda Sensing|||||| +|Honda|Fit 2018-19|Honda Sensing|||||| |Honda|Freed 2020|Honda Sensing|||||| |Honda|HR-V 2019-20|Honda Sensing|||||| -|Honda|Insight 2019-21|All|||||| -|Honda|Inspire 2018|All|||||| +|Honda|Insight 2019-21|All|||||| +|Honda|Inspire 2018|All|||||| |Honda|Odyssey 2018-20|Honda Sensing|||||| -|Honda|Passport 2019-21|All|||||| -|Honda|Pilot 2016-21|Honda Sensing|||||| -|Honda|Ridgeline 2017-22|Honda Sensing|||||| +|Honda|Passport 2019-21|All|||||| +|Honda|Pilot 2016-21|Honda Sensing|||||| +|Honda|Ridgeline 2017-22|Honda Sensing|||||| |Hyundai|Elantra 2017-19|SCC + LKAS|||||| |Hyundai|Genesis 2015-16|SCC + LKAS|||||| |Hyundai|Ioniq Electric 2019|SCC + LKAS|||||| |Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|||||| |Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|||||| +|Hyundai|Sonata 2018-19|SCC + LKAS|||||| |Hyundai|Tucson 2021|SCC + LKAS|||||| |Hyundai|Veloster 2019-20|SCC + LKAS|||||| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| -|Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| +|Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| |Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| |Kia|Optima 2017|SCC + LKAS|||||| |Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| +|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|IS 2017-19|All|||||| |Lexus|RC 2020|All|||||| |Lexus|RX 2016-18|All|[3](#footnotes)||||| @@ -207,20 +206,21 @@ How We Rate The Cars |Nissan|X-Trail 2017|ProPILOT|||||| |Subaru|Ascent 2019-20|All|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| -|Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| -|Subaru|Impreza 2020-21|EyeSight|||||| -|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| |Toyota|C-HR 2017-21|All|||||| -|Toyota|C-HR Hybrid 2017-19|All|||||| +|Toyota|C-HR Hybrid 2017-19|All|||||| |Toyota|Corolla 2017-19|All|[3](#footnotes)||||| |Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| -|Volkswagen|Atlas 2018-19, 2022[7](#footnotes)|Driver Assistance|||||| +|Toyota|RAV4 2022|All|||||| +|Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Jetta 2018-21|Driver Assistance|||||| |Volkswagen|Jetta GLI 2021|Driver Assistance|||||| +|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|||||| diff --git a/release/files_common b/release/files_common index ceb0110606e932..5230511ddd1e62 100644 --- a/release/files_common +++ b/release/files_common @@ -104,6 +104,7 @@ selfdrive/car/fw_versions.py selfdrive/car/isotp_parallel_query.py selfdrive/car/tests/__init__.py selfdrive/car/tests/test_car_interfaces.py +selfdrive/car/torque_data.json selfdrive/car/body/*.py selfdrive/car/chrysler/*.py diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index e47b0855337c15..55672dd4ab60c0 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -23,14 +23,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerRateCost = 0.7 ret.steerLimitTimer = 0.4 - # set max lateral acceleration - if candidate in (CAR.PACIFICA_2018, CAR.PACIFICA_2019_HYBRID, CAR.JEEP_CHEROKEE, CAR.JEEP_CHEROKEE_2019): - ret.maxLateralAccel = 1.6 - if candidate in (CAR.PACIFICA_2018_HYBRID,): - ret.maxLateralAccel = 1.4 - if candidate in (CAR.PACIFICA_2017_HYBRID,): - ret.maxLateralAccel = 1.2 - if candidate in (CAR.JEEP_CHEROKEE, CAR.JEEP_CHEROKEE_2019): ret.wheelbase = 2.91 # in meters ret.steerRatio = 12.7 diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index e3b46c29e4da60..3700a248355be1 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union, no_type_check TACO_TORQUE_THRESHOLD = 2.5 # m/s^2 -GREAT_TORQUE_THRESHOLD = 1.5 # m/s^2 +GREAT_TORQUE_THRESHOLD = 1.4 # m/s^2 GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2 diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index bc430865862f8f..498f4b334732f6 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -83,7 +83,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerRatio = 17.7 # Stock 15.7, LiveParameters tire_stiffness_factor = 0.469 # Stock Michelin Energy Saver A/S, LiveParameters ret.centerToFront = ret.wheelbase * 0.45 # Volt Gen 1, TODO corner weigh - ret.maxLateralAccel = 1.6 ret.lateralTuning.pid.kpBP = [0., 40.] ret.lateralTuning.pid.kpV = [0., 0.17] @@ -111,7 +110,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.wheelbase = 2.86 ret.steerRatio = 14.4 # end to end is 13.46 ret.centerToFront = ret.wheelbase * 0.4 - ret.maxLateralAccel = 1.4 ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_acadia() elif candidate == CAR.BUICK_REGAL: diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index de5db98ea67dd3..ea7deab5d70b24 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -100,7 +100,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 8000], [0, 2560, 3840]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.1]] else: - ret.maxLateralAccel = 0.4 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560], [0, 2560]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]] tire_stiffness_factor = 1. @@ -113,7 +112,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 15.38 # 10.93 is end-to-end spec ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 1. - ret.maxLateralAccel = 1.6 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] elif candidate in (CAR.ACCORD, CAR.ACCORDH): @@ -128,7 +126,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl if eps_modified: ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.09]] else: - ret.maxLateralAccel = 1.6 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]] elif candidate == CAR.ACURA_ILX: @@ -165,7 +162,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 2560, 10000], [0, 2560, 3840]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.21], [0.07]] else: - ret.maxLateralAccel = 1.7 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.64], [0.192]] tire_stiffness_factor = 0.677 @@ -190,7 +186,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 13.06 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.75 - ret.maxLateralAccel = 1.7 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.05]] elif candidate == CAR.FREED: @@ -222,7 +217,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.centerToFront = ret.wheelbase * 0.38 ret.steerRatio = 15.0 # as spec tire_stiffness_factor = 0.444 - ret.maxLateralAccel = 0.9 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 1000], [0, 1000]] # TODO: determine if there is a dead zone at the top end ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.8], [0.24]] @@ -232,7 +226,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.75 ret.centerToFront = ret.wheelbase * 0.41 ret.steerRatio = 11.95 # as spec - ret.maxLateralAccel = 1.2 ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 3840], [0, 3840]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.06]] tire_stiffness_factor = 0.677 @@ -245,7 +238,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.35 # as spec ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.82 - ret.maxLateralAccel = 0.8 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.28], [0.08]] elif candidate == CAR.ODYSSEY_CHN: @@ -266,7 +258,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 17.25 # as spec ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.444 - ret.maxLateralAccel = 1.5 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.38], [0.11]] elif candidate == CAR.RIDGELINE: @@ -277,7 +268,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 15.59 # as spec ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.444 - ret.maxLateralAccel = 1.3 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.38], [0.11]] elif candidate == CAR.INSIGHT: @@ -288,7 +278,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 15.0 # 12.58 is spec end-to-end ret.lateralParams.torqueBP, ret.lateralParams.torqueV = [[0, 4096], [0, 4096]] # TODO: determine if there is a dead zone at the top end tire_stiffness_factor = 0.82 - ret.maxLateralAccel = 1.4 ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.18]] elif candidate == CAR.HONDA_E: diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 831b318d556208..08af65499646fb 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -63,7 +63,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl # Values from optimizer ret.steerRatio = 16.55 # 13.8 is spec end-to-end tire_stiffness_factor = 0.82 - ret.maxLateralAccel = 3.2 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[9., 22.], [9., 22.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.35], [0.05, 0.09]] elif candidate in (CAR.SONATA, CAR.SONATA_HYBRID): @@ -72,7 +71,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.84 ret.steerRatio = 13.27 * 1.15 # 15% higher at the center seems reasonable tire_stiffness_factor = 0.65 - ret.maxLateralAccel = 2.5 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] elif candidate == CAR.SONATA_LF: @@ -80,7 +78,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 4497. * CV.LB_TO_KG ret.wheelbase = 2.804 ret.steerRatio = 13.27 * 1.15 # 15% higher at the center seems reasonable - ret.maxLateralAccel = 1.8 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] elif candidate == CAR.PALISADE: @@ -89,7 +86,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.90 ret.steerRatio = 15.6 * 1.15 tire_stiffness_factor = 0.63 - ret.maxLateralAccel = 2.5 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.05]] elif candidate in (CAR.ELANTRA, CAR.ELANTRA_GT_I30): @@ -146,7 +142,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.7 ret.steerRatio = 13.73 # Spec tire_stiffness_factor = 0.385 - ret.maxLateralAccel = 3.0 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] if candidate not in (CAR.IONIQ_EV_2020, CAR.IONIQ_PHEV, CAR.IONIQ_HEV_2022): @@ -196,7 +191,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.7 ret.steerRatio = 13.9 if CAR.KIA_NIRO_HEV_2021 else 13.73 # Spec tire_stiffness_factor = 0.385 - ret.maxLateralAccel = 2.9 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] if candidate == CAR.KIA_NIRO_HEV: @@ -228,7 +222,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 1825. + STD_CARGO_KG ret.wheelbase = 2.78 ret.steerRatio = 14.4 * 1.15 # 15% higher at the center seems reasonable - ret.maxLateralAccel = 2.4 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] elif candidate == CAR.KIA_FORTE: @@ -254,7 +247,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.85 ret.steerRatio = 13.27 # 2021 Kia K5 Steering Ratio (all trims) tire_stiffness_factor = 0.5 - ret.maxLateralAccel = 2.1 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] elif candidate == CAR.KIA_EV6: diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index a2c11bf02340d3..9220aee522f7e2 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -1,3 +1,4 @@ +import json import os import time from abc import abstractmethod, ABC @@ -19,6 +20,7 @@ MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS ACCEL_MAX = 2.0 ACCEL_MIN = -3.5 +TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data.json') # generic car and radar interfaces @@ -81,7 +83,7 @@ def get_std_params(candidate, fingerprint): ret.steerControlType = car.CarParams.SteerControlType.torque ret.minSteerSpeed = 0. ret.wheelSpeedFactor = 1.0 - ret.maxLateralAccel = float('nan') + ret.maxLateralAccel = CarInterfaceBase.get_torque_params(candidate)['MAX_LAT_ACCEL_MEASURED'] ret.pcmCruise = True # openpilot's state is tied to the PCM's cruise state on most cars ret.minEnableSpeed = -1. # enable is done by stock ACC, so ignore this @@ -105,6 +107,12 @@ def get_std_params(candidate, fingerprint): ret.steerLimitTimer = 1.0 return ret + @staticmethod + def get_torque_params(candidate, default=float('NaN')): + with open(TORQUE_PARAMS_PATH) as f: + data = json.load(f) + return {key: data[key].get(candidate, default) for key in data} + @abstractmethod def _update(self, c: car.CarControl) -> car.CarState: pass diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index edd26a44491d41..d0d8e91ce18f70 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -51,7 +51,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 17 # learned, 14 stock ret.steerActuatorDelay = 0.1 - ret.maxLateralAccel = 1.3 ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.045, 0.042, 0.20], [0.04, 0.035, 0.045]] @@ -62,7 +61,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 17 # learned, 14 stock ret.steerActuatorDelay = 0.1 - ret.maxLateralAccel = 3.2 ret.lateralTuning.pid.kf = 0.000038 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.065, 0.2], [0.001, 0.015, 0.025]] diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 92024ab0c2f1b9..52c89440c74226 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import math import unittest import importlib from parameterized import parameterized @@ -39,7 +40,9 @@ def test_car_interfaces(self, car_name): if tuning == 'pid': self.assertTrue(len(car_params.lateralTuning.pid.kpV)) elif tuning == 'torque': - self.assertTrue(car_params.lateralTuning.torque.kf > 0) + kf = car_params.lateralTuning.torque.kf + self.assertTrue(not math.isnan(kf) and kf > 0) + self.assertTrue(not math.isnan(car_params.lateralTuning.torque.friction)) elif tuning == 'indi': self.assertTrue(len(car_params.lateralTuning.indi.outerLoopGainV)) diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index fed6989d409135..b4bc14ef005aab 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -3,6 +3,7 @@ from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE, generate_cars_md, get_all_car_info +from selfdrive.car.docs_definitions import Column, Star class TestCarDocs(unittest.TestCase): @@ -37,6 +38,11 @@ def test_naming_conventions(self): if "rav4" in tokens: self.assertIn("RAV4", car.model, "Use correct capitalization") + def test_torque_star(self): + for car in self.all_cars: + if car.car_name == "honda": + self.assertTrue(car.row[Column.STEERING_TORQUE] in (Star.EMPTY, Star.HALF), f"{car.name} has full torque star") + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/car/torque_data.json b/selfdrive/car/torque_data.json new file mode 100644 index 00000000000000..63ee0e9bcd9e12 --- /dev/null +++ b/selfdrive/car/torque_data.json @@ -0,0 +1 @@ +{"LAT_ACCEL_FACTOR": {"HONDA PILOT 2017": 1.682289482065265, "HONDA CIVIC 2016": 1.5248128495527884, "TOYOTA CAMRY 2018": 2.1115709806216447, "TOYOTA COROLLA HYBRID TSS2 2019": 2.3250600977240077, "TOYOTA RAV4 2019": 2.625504029066767, "HYUNDAI PALISADE 2020": 2.5250855675875634, "TOYOTA SIENNA 2018": 1.8254254785341577, "ACURA RDX 2020": 1.3998101622214894, "TOYOTA RAV4 2017": 1.948190869577896, "HONDA RIDGELINE 2017": 1.4158181862793415, "TOYOTA PRIUS 2017": 1.9142926195557595, "TOYOTA HIGHLANDER HYBRID 2020": 2.1097056247344392, "HYUNDAI SONATA 2020": 3.2488989629905944, "KIA STINGER GT2 2018": 2.7592622336517834, "TOYOTA HIGHLANDER 2020": 2.0408544157877055, "HONDA ACCORD 2018": 1.6374118241564064, "TOYOTA PRIUS TSS2 2021": 2.3207270770298365, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.46050785084946, "LEXUS NX 2020": 2.29533657249232, "TOYOTA RAV4 HYBRID 2019": 2.4003012079562085, "HONDA CIVIC (BOSCH) 2019": 1.6523031416671652, "KIA NIRO HYBRID 2021": 2.743464625803003, "HONDA ACCORD HYBRID 2018": 1.5904016830979033, "LEXUS NX HYBRID 2018": 2.398678119681945, "TOYOTA COROLLA TSS2 2019": 2.3859244449846466, "VOLKSWAGEN ARTEON 1ST GEN": 1.4249208219414902, "TOYOTA CAMRY HYBRID 2021": 2.5434553806317055, "VOLKSWAGEN JETTA 7TH GEN": 1.2228130240634283, "HONDA INSIGHT 2019": 1.468352089969897, "SUBARU FORESTER 2019": 3.6185035528523546, "HYUNDAI ELANTRA 2021": 3.5294999663335185, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.2179616966432905, "HYUNDAI KONA HYBRID 2020": 4.493208192966529, "HONDA ODYSSEY 2018": 1.8838175399087222, "LEXUS RX 2016": 1.3912132245094184, "TOYOTA COROLLA 2017": 3.0143547548384735, "LEXUS ES 2019": 2.012201253045193, "HYUNDAI SANTA FE 2019": 3.039728566484244, "TOYOTA AVALON 2022": 2.4619858654670885, "JEEP GRAND CHEROKEE V6 2018": 1.8411674990629987, "CHEVROLET VOLT PREMIER 2017": 1.5943438675127841, "TOYOTA RAV4 HYBRID 2017": 1.9803053616868995, "LEXUS RX 2020": 1.664616846377383, "TOYOTA HIGHLANDER HYBRID 2018": 1.8866764457400844, "TOYOTA CAMRY HYBRID 2018": 2.014213351947917, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.4428896585442685, "TOYOTA MIRAI 2021": 2.7217623852898853, "LEXUS IS 2018": 3.5624668608596837, "TOYOTA HIGHLANDER 2017": 1.9199133105823853, "HYUNDAI SONATA HYBRID 2021": 2.7313907441569554, "VOLKSWAGEN ATLAS 1ST GEN": 1.4483948408160645, "LEXUS ES HYBRID 2019": 2.4138026617523547, "HYUNDAI GENESIS 2015-2016": 1.7636839808044658, "JEEP GRAND CHEROKEE 2019": 1.787264083939164, "SUBARU ASCENT LIMITED 2019": 3.0494069339774565, "HONDA CR-V 2017": 1.9828470679233807, "HONDA FIT 2018": 1.594940026552055, "TOYOTA CAMRY 2021": 2.5057990840460342, "AUDI Q3 2ND GEN": 1.4558300885316715, "AUDI A3 3RD GEN": 1.5304173783542625, "LEXUS RX HYBRID 2017": 1.577216425446677, "HONDA CIVIC 2022": 2.69252285552613, "GENESIS G70 2018": 3.866842361627636, "CHRYSLER PACIFICA HYBRID 2018": 1.5771851419640903, "VOLKSWAGEN PASSAT 8TH GEN": 1.2985597059739313, "HONDA CR-V 2016": 0.7745984062630755, "HYUNDAI IONIQ PHEV 2020": 2.5696218908589383, "GMC ACADIA DENALI 2018": 1.3310088601868082, "HYUNDAI SONATA 2019": 1.9736552675022665, "TOYOTA AVALON 2019": 1.7245149905226294, "TOYOTA C-HR 2018": 1.5895016960662856, "HONDA CR-V HYBRID 2019": 2.0687746810729193, "CHRYSLER PACIFICA 2020": 1.40536880000744, "HYUNDAI IONIQ ELECTRIC 2020": 3.3220838625838667, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 1.7753192756242595, "KIA OPTIMA SX 2019 & 2016": 3.12625562280304, "TOYOTA AVALON HYBRID 2019": 1.7681286449373381, "TOYOTA RAV4 HYBRID 2022": 2.5518187542231816, "HONDA PASSPORT 2021": 1.5174924139130355, "KIA K5 2021": 2.482916204106975, "ACURA ILX 2016": 1.5237423964720282, "HYUNDAI IONIQ HYBRID 2017-2019": 2.3723887901632645, "KIA NIRO EV 2020": 2.924651969180446, "SUBARU IMPREZA SPORT 2020": 2.5317689549587694, "CHRYSLER PACIFICA HYBRID 2017": 1.167126725149114, "HYUNDAI KONA ELECTRIC 2019": 4.201092987427836, "HYUNDAI ELANTRA HYBRID 2021": 3.7153193626001926, "HYUNDAI SANTA FE HYBRID 2022": 3.3049230586030545, "CHRYSLER PACIFICA 2018": 1.524867383058782, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 2.5970979517766213, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.5460982690267173, "MAZDA CX-9 2021": 1.9514800984278198, "HYUNDAI SANTA FE 2022": 3.5354982200434524, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.8902492836532216, "HONDA HRV 2019": 2.1262957371020352, "TOYOTA AVALON HYBRID 2022": 2.4142150048378683, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": 2.4086794443413906, "VOLKSWAGEN TAOS 1ST GEN": 2.0031666974545947, "KIA FORTE E 2018 & GT 2021": 2.022553820222557, "CADILLAC ESCALADE ESV 2016": 1.5522339636408988, "TOYOTA C-HR 2021": 1.6519334844316687, "TOYOTA C-HR HYBRID 2018": 1.3193315010905482}, "MAX_LAT_ACCEL_MEASURED": {"HONDA PILOT 2017": 0.9069354290994807, "HONDA CIVIC 2016": 0.4030275472529351, "TOYOTA CAMRY 2018": 1.686123168195758, "TOYOTA COROLLA HYBRID TSS2 2019": 1.9139332621491167, "TOYOTA RAV4 2019": 2.234047196286479, "HYUNDAI PALISADE 2020": 1.8303582523301922, "TOYOTA SIENNA 2018": 1.4752503435300715, "ACURA RDX 2020": 0.40911581320000334, "TOYOTA RAV4 2017": 1.6622227720995595, "HONDA RIDGELINE 2017": 0.8224685813281227, "TOYOTA PRIUS 2017": 1.4548827870876067, "TOYOTA HIGHLANDER HYBRID 2020": 2.0649784271823037, "HYUNDAI SONATA 2020": 2.243989856570093, "KIA STINGER GT2 2018": 1.9531287107084392, "TOYOTA HIGHLANDER 2020": 1.659381392090836, "HONDA ACCORD 2018": 0.40486739531686267, "TOYOTA PRIUS TSS2 2021": 1.861541601048098, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.1930739374812243, "LEXUS NX 2020": 1.565268724838564, "TOYOTA RAV4 HYBRID 2019": 2.0915384047218426, "HONDA CIVIC (BOSCH) 2019": 0.4062886118517984, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 0.35128914564548286, "LEXUS NX HYBRID 2018": 1.81821359787186, "TOYOTA COROLLA TSS2 2019": 1.911280958056631, "VOLKSWAGEN ARTEON 1ST GEN": 1.2587939472578302, "TOYOTA CAMRY HYBRID 2021": 2.312510643730013, "VOLKSWAGEN JETTA 7TH GEN": 1.232161945396623, "HONDA INSIGHT 2019": 0.5174836462945298, "SUBARU FORESTER 2019": 2.29255993930968, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.133978602602408, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 0.8254773781363679, "LEXUS RX 2016": 1.0954776820595344, "TOYOTA COROLLA 2017": 2.2012870528168964, "LEXUS ES 2019": 2.069508805495439, "HYUNDAI SANTA FE 2019": 2.3763720477660253, "TOYOTA AVALON 2022": 2.531962323786023, "JEEP GRAND CHEROKEE V6 2018": 1.4193323242487865, "CHEVROLET VOLT PREMIER 2017": 1.8576430337666092, "TOYOTA RAV4 HYBRID 2017": 1.7425797219020926, "LEXUS RX 2020": 1.5118835180227874, "TOYOTA HIGHLANDER HYBRID 2018": 1.6872527654528833, "TOYOTA CAMRY HYBRID 2018": 1.6793468378089895, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.5614447712441282, "TOYOTA MIRAI 2021": 2.271146483563897, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 1.6573774863189379, "HYUNDAI SONATA HYBRID 2021": 1.9464120717803253, "VOLKSWAGEN ATLAS 1ST GEN": 1.6867005451451638, "LEXUS ES HYBRID 2019": 1.956450687999482, "HYUNDAI GENESIS 2015-2016": 1.5359761378898085, "JEEP GRAND CHEROKEE 2019": 1.2418961305308847, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 0.2642062271814174, "HONDA FIT 2018": 0.5896345937094754, "TOYOTA CAMRY 2021": 2.1783533980215166, "AUDI Q3 2ND GEN": 1.1582239457022647, "AUDI A3 3RD GEN": 1.598699116126939, "LEXUS RX HYBRID 2017": 1.319771127672888, "HONDA CIVIC 2022": 1.1806949852580793, "GENESIS G70 2018": 2.2496820850331134, "CHRYSLER PACIFICA HYBRID 2018": 1.294798200968084, "VOLKSWAGEN PASSAT 8TH GEN": 1.247540921731637, "HONDA CR-V 2016": 0.6991119250342539, "HYUNDAI IONIQ PHEV 2020": 1.9062392690595655, "GMC ACADIA DENALI 2018": 1.2986994230652662, "HYUNDAI SONATA 2019": 1.257445187146704, "TOYOTA AVALON 2019": 1.664577368475227, "TOYOTA C-HR 2018": 1.308490445144888, "HONDA CR-V HYBRID 2019": 0.4693072746041504, "CHRYSLER PACIFICA 2020": 1.1712413003138664, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": 1.1573057001955744, "LEXUS NX 2018": 1.9457312007432144, "KIA OPTIMA SX 2019 & 2016": 2.0928228595938845, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 1.7647290773049569, "HONDA PASSPORT 2021": 0.8248357750132685, "KIA K5 2021": 1.4628018983720577, "ACURA ILX 2016": 0.6330753921140401, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 2.020186575503497, "SUBARU IMPREZA SPORT 2020": 2.136786720514988, "CHRYSLER PACIFICA HYBRID 2017": 1.0642918033307907, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 1.3654603720349934, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.255230465866663, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 3.3823387508235827, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.544104124172169, "HONDA HRV 2019": 0.7492792210307291, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.8127509604734238, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 1.6590543949912684, "KIA FORTE E 2018 & GT 2021": 2.3970573789339786, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 1.3559230155096402, "TOYOTA C-HR HYBRID 2018": 0.8910235787356033}, "FRICTION": {"HONDA PILOT 2017": 0.2168217463499328, "HONDA CIVIC 2016": 0.28406761310944795, "TOYOTA CAMRY 2018": 0.1327947477896041, "TOYOTA COROLLA HYBRID TSS2 2019": 0.21792021497538405, "TOYOTA RAV4 2019": 0.12757022360707945, "HYUNDAI PALISADE 2020": 0.13391574986922777, "TOYOTA SIENNA 2018": 0.1853443239485906, "ACURA RDX 2020": 0.18058553315570297, "TOYOTA RAV4 2017": 0.14319170324556796, "HONDA RIDGELINE 2017": 0.2380553573913589, "TOYOTA PRIUS 2017": 0.2079869382946584, "TOYOTA HIGHLANDER HYBRID 2020": 0.14038812589302646, "HYUNDAI SONATA 2020": 0.08266051305053168, "KIA STINGER GT2 2018": 0.11909534626930472, "TOYOTA HIGHLANDER 2020": 0.14658637853444048, "HONDA ACCORD 2018": 0.21616610462729247, "TOYOTA PRIUS TSS2 2021": 0.20613763260512002, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.16250373743651828, "LEXUS NX 2020": 0.14404022591302845, "TOYOTA RAV4 HYBRID 2019": 0.1319247989758836, "HONDA CIVIC (BOSCH) 2019": 0.2575217845562353, "KIA NIRO HYBRID 2021": 0.14468633728800306, "HONDA ACCORD HYBRID 2018": 0.21150723931119184, "LEXUS NX HYBRID 2018": 0.16117151597250162, "TOYOTA COROLLA TSS2 2019": 0.21045927995242877, "VOLKSWAGEN ARTEON 1ST GEN": 0.17828895368353925, "TOYOTA CAMRY HYBRID 2021": 0.16283734136957057, "VOLKSWAGEN JETTA 7TH GEN": 0.19508489725001105, "HONDA INSIGHT 2019": 0.25750800088299297, "SUBARU FORESTER 2019": 0.11783702069698135, "HYUNDAI ELANTRA 2021": 0.09377564130711125, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.14740189509875762, "HYUNDAI KONA HYBRID 2020": 0.0863709736632968, "HONDA ODYSSEY 2018": 0.2125595696498247, "LEXUS RX 2016": 0.21475140622981923, "TOYOTA COROLLA 2017": 0.12325064090161544, "LEXUS ES 2019": 0.12757526660498053, "HYUNDAI SANTA FE 2019": 0.12230125806479573, "TOYOTA AVALON 2022": 0.11030226705639488, "JEEP GRAND CHEROKEE V6 2018": 0.12871972792344108, "CHEVROLET VOLT PREMIER 2017": 0.16697256960295873, "TOYOTA RAV4 HYBRID 2017": 0.14074453855329072, "LEXUS RX 2020": 0.2249895411716623, "TOYOTA HIGHLANDER HYBRID 2018": 0.16692807938039034, "TOYOTA CAMRY HYBRID 2018": 0.13418904852016877, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.19324413131475543, "TOYOTA MIRAI 2021": 0.20035237756713503, "LEXUS IS 2018": 0.073103111226694, "TOYOTA HIGHLANDER 2017": 0.17502689439420385, "HYUNDAI SONATA HYBRID 2021": 0.09518615688045734, "VOLKSWAGEN ATLAS 1ST GEN": 0.12761803335799474, "LEXUS ES HYBRID 2019": 0.1682771025433274, "HYUNDAI GENESIS 2015-2016": 0.10254237048034251, "JEEP GRAND CHEROKEE 2019": 0.15702739382013717, "SUBARU ASCENT LIMITED 2019": 0.12936982863095342, "HONDA CR-V 2017": 0.22518506713451308, "HONDA FIT 2018": 0.10803295063463647, "TOYOTA CAMRY 2021": 0.15512845523424743, "AUDI Q3 2ND GEN": 0.14083949977629878, "AUDI A3 3RD GEN": 0.1611945965384188, "LEXUS RX HYBRID 2017": 0.19322020114452776, "HONDA CIVIC 2022": 0.24279247053469405, "GENESIS G70 2018": 0.06869638264150804, "CHRYSLER PACIFICA HYBRID 2018": 0.13887505891474383, "VOLKSWAGEN PASSAT 8TH GEN": 0.21714039653367842, "HONDA CR-V 2016": 0.41726236462791455, "HYUNDAI IONIQ PHEV 2020": 0.13800461817330806, "GMC ACADIA DENALI 2018": 0.3447163106452739, "HYUNDAI SONATA 2019": 0.15371520337813344, "TOYOTA AVALON 2019": 0.10392921606262978, "TOYOTA C-HR 2018": 0.2015190716953846, "HONDA CR-V HYBRID 2019": 0.19595630321202379, "CHRYSLER PACIFICA 2020": 0.14337114313208268, "HYUNDAI IONIQ ELECTRIC 2020": 0.08104502306679212, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.1471001686544422, "KIA OPTIMA SX 2019 & 2016": 0.11703652166984638, "TOYOTA AVALON HYBRID 2019": 0.10863628402866225, "TOYOTA RAV4 HYBRID 2022": 0.14334213238415072, "HONDA PASSPORT 2021": 0.19826160782809032, "KIA K5 2021": 0.1027179720106188, "ACURA ILX 2016": 0.35663988815912573, "HYUNDAI IONIQ HYBRID 2017-2019": 0.12332151728479951, "KIA NIRO EV 2020": 0.0892074288578785, "SUBARU IMPREZA SPORT 2020": 0.15841234487251604, "CHRYSLER PACIFICA HYBRID 2017": 0.1345638758810282, "HYUNDAI KONA ELECTRIC 2019": 0.08503096350356723, "HYUNDAI ELANTRA HYBRID 2021": 0.09887804390243872, "HYUNDAI SANTA FE HYBRID 2022": 0.11171499761140577, "CHRYSLER PACIFICA 2018": 0.13611561752951415, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 0.10502695501512567, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.21818156330777305, "MAZDA CX-9 2021": 0.1793735649504697, "HYUNDAI SANTA FE 2022": 0.09184808719698756, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.14050744688135813, "HONDA HRV 2019": 0.17840321248608593, "TOYOTA AVALON HYBRID 2022": 0.16159049452515487, "SUBARU IMPREZA LIMITED 2019": 0.20322553080306893, "GENESIS G80 2017": 0.07934444681782107, "VOLKSWAGEN TAOS 1ST GEN": 0.18276122764341485, "KIA FORTE E 2018 & GT 2021": 0.11406160665068436, "CADILLAC ESCALADE ESV 2016": 0.15063766975884627, "TOYOTA C-HR 2021": 0.22798633346500694, "TOYOTA C-HR HYBRID 2018": 0.2036375866375624}, "ERROR_RATIO": {"HONDA PILOT 2017": 0.6158457247286419, "HONDA CIVIC 2016": 2.0785618623350928, "TOYOTA CAMRY 2018": 0.17356565057429169, "TOYOTA COROLLA HYBRID TSS2 2019": 0.10094741777075293, "TOYOTA RAV4 2019": 0.11812042718338775, "HYUNDAI PALISADE 2020": 0.30639442561268304, "TOYOTA SIENNA 2018": 0.1117307389748361, "ACURA RDX 2020": 1.9801454495960717, "TOYOTA RAV4 2017": 0.08589486116378196, "HONDA RIDGELINE 2017": 0.4319851914417577, "TOYOTA PRIUS 2017": 0.17281316158588575, "TOYOTA HIGHLANDER HYBRID 2020": 0.046325388721577, "HYUNDAI SONATA 2020": 0.4109860794021653, "KIA STINGER GT2 2018": 0.3517628781488943, "TOYOTA HIGHLANDER 2020": 0.14155072865224166, "HONDA ACCORD 2018": 2.510398061115294, "TOYOTA PRIUS TSS2 2021": 0.13593456264106363, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.08794943266738546, "LEXUS NX 2020": 0.3743942573190866, "TOYOTA RAV4 HYBRID 2019": 0.0845492503791727, "HONDA CIVIC (BOSCH) 2019": 2.4329816697390063, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 2.9252406767451804, "LEXUS NX HYBRID 2018": 0.23060712246809048, "TOYOTA COROLLA TSS2 2019": 0.13822363784977285, "VOLKSWAGEN ARTEON 1ST GEN": 0.009661691674299285, "TOYOTA CAMRY HYBRID 2021": 0.029451711159377333, "VOLKSWAGEN JETTA 7TH GEN": 0.16591473170144055, "HONDA INSIGHT 2019": 1.3398692842898896, "SUBARU FORESTER 2019": 0.5269683780697442, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.02971857401969039, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 1.0245957242729038, "LEXUS RX 2016": 0.07392586589971588, "TOYOTA COROLLA 2017": 0.31336988069649124, "LEXUS ES 2019": 0.08933657038050916, "HYUNDAI SANTA FE 2019": 0.2276812089092099, "TOYOTA AVALON 2022": 0.07120118798045925, "JEEP GRAND CHEROKEE V6 2018": 0.2065164316228118, "CHEVROLET VOLT PREMIER 2017": 0.2316223989408518, "TOYOTA RAV4 HYBRID 2017": 0.055653752888652736, "LEXUS RX 2020": 0.047792182371008345, "TOYOTA HIGHLANDER HYBRID 2018": 0.019259474082467202, "TOYOTA CAMRY HYBRID 2018": 0.11949733140330816, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.1996863736436734, "TOYOTA MIRAI 2021": 0.11019259478417197, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 0.05279963713251727, "HYUNDAI SONATA HYBRID 2021": 0.3543918194389536, "VOLKSWAGEN ATLAS 1ST GEN": 0.21694647502209782, "LEXUS ES HYBRID 2019": 0.14775474433507507, "HYUNDAI GENESIS 2015-2016": 0.0814892037361157, "JEEP GRAND CHEROKEE 2019": 0.3126997097753535, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 5.652613829506629, "HONDA FIT 2018": 1.5217432826711779, "TOYOTA CAMRY 2021": 0.07910435053686729, "AUDI Q3 2ND GEN": 0.13535089102138698, "AUDI A3 3RD GEN": 0.14353941401245793, "LEXUS RX HYBRID 2017": 0.048663813961824696, "HONDA CIVIC 2022": 1.0748206908458815, "GENESIS G70 2018": 0.688303429295532, "CHRYSLER PACIFICA HYBRID 2018": 0.11083725786301112, "VOLKSWAGEN PASSAT 8TH GEN": 0.13315924904555493, "HONDA CR-V 2016": 0.488871482749128, "HYUNDAI IONIQ PHEV 2020": 0.2756096845519595, "GMC ACADIA DENALI 2018": 0.24055364003040136, "HYUNDAI SONATA 2019": 0.4473315280277132, "TOYOTA AVALON 2019": 0.026428086100632363, "TOYOTA C-HR 2018": 0.06075105822970755, "HONDA CR-V HYBRID 2019": 2.9906016360828276, "CHRYSLER PACIFICA 2020": 0.07748732608487266, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.16318394527060903, "KIA OPTIMA SX 2019 & 2016": 0.4378756841929454, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 0.36478548056633514, "HONDA PASSPORT 2021": 0.5993860184637646, "KIA K5 2021": 0.6271500841947655, "ACURA ILX 2016": 0.8435442647921855, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 0.40355577782011604, "SUBARU IMPREZA SPORT 2020": 0.11071291640854522, "CHRYSLER PACIFICA HYBRID 2017": 0.029812269495458284, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 0.01705753895996445, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.05790668871480552, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 0.018126919430513307, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.1331760659016062, "HONDA HRV 2019": 1.599688433820939, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.2514545160390271, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 0.09725484306423876, "KIA FORTE E 2018 & GT 2021": 0.20381871942480628, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 0.05016813984196128, "TOYOTA C-HR HYBRID 2018": 0.2521485862766935}} \ No newline at end of file diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 9440c73f5d8bf1..e737b4d2e7a441 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -74,7 +74,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelSpeedFactor = 1.035 tire_stiffness_factor = 0.5533 ret.mass = 4481. * CV.LB_TO_KG + STD_CARGO_KG # mean between min and max - ret.maxLateralAccel = 1.4 set_lat_tune(ret.lateralTuning, LatTunes.PID_C) elif candidate in (CAR.CHR, CAR.CHRH): @@ -83,7 +82,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 13.6 tire_stiffness_factor = 0.7933 ret.mass = 3300. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 1.3 set_lat_tune(ret.lateralTuning, LatTunes.PID_F) elif candidate in (CAR.CAMRY, CAR.CAMRYH, CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2): @@ -96,7 +94,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.maxLateralAccel = 2.4 set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.05) else: - ret.maxLateralAccel = 2.0 set_lat_tune(ret.lateralTuning, LatTunes.PID_C) elif candidate in (CAR.HIGHLANDER_TSS2, CAR.HIGHLANDERH_TSS2): @@ -105,7 +102,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 16.0 tire_stiffness_factor = 0.8 ret.mass = 4700. * CV.LB_TO_KG + STD_CARGO_KG # 4260 + 4-5 people - ret.maxLateralAccel = 2.0 set_lat_tune(ret.lateralTuning, LatTunes.PID_G) elif candidate in (CAR.HIGHLANDER, CAR.HIGHLANDERH): @@ -114,7 +110,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 16.0 tire_stiffness_factor = 0.8 ret.mass = 4607. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid limited - ret.maxLateralAccel = 1.8 set_lat_tune(ret.lateralTuning, LatTunes.PID_G) elif candidate in (CAR.AVALON, CAR.AVALON_2019, CAR.AVALONH_2019, CAR.AVALON_TSS2, CAR.AVALONH_TSS2): @@ -125,7 +120,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.8 # Found at https://pressroom.toyota.com/releases/2016+avalon+product+specs.download tire_stiffness_factor = 0.7983 ret.mass = 3505. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid - ret.maxLateralAccel = 1.6 set_lat_tune(ret.lateralTuning, LatTunes.PID_H) elif candidate in (CAR.RAV4_TSS2, CAR.RAV4_TSS2_2022, CAR.RAV4H_TSS2, CAR.RAV4H_TSS2_2022): @@ -134,7 +128,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.3 tire_stiffness_factor = 0.7933 ret.mass = 3585. * CV.LB_TO_KG + STD_CARGO_KG # Average between ICE and Hybrid - ret.maxLateralAccel = 2.5 set_lat_tune(ret.lateralTuning, LatTunes.PID_D) # 2019+ RAV4 TSS2 uses two different steering racks and specific tuning seems to be necessary. @@ -159,7 +152,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 16.0 # not optimized tire_stiffness_factor = 0.444 # not optimized yet ret.mass = 3677. * CV.LB_TO_KG + STD_CARGO_KG # mean between min and max - ret.maxLateralAccel = 2.2 set_lat_tune(ret.lateralTuning, LatTunes.PID_D) elif candidate == CAR.SIENNA: @@ -168,7 +160,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 15.5 tire_stiffness_factor = 0.444 ret.mass = 4590. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 1.6 set_lat_tune(ret.lateralTuning, LatTunes.PID_J) elif candidate in (CAR.LEXUS_IS, CAR.LEXUS_RC): @@ -192,7 +183,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.7 tire_stiffness_factor = 0.444 # not optimized yet ret.mass = 4070 * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 2.0 set_lat_tune(ret.lateralTuning, LatTunes.PID_C) elif candidate == CAR.PRIUS_TSS2: @@ -201,7 +191,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 13.4 # True steerRatio from older prius tire_stiffness_factor = 0.6371 # hand-tune ret.mass = 3115. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 2.0 set_lat_tune(ret.lateralTuning, LatTunes.PID_N) elif candidate == CAR.MIRAI: @@ -210,7 +199,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 14.8 tire_stiffness_factor = 0.8 ret.mass = 4300. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 2.4 set_lat_tune(ret.lateralTuning, LatTunes.PID_C) elif candidate in (CAR.ALPHARD_TSS2, CAR.ALPHARDH_TSS2): diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 90bbd6d8893242..3a0d7c8ce852fc 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -64,17 +64,14 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa elif candidate == CAR.ATLAS_MK1: ret.mass = 2011 + STD_CARGO_KG ret.wheelbase = 2.98 - ret.maxLateralAccel = 1.4 elif candidate == CAR.GOLF_MK7: ret.mass = 1397 + STD_CARGO_KG ret.wheelbase = 2.62 - ret.maxLateralAccel = 1.5 elif candidate == CAR.JETTA_MK7: ret.mass = 1328 + STD_CARGO_KG ret.wheelbase = 2.71 - ret.maxLateralAccel = 1.2 elif candidate == CAR.PASSAT_MK8: ret.mass = 1551 + STD_CARGO_KG @@ -95,7 +92,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa elif candidate == CAR.TIGUAN_MK2: ret.mass = 1715 + STD_CARGO_KG ret.wheelbase = 2.74 - ret.maxLateralAccel = 1.1 elif candidate == CAR.TOURAN_MK2: ret.mass = 1516 + STD_CARGO_KG @@ -113,7 +109,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa elif candidate == CAR.AUDI_A3_MK3: ret.mass = 1335 + STD_CARGO_KG ret.wheelbase = 2.61 - ret.maxLateralAccel = 1.7 elif candidate == CAR.AUDI_Q2_MK1: ret.mass = 1205 + STD_CARGO_KG @@ -122,7 +117,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa elif candidate == CAR.AUDI_Q3_MK2: ret.mass = 1623 + STD_CARGO_KG ret.wheelbase = 2.68 - ret.maxLateralAccel = 1.6 elif candidate == CAR.SEAT_ATECA_MK1: ret.mass = 1900 + STD_CARGO_KG diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 2f425709341d58..298de7999a1cda 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -f301bdf52b271c8c4ef0f2bce3be9c8f90037652 +935abbc21282e10572c9324382feba3fee2fe379 \ No newline at end of file From f398b3efc92f59f407379d38ead0f99bf814b95c Mon Sep 17 00:00:00 2001 From: AlexandreSato <66435071+AlexandreSato@users.noreply.github.com> Date: Fri, 10 Jun 2022 22:45:32 -0300 Subject: [PATCH 041/436] Fix Lexus NX Hybrid 2020 engine ecu (#24817) Wrong address in engine ecu. --- selfdrive/car/toyota/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 7e6cfa0d09f1ca..2c151266cff82c 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1585,7 +1585,7 @@ class ToyotaCarInfo(CarInfo): ], }, CAR.LEXUS_NXH_TSS2: { - (Ecu.engine, 0x700, None): [ + (Ecu.engine, 0x7e0, None): [ b'\x0237887000\x00\x00\x00\x00\x00\x00\x00\x00A4701000\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ From 88a100435f1da9bf061b360e8a5cd02e414d6e8b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 10 Jun 2022 22:52:34 -0700 Subject: [PATCH 042/436] compatibility docs: auto-generate star descriptions (#24809) * Auto-generate star descriptions * Need this for the website * And this * required changes to make the website generation work * better names * Revert "better names" This reverts commit be7dbbb5d846d7d55a1ad69533945e6a6c8a0b7c. * simpler --- selfdrive/car/CARS_template.md | 29 ++++++++-------------------- selfdrive/car/docs.py | 4 ++-- selfdrive/car/docs_definitions.py | 32 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index f93dc64b06c4f2..891445a558705d 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -14,28 +14,15 @@ Cars are organized into three tiers: How We Rate The Cars --- -### openpilot Adaptive Cruise Control (ACC) -- {{star_icon.format(Star.FULL.value)}} - openpilot is able to control the gas and brakes. -- {{star_icon.format(Star.HALF.value)}} - openpilot is able to control the gas and brakes with some restrictions. -- {{star_icon.format(Star.EMPTY.value)}} - The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system. - -### Stop and Go -- {{star_icon.format(Star.FULL.value)}} - Adaptive Cruise Control (ACC) operates down to 0 mph. -- {{star_icon.format(Star.EMPTY.value)}} - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed. - -### Steer to 0 -- {{star_icon.format(Star.FULL.value)}} - openpilot can control the steering wheel down to 0 mph. -- {{star_icon.format(Star.EMPTY.value)}} - No steering control below certain speeds. - -### Steering Torque -- {{star_icon.format(Star.FULL.value)}} - Car has enough steering torque to take tighter turns. -- {{star_icon.format(Star.HALF.value)}} - Car has enough steering torque for comfortable highway driving. -- {{star_icon.format(Star.EMPTY.value)}} - Limited ability to make turns. - -### Actively Maintained -- {{star_icon.format(Star.FULL.value)}} - Mainline software support, harness hardware sold by comma, lots of users, primary development target. -- {{star_icon.format(Star.EMPTY.value)}} - Low user count, community maintained, harness hardware not sold by comma. +{% for star_row in star_descriptions.values() %} +{% for name, stars in star_row.items() %} +### {{name}} +{% for star, description in stars %} +- {{star_icon.format(star)}} - {{description}} +{% endfor %} +{% endfor %} +{% endfor %} **All supported cars can move between the tiers as support changes.** {% for tier, cars in tiers.items() %} diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 899f2c087893ac..860503dbdde74d 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -7,7 +7,7 @@ from typing import Dict, List from common.basedir import BASEDIR -from selfdrive.car.docs_definitions import CarInfo, Column, Star, Tier +from selfdrive.car.docs_definitions import STAR_DESCRIPTIONS, CarInfo, Column, Star, Tier from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR from selfdrive.car.tests.routes import non_tested_cars @@ -65,7 +65,7 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: footnotes = [fn.value.text for fn in ALL_FOOTNOTES] cars_md: str = template.render(tiers=sort_by_tier(all_car_info), all_car_info=all_car_info, - footnotes=footnotes, Star=Star, Column=Column) + footnotes=footnotes, Star=Star, Column=Column, star_descriptions=STAR_DESCRIPTIONS) return cars_md diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 3700a248355be1..f09dad78601093 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -150,3 +150,35 @@ class Harness(Enum): nissan_b = "Nissan B" mazda = "Mazda" none = "None" + + +STAR_DESCRIPTIONS = { + "Gas & Brakes": { # icon and row name + "openpilot Adaptive Cruise Control (ACC)": [ # star column + [Star.FULL.value, "openpilot is able to control the gas and brakes."], + [Star.HALF.value, "openpilot is able to control the gas and brakes with some restrictions."], + [Star.EMPTY.value, "The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system."], + ], + Column.FSR_LONGITUDINAL.value: [ + [Star.FULL.value, "Adaptive Cruise Control (ACC) operates down to 0 mph."], + [Star.EMPTY.value, "Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed."], + ], + }, + "Steering": { + Column.FSR_STEERING.value: [ + [Star.FULL.value, "openpilot can control the steering wheel down to 0 mph."], + [Star.EMPTY.value, "No steering control below certain speeds."], + ], + Column.STEERING_TORQUE.value: [ + [Star.FULL.value, "Car has enough steering torque to take tighter turns."], + [Star.HALF.value, "Car has enough steering torque for comfortable highway driving."], + [Star.EMPTY.value, "Limited ability to make turns."], + ], + }, + "Support": { + Column.MAINTAINED.value: [ + [Star.FULL.value, "Mainline software support, harness hardware sold by comma, lots of users, primary development target."], + [Star.EMPTY.value, "Low user count, community maintained, harness hardware not sold by comma."], + ], + }, +} From 843e59f6f0be33e6c51a35ef8912f5b7c4b58ca5 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Fri, 10 Jun 2022 22:52:48 -0700 Subject: [PATCH 043/436] Misc torque control fixes (#24801) * Fiction compensation should be based on error * Update refs * Add deadzone * update ref --- cereal | 2 +- selfdrive/car/toyota/interface.py | 2 +- selfdrive/car/toyota/tunes.py | 4 ++-- selfdrive/controls/lib/drive_helpers.py | 16 +++++++++++++--- selfdrive/controls/lib/latcontrol_torque.py | 20 +++++++++++--------- selfdrive/controls/lib/longcontrol.py | 12 +----------- selfdrive/test/process_replay/ref_commit | 2 +- 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/cereal b/cereal index ff49c8e126ea04..a197e38c0cef00 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit ff49c8e126ea04889af13d690ceaf520ce7084d2 +Subproject commit a197e38c0cef00ececf4c7500438d37659d9199e diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index e737b4d2e7a441..83eed611c31f42 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -39,7 +39,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl tire_stiffness_factor = 0.6371 # hand-tune ret.mass = 3045. * CV.LB_TO_KG + STD_CARGO_KG ret.maxLateralAccel = 1.7 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.06) + set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.06, steering_angle_deadzone_deg=1.0) elif candidate == CAR.PRIUS_V: stop_and_go = True diff --git a/selfdrive/car/toyota/tunes.py b/selfdrive/car/toyota/tunes.py index e97ed6b056792a..3de6daae21b468 100644 --- a/selfdrive/car/toyota/tunes.py +++ b/selfdrive/car/toyota/tunes.py @@ -50,9 +50,9 @@ def set_long_tune(tune, name): ###### LAT ###### -def set_lat_tune(tune, name, MAX_LAT_ACCEL=2.5, FRICTION=0.01, use_steering_angle=True): +def set_lat_tune(tune, name, MAX_LAT_ACCEL=2.5, FRICTION=0.01, steering_angle_deadzone_deg=0.0, use_steering_angle=True): if name == LatTunes.TORQUE: - set_torque_tune(tune, MAX_LAT_ACCEL, FRICTION) + set_torque_tune(tune, MAX_LAT_ACCEL, FRICTION, steering_angle_deadzone_deg) elif 'PID' in str(name): tune.init('pid') tune.pid.kiBP = [0.0] diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 91981259aa04a2..1fecdd7c638a1a 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -38,6 +38,16 @@ class MPC_COST_LAT: STEER_RATE = 1.0 +def apply_deadzone(error, deadzone): + if error > deadzone: + error -= deadzone + elif error < - deadzone: + error += deadzone + else: + error = 0. + return error + + def rate_limit(new_value, last_value, dw_step, up_step): return clip(new_value, last_value + dw_step, last_value + up_step) @@ -98,10 +108,10 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): curvatures = [0.0]*CONTROL_N curvature_rates = [0.0]*CONTROL_N v_ego = max(v_ego, 0.1) - + # TODO this needs more thought, use .2s extra for now to estimate other delays delay = CP.steerActuatorDelay + .2 - + # MPC can plan to turn the wheel and turn back before t_delay. This means # in high delay cases some corrections never even get commanded. So just use # psi to calculate a simple linearization of desired curvature @@ -109,7 +119,7 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): psi = interp(delay, T_IDXS[:CONTROL_N], psis) average_curvature_desired = psi / (v_ego * delay) desired_curvature = 2 * average_curvature_desired - current_curvature_desired - + # This is the "desired rate of the setpoint" not an actual desired rate desired_curvature_rate = curvature_rates[0] max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 47f0b69f2bda76..12714082a61eb9 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -4,6 +4,7 @@ from common.numpy_fast import interp from selfdrive.controls.lib.latcontrol import LatControl, MIN_STEER_SPEED from selfdrive.controls.lib.pid import PIDController +from selfdrive.controls.lib.drive_helpers import apply_deadzone from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY # At higher speeds (25+mph) we can assume: @@ -22,13 +23,14 @@ JERK_THRESHOLD = 0.2 -def set_torque_tune(tune, MAX_LAT_ACCEL=2.5, FRICTION=0.01): +def set_torque_tune(tune, MAX_LAT_ACCEL=2.5, FRICTION=0.01, steering_angle_deadzone_deg=0.0): tune.init('torque') tune.torque.useSteeringAngle = True tune.torque.kp = 1.0 / MAX_LAT_ACCEL tune.torque.kf = 1.0 / MAX_LAT_ACCEL tune.torque.ki = 0.1 / MAX_LAT_ACCEL tune.torque.friction = FRICTION + tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg class LatControlTorque(LatControl): @@ -40,10 +42,7 @@ def __init__(self, CP, CI): self.use_steering_angle = CP.lateralTuning.torque.useSteeringAngle self.friction = CP.lateralTuning.torque.friction self.kf = CP.lateralTuning.torque.kf - - def reset(self): - super().reset() - self.pid.reset() + self.steering_angle_deadzone_deg = CP.lateralTuning.torque.steeringAngleDeadzoneDeg def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): pid_log = log.ControlsState.LateralTorqueState.new_message() @@ -54,24 +53,27 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi else: if self.use_steering_angle: actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) else: actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) actual_curvature_llk = llk.angularVelocityCalibrated.value[2] / CS.vEgo actual_curvature = interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_llk]) + curvature_deadzone = 0.0 desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - + # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature - desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 + #desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 actual_lateral_accel = actual_curvature * CS.vEgo ** 2 + lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 setpoint = desired_lateral_accel + LOW_SPEED_FACTOR * desired_curvature measurement = actual_lateral_accel + LOW_SPEED_FACTOR * actual_curvature - error = setpoint - measurement + error = apply_deadzone(setpoint - measurement, lateral_accel_deadzone) pid_log.error = error ff = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY # convert friction into lateral accel units for feedforward - friction_compensation = interp(desired_lateral_jerk, [-JERK_THRESHOLD, JERK_THRESHOLD], [-self.friction, self.friction]) + friction_compensation = interp(error, [-JERK_THRESHOLD, JERK_THRESHOLD], [-self.friction, self.friction]) ff += friction_compensation / self.kf freeze_integrator = CS.steeringRateLimited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(error, diff --git a/selfdrive/controls/lib/longcontrol.py b/selfdrive/controls/lib/longcontrol.py index f2cd28219812fe..e9458607d5d1c0 100644 --- a/selfdrive/controls/lib/longcontrol.py +++ b/selfdrive/controls/lib/longcontrol.py @@ -1,7 +1,7 @@ from cereal import car from common.numpy_fast import clip, interp from common.realtime import DT_CTRL -from selfdrive.controls.lib.drive_helpers import CONTROL_N +from selfdrive.controls.lib.drive_helpers import CONTROL_N, apply_deadzone from selfdrive.controls.lib.pid import PIDController from selfdrive.modeld.constants import T_IDXS @@ -12,16 +12,6 @@ ACCEL_MAX_ISO = 2.0 # m/s^2 -def apply_deadzone(error, deadzone): - if error > deadzone: - error -= deadzone - elif error < - deadzone: - error += deadzone - else: - error = 0. - return error - - def long_control_state_trans(CP, active, long_control_state, v_ego, v_target, v_target_future, brake_pressed, cruise_standstill): """Update longitudinal control state machine""" diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 298de7999a1cda..613b7f26aef06b 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -935abbc21282e10572c9324382feba3fee2fe379 \ No newline at end of file +123506cad1877e93bfe5c91ecdce654ef339959b From e3750877202a072e884ad0fb88709b23226c3a59 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 11 Jun 2022 00:14:58 -0700 Subject: [PATCH 044/436] Honda carcontroller and signal cleanup (#24806) * common signals * move stopping * space * clean up * bump opendbc --- opendbc | 2 +- selfdrive/car/honda/carcontroller.py | 27 ++++----------- selfdrive/car/honda/hondacan.py | 49 ++++++++++++---------------- 3 files changed, 29 insertions(+), 49 deletions(-) diff --git a/opendbc b/opendbc index ec0e1f20bae4c3..58a2c9b2fc5c60 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit ec0e1f20bae4c39326036c0061418404ac15ae9e +Subproject commit 58a2c9b2fc5c60ca68d3dced09f6c4c72ca72415 diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index cf01fa2aaa58fe..a6aa84adf6a6d4 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -95,8 +95,8 @@ def process_hud_alert(hud_alert): HUDData = namedtuple("HUDData", - ["pcm_accel", "v_cruise", "car", - "lanes", "fcw", "acc_alert", "steer_required"]) + ["pcm_accel", "v_cruise", "lead_visible", + "lanes_visible", "fcw", "acc_alert", "steer_required"]) class CarController: @@ -138,19 +138,6 @@ def update(self, CC, CS): self.brake_last = rate_limit(pre_limit_brake, self.brake_last, -2., DT_CTRL) # vehicle hud display, wait for one update from 10Hz 0x304 msg - if hud_control.lanesVisible: - hud_lanes = 1 - else: - hud_lanes = 0 - - if CC.enabled: - if hud_control.leadVisible: - hud_car = 2 - else: - hud_car = 1 - else: - hud_car = 0 - fcw_display, steer_required, acc_alert = process_hud_alert(hud_control.visualAlert) # **** process the car messages **** @@ -172,8 +159,6 @@ def update(self, CC, CS): can_sends.append(hondacan.create_steering_control(self.packer, apply_steer, CC.latActive, self.CP.carFingerprint, idx, CS.CP.openpilotLongitudinalControl)) - stopping = actuators.longControlState == LongCtrlState.stopping - # wind brake from air resistance decel at high speed wind_brake = interp(CS.out.vEgo, [0.0, 2.3, 35.0], [0.001, 0.002, 0.15]) # all of this is only relevant for HONDA NIDEC @@ -222,6 +207,8 @@ def update(self, CC, CS): if self.CP.carFingerprint in HONDA_BOSCH: self.accel = clip(accel, self.params.BOSCH_ACCEL_MIN, self.params.BOSCH_ACCEL_MAX) self.gas = interp(accel, self.params.BOSCH_GAS_LOOKUP_BP, self.params.BOSCH_GAS_LOOKUP_V) + + stopping = actuators.longControlState == LongCtrlState.stopping can_sends.extend(hondacan.create_acc_commands(self.packer, CC.enabled, CC.longActive, self.accel, self.gas, idx, stopping, self.CP.carFingerprint)) else: @@ -252,9 +239,9 @@ def update(self, CC, CS): # Send dashboard UI commands. if self.frame % 10 == 0: idx = (self.frame // 10) % 4 - hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_car, - hud_lanes, fcw_display, acc_alert, steer_required) - can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud)) + hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_control.leadVisible, + hud_control.lanesVisible, fcw_display, acc_alert, steer_required) + can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud)) if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH: self.speed = pcm_speed diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index dcdc0e5f94d7c5..9c9e873e18ba63 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -98,47 +98,40 @@ def create_bosch_supplemental_1(packer, car_fingerprint, idx): return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values, idx) -def create_ui_commands(packer, CP, pcm_speed, hud, is_metric, idx, stock_hud): +def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud): commands = [] bus_pt = get_pt_bus(CP.carFingerprint) radar_disabled = CP.carFingerprint in HONDA_BOSCH and CP.openpilotLongitudinalControl bus_lkas = get_lkas_cmd_bus(CP.carFingerprint, radar_disabled) if CP.openpilotLongitudinalControl: + acc_hud_values = { + 'CRUISE_SPEED': hud.v_cruise, + 'ENABLE_MINI_CAR': 1, + 'HUD_DISTANCE': 3, # max distance setting on display + 'IMPERIAL_UNIT': int(not is_metric), + 'HUD_LEAD': 2 if enabled and hud.lead_visible else 1 if enabled else 0, + 'SET_ME_X01_2': 1, + } + if CP.carFingerprint in HONDA_BOSCH: - acc_hud_values = { - 'CRUISE_SPEED': hud.v_cruise, - 'ENABLE_MINI_CAR': 1, - 'SET_TO_1': 1, - 'HUD_LEAD': hud.car, - 'HUD_DISTANCE': 3, - 'ACC_ON': hud.car != 0, - 'SET_TO_X1': 1, - 'IMPERIAL_UNIT': int(not is_metric), - 'FCM_OFF': 1, - } + acc_hud_values['ACC_ON'] = hud.car != 0 + acc_hud_values['FCM_OFF'] = 1 + acc_hud_values['FCM_OFF_2'] = 1 else: - acc_hud_values = { - 'PCM_SPEED': pcm_speed * CV.MS_TO_KPH, - 'PCM_GAS': hud.pcm_accel, - 'CRUISE_SPEED': hud.v_cruise, - 'ENABLE_MINI_CAR': 1, - 'HUD_LEAD': hud.car, - 'HUD_DISTANCE': 3, # max distance setting on display - 'IMPERIAL_UNIT': int(not is_metric), - 'SET_ME_X01_2': 1, - 'SET_ME_X01': 1, - "FCM_OFF": stock_hud["FCM_OFF"], - "FCM_OFF_2": stock_hud["FCM_OFF_2"], - "FCM_PROBLEM": stock_hud["FCM_PROBLEM"], - "ICONS": stock_hud["ICONS"], - } + acc_hud_values['PCM_SPEED'] = pcm_speed * CV.MS_TO_KPH + acc_hud_values['PCM_GAS'] = hud.pcm_accel + acc_hud_values['SET_ME_X01'] = 1 + acc_hud_values['FCM_OFF'] = stock_hud['FCM_OFF'] + acc_hud_values['FCM_OFF_2'] = stock_hud['FCM_OFF_2'] + acc_hud_values['FCM_PROBLEM'] = stock_hud['FCM_PROBLEM'] + acc_hud_values['ICONS'] = stock_hud['ICONS'] commands.append(packer.make_can_msg("ACC_HUD", bus_pt, acc_hud_values, idx)) lkas_hud_values = { 'SET_ME_X41': 0x41, 'STEERING_REQUIRED': hud.steer_required, - 'SOLID_LANES': hud.lanes, + 'SOLID_LANES': hud.lanes_visible, 'BEEP': 0, } From 1847a70a47b12ff5c7e7db9361f69d6d1e79a919 Mon Sep 17 00:00:00 2001 From: Maykon Pacheco <38124066+maykonpacheco@users.noreply.github.com> Date: Sat, 11 Jun 2022 14:08:24 -0400 Subject: [PATCH 045/436] test for the strip_bz2_extension method (#24826) --- selfdrive/athena/tests/test_athenad.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/selfdrive/athena/tests/test_athenad.py b/selfdrive/athena/tests/test_athenad.py index b6457ca01da3d0..a0b308c216c53a 100755 --- a/selfdrive/athena/tests/test_athenad.py +++ b/selfdrive/athena/tests/test_athenad.py @@ -120,6 +120,13 @@ def test_listDataDirectory(self): self.assertTrue(resp, 'list empty!') self.assertCountEqual(resp, expected) + def test_strip_bz2_extension(self): + fn = os.path.join(athenad.ROOT, 'qlog.bz2') + Path(fn).touch() + if fn.endswith('.bz2'): + self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4]) + + @with_http_server def test_do_upload(self, host): fn = os.path.join(athenad.ROOT, 'qlog.bz2') From a029245a784acb9fe3d8db7f1023aab222ec0dba Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 12:20:24 -0700 Subject: [PATCH 046/436] CI: fix docker push for base image --- .github/workflows/selfdrive_tests.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 04ae841f77b063..567fc05aa56e56 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -20,8 +20,7 @@ env: RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/sh -c BUILD_CL: | - docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base_cl) || true - docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true + docker pull $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest || true docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl . RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache $CL_BASE_IMAGE /bin/sh -c @@ -201,12 +200,15 @@ jobs: submodules: true - name: Build Docker image run: eval "$BUILD" + - name: Push to container registry + run: | + $DOCKER_LOGIN + docker push $DOCKER_REGISTRY/$BASE_IMAGE:latest - name: Build CL Docker image run: eval "$BUILD_CL" - name: Push to container registry run: | $DOCKER_LOGIN - docker push $DOCKER_REGISTRY/$BASE_IMAGE:latest docker push $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest static_analysis: From 1c238e8c773eb58f88fc69cdcb77dba71430b735 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 13:06:07 -0700 Subject: [PATCH 047/436] pylint: add PyQt5 support --- .pylintrc | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.pylintrc b/.pylintrc index 73035360ca68fb..27539b743c63ba 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-whitelist=scipy cereal.messaging.messaging_pyx +extension-pkg-whitelist=scipy,cereal.messaging.messaging_pyx,PyQt5 # Add files or directories to the blacklist. They should be base names, not # paths. @@ -250,13 +250,6 @@ max-line-length=100 # Maximum number of lines in a module max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no From 5add0c6159db3810c17f0b27afd3dda882f95586 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Sat, 11 Jun 2022 15:32:12 -0700 Subject: [PATCH 048/436] simulator: run simulator test in ci (#24691) * run simulator test in ci * block navd process * block ui * fix jenkins * build docker * remove tty * remove tty for carla * detach carla_sim * more retries * only build once * add more time for bridge * cleanup * use qt offscreen * expose to docker * block ui * use new dockerimage * fix * from ubuntu20.04 * install curl * add ssh * add locales * noninteractive * syntax * use base * smaller image * add git + git lfs * kill carla * run in parallel * fix missing agents * default agent? * little cleanup * default doesn't work * not in ci * fix path * fix path * new msg Co-authored-by: Adeeb Shihadeh --- Jenkinsfile | 129 ++++++++++-------- tools/sim/Dockerfile.sim | 2 +- tools/sim/Dockerfile.sim_nvidia | 21 +++ tools/sim/launch_openpilot.sh | 4 + tools/sim/lib/can.py | 1 + tools/sim/start_carla.sh | 7 +- tools/sim/start_openpilot_docker.sh | 22 +-- tools/sim/{test => tests}/__init__.py | 0 .../{test => tests}/test_carla_integration.py | 19 ++- 9 files changed, 133 insertions(+), 72 deletions(-) create mode 100644 tools/sim/Dockerfile.sim_nvidia rename tools/sim/{test => tests}/__init__.py (100%) rename tools/sim/{test => tests}/test_carla_integration.py (85%) diff --git a/Jenkinsfile b/Jenkinsfile index 3b134d7c0d8d94..fed587bd2148b9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -42,6 +42,7 @@ def phone_steps(String device_type, steps) { pipeline { agent none environment { + CI = "1" TEST_DIR = "/data/openpilot" SOURCE_DIR = "/data/openpilot_source/" } @@ -74,71 +75,89 @@ pipeline { } } - stages { - stage('On-device Tests') { - agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } - stages { - stage('parallel tests') { - parallel { - stage('build') { - environment { - R3_PUSH = "${env.BRANCH_NAME == 'master' ? '1' : ' '}" - } - steps { - phone_steps("tici", [ - ["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"], - ["build openpilot", "cd selfdrive/manager && ./build.py"], - ["test manager", "python selfdrive/manager/test/test_manager.py"], - ["onroad tests", "cd selfdrive/test/ && ./test_onroad.py"], - ["test car interfaces", "cd selfdrive/car/tests/ && ./test_car_interfaces.py"], - ]) - } - } - - stage('HW + Unit Tests') { - steps { - phone_steps("tici2", [ - ["build", "cd selfdrive/manager && ./build.py"], - ["test power draw", "python selfdrive/hardware/tici/test_power_draw.py"], - ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], - ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], - ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], - ["test sensord", "python selfdrive/sensord/test/test_sensord.py"], - ]) - } - } - - stage('camerad') { - steps { - phone_steps("tici-party", [ - ["build", "cd selfdrive/manager && ./build.py"], - ["test camerad", "python selfdrive/camerad/test/test_camerad.py"], - ["test exposure", "python selfdrive/camerad/test/test_exposure.py"], - ]) - } - } - - stage('replay') { - steps { - phone_steps("tici3", [ - ["build", "cd selfdrive/manager && ./build.py"], - ["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"], - ]) - } - } - - } + parallel { + + stage('simulator') { + agent { + dockerfile { + filename 'Dockerfile.sim_nvidia' + dir 'tools/sim' + args '--user=root' } } + steps { + sh "git config --global --add safe.directory ${WORKSPACE}" + sh "git lfs pull" + sh "${WORKSPACE}/tools/sim/build_container.sh" + sh "DETACH=1 ${WORKSPACE}/tools/sim/start_carla.sh" + sh "${WORKSPACE}/tools/sim/start_openpilot_docker.sh" + } post { always { - cleanWs() + sh "docker kill carla_sim || true" + sh "rm -rf ${WORKSPACE}/* || true" + sh "rm -rf .* || true" } } + } + stage('build') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + environment { + R3_PUSH = "${env.BRANCH_NAME == 'master' ? '1' : ' '}" + } + steps { + phone_steps("tici", [ + ["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"], + ["build openpilot", "cd selfdrive/manager && ./build.py"], + ["test manager", "python selfdrive/manager/test/test_manager.py"], + ["onroad tests", "cd selfdrive/test/ && ./test_onroad.py"], + ["test car interfaces", "cd selfdrive/car/tests/ && ./test_car_interfaces.py"], + ]) + } + } + + stage('HW + Unit Tests') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tici2", [ + ["build", "cd selfdrive/manager && ./build.py"], + ["test power draw", "python selfdrive/hardware/tici/test_power_draw.py"], + ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], + ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], + ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], + ["test sensord", "python selfdrive/sensord/test/test_sensord.py"], + ]) + } } + stage('camerad') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tici-party", [ + ["build", "cd selfdrive/manager && ./build.py"], + ["test camerad", "python selfdrive/camerad/test/test_camerad.py"], + ["test exposure", "python selfdrive/camerad/test/test_exposure.py"], + ]) + } + } + + stage('replay') { + agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } + steps { + phone_steps("tici3", [ + ["build", "cd selfdrive/manager && ./build.py"], + ["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"], + ]) + } + } + } + + post { + always { + cleanWs() + } } } } diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index d869db90a67ade..d838efed133e95 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -27,6 +27,6 @@ COPY ./system $HOME/openpilot/system COPY ./tools $HOME/openpilot/tools WORKDIR $HOME/openpilot -RUN scons -j$(nproc) +RUN scons -j12 RUN python -c "from selfdrive.test.helpers import set_params_enabled; set_params_enabled()" diff --git a/tools/sim/Dockerfile.sim_nvidia b/tools/sim/Dockerfile.sim_nvidia new file mode 100644 index 00000000000000..5e5dd263da9c2b --- /dev/null +++ b/tools/sim/Dockerfile.sim_nvidia @@ -0,0 +1,21 @@ +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + apt-utils \ + sudo \ + ssh \ + curl \ + ca-certificates \ + git \ + git-lfs && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://get.docker.com -o get-docker.sh && \ + sudo sh get-docker.sh && \ + distribution=$(. /etc/os-release;echo $ID$VERSION_ID) && \ + curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - && \ + curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list && \ + sudo apt-get update && \ + sudo apt-get install -y nvidia-docker2 diff --git a/tools/sim/launch_openpilot.sh b/tools/sim/launch_openpilot.sh index 82fc4a71acdcd0..15f45b4cc2e0a1 100755 --- a/tools/sim/launch_openpilot.sh +++ b/tools/sim/launch_openpilot.sh @@ -6,6 +6,10 @@ export SIMULATION="1" export FINGERPRINT="HONDA CIVIC 2016" export BLOCK="camerad,loggerd,encoderd" +if [[ "$CI" ]]; then + # TODO: offscreen UI should work + export BLOCK="${BLOCK},ui" +fi DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd ../../selfdrive/manager && exec ./manager.py diff --git a/tools/sim/lib/can.py b/tools/sim/lib/can.py index af0f415bbc1c97..2b1048fef297e0 100755 --- a/tools/sim/lib/can.py +++ b/tools/sim/lib/can.py @@ -63,6 +63,7 @@ def can_function(pm, speed, angle, idx, cruise_button, is_engaged): msg.append(packer.make_can_msg("SCM_FEEDBACK", 0, {"MAIN_ON": 1}, idx)) msg.append(packer.make_can_msg("POWERTRAIN_DATA", 0, {"ACC_STATUS": int(is_engaged)}, idx)) msg.append(packer.make_can_msg("HUD_SETTING", 0, {})) + msg.append(packer.make_can_msg("CAR_SPEED", 0, {})) # *** cam bus *** msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {}, idx)) diff --git a/tools/sim/start_carla.sh b/tools/sim/start_carla.sh index 912c7d6f083c5e..abdaee4ea8a44c 100755 --- a/tools/sim/start_carla.sh +++ b/tools/sim/start_carla.sh @@ -17,12 +17,17 @@ fi docker pull carlasim/carla:0.9.12 +EXTRA_ARGS="-it" +if [[ "$DETACH" ]]; then + EXTRA_ARGS="-d" +fi + docker run \ --name carla_sim \ --rm \ --gpus all \ --net=host \ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \ - -it \ + $EXTRA_ARGS \ carlasim/carla:0.9.12 \ /bin/bash ./CarlaUE4.sh -opengl -nosound -RenderOffScreen -benchmark -fps=20 -quality-level=Low diff --git a/tools/sim/start_openpilot_docker.sh b/tools/sim/start_openpilot_docker.sh index 106dbdeccbea91..e48e63574d3f45 100755 --- a/tools/sim/start_openpilot_docker.sh +++ b/tools/sim/start_openpilot_docker.sh @@ -3,22 +3,26 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" cd $DIR -# expose X to the container -xhost +local:root - -docker pull ghcr.io/commaai/openpilot-sim:latest - OPENPILOT_DIR="/openpilot" -if ! [[ -z "$MOUNT_OPENPILOT" ]] -then +if ! [[ -z "$MOUNT_OPENPILOT" ]]; then OPENPILOT_DIR="$(dirname $(dirname $DIR))" EXTRA_ARGS="-v $OPENPILOT_DIR:$OPENPILOT_DIR -e PYTHONPATH=$OPENPILOT_DIR:$PYTHONPATH" fi +if [[ "$CI" ]]; then + CMD="CI=1 ${OPENPILOT_DIR}/tools/sim/tests/test_carla_integration.py" +else + # expose X to the container + xhost +local:root + + docker pull ghcr.io/commaai/openpilot-sim:latest + CMD="./tmux_script.sh $*" + EXTRA_ARGS="${EXTRA_ARGS} -it" +fi + docker run --net=host\ --name openpilot_client \ --rm \ - -it \ --gpus all \ --device=/dev/dri:/dev/dri \ --device=/dev/input:/dev/input \ @@ -29,4 +33,4 @@ docker run --net=host\ -w "$OPENPILOT_DIR/tools/sim" \ $EXTRA_ARGS \ ghcr.io/commaai/openpilot-sim:latest \ - /bin/bash -c "./tmux_script.sh $*" + /bin/bash -c "$CMD" diff --git a/tools/sim/test/__init__.py b/tools/sim/tests/__init__.py similarity index 100% rename from tools/sim/test/__init__.py rename to tools/sim/tests/__init__.py diff --git a/tools/sim/test/test_carla_integration.py b/tools/sim/tests/test_carla_integration.py similarity index 85% rename from tools/sim/test/test_carla_integration.py rename to tools/sim/tests/test_carla_integration.py index 8f76abb5616594..43db783f4e38d4 100755 --- a/tools/sim/test/test_carla_integration.py +++ b/tools/sim/tests/test_carla_integration.py @@ -2,13 +2,18 @@ import subprocess import time import unittest +import os from multiprocessing import Queue from cereal import messaging +from common.basedir import BASEDIR from selfdrive.manager.helpers import unblock_stdout from tools.sim import bridge from tools.sim.bridge import CarlaBridge +CI = "CI" in os.environ + +SIM_DIR = os.path.join(BASEDIR, "tools/sim") class TestCarlaIntegration(unittest.TestCase): """ @@ -19,10 +24,12 @@ class TestCarlaIntegration(unittest.TestCase): def setUp(self): self.processes = [] - # We want to make sure that carla_sim docker isn't still running. - subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) - self.carla_process = subprocess.Popen(".././start_carla.sh") + if not CI: + # We want to make sure that carla_sim docker isn't still running. + subprocess.run("docker rm -f carla_sim", shell=True, stderr=subprocess.PIPE, check=False) + self.carla_process = subprocess.Popen("./start_carla.sh", cwd=SIM_DIR) + # Too many lagging messages in bridge.py can cause a crash. This prevents it. unblock_stdout() # Wait 10 seconds to startup carla @@ -30,16 +37,16 @@ def setUp(self): def test_engage(self): # Startup manager and bridge.py. Check processes are running, then engage and verify. - p_manager = subprocess.Popen("./launch_openpilot.sh", cwd='../') + p_manager = subprocess.Popen("./launch_openpilot.sh", cwd=SIM_DIR) self.processes.append(p_manager) sm = messaging.SubMaster(['controlsState', 'carEvents', 'managerState']) q = Queue() carla_bridge = CarlaBridge(bridge.parse_args([])) - p_bridge = carla_bridge.run(q, retries=3) + p_bridge = carla_bridge.run(q, retries=10) self.processes.append(p_bridge) - max_time_per_step = 20 + max_time_per_step = 60 # Wait for bridge to startup start_waiting = time.monotonic() From 1139fe507b01f34de9714c99228f411558b44231 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 16:38:24 -0700 Subject: [PATCH 049/436] Move selfdrive/hardware/ to system/ (#24725) * move hardware to system/ * fix mypy --- Jenkinsfile | 2 +- common/basedir.py | 2 +- common/modeldata.h | 2 +- common/params.cc | 2 +- common/realtime.py | 2 +- common/swaglog.cc | 2 +- common/tests/test_swaglog.cc | 2 +- launch_chffrplus.sh | 6 ++-- release/files_common | 30 +++++++++--------- scripts/disable-powersave.py | 2 +- selfdrive/athena/athenad.py | 2 +- selfdrive/athena/registration.py | 2 +- selfdrive/boardd/boardd.cc | 2 +- selfdrive/boardd/main.cc | 2 +- selfdrive/boardd/pandad.py | 2 +- .../boardd/tests/test_boardd_loopback.py | 2 +- selfdrive/camerad/cameras/camera_common.cc | 2 +- selfdrive/camerad/cameras/camera_common.h | 2 +- selfdrive/camerad/main.cc | 2 +- selfdrive/camerad/snapshot/snapshot.py | 2 +- selfdrive/camerad/test/test_camerad.py | 2 +- selfdrive/camerad/test/test_exposure.py | 2 +- selfdrive/controls/controlsd.py | 2 +- selfdrive/hardware | 1 + selfdrive/loggerd/config.py | 2 +- selfdrive/loggerd/logger.h | 2 +- selfdrive/loggerd/loggerd.h | 2 +- selfdrive/loggerd/tests/test_encoder.py | 2 +- selfdrive/loggerd/uploader.py | 2 +- selfdrive/manager/build.py | 2 +- selfdrive/manager/manager.py | 2 +- selfdrive/manager/process.py | 2 +- selfdrive/manager/process_config.py | 2 +- selfdrive/manager/test/test_manager.py | 2 +- selfdrive/modeld/modeld.cc | 2 +- selfdrive/modeld/models/dmonitoring.cc | 2 +- selfdrive/modeld/thneed/compile.cc | 2 +- selfdrive/sensord/test/test_sensord.py | 2 +- selfdrive/sentry.py | 2 +- selfdrive/statsd.py | 2 +- selfdrive/swaglog.py | 2 +- selfdrive/test/helpers.py | 2 +- selfdrive/test/process_replay/model_replay.py | 2 +- selfdrive/test/process_replay/test_debayer.py | 2 +- selfdrive/thermald/power_monitoring.py | 2 +- selfdrive/thermald/thermald.py | 2 +- selfdrive/timezoned.py | 2 +- selfdrive/ui/main.cc | 2 +- selfdrive/ui/qt/api.cc | 2 +- selfdrive/ui/qt/maps/map_helpers.cc | 2 +- selfdrive/ui/qt/offroad/settings.cc | 2 +- selfdrive/ui/qt/qt_window.h | 2 +- selfdrive/ui/qt/setup/setup.cc | 2 +- selfdrive/ui/qt/setup/updater.cc | 2 +- selfdrive/ui/qt/spinner.cc | 2 +- selfdrive/ui/qt/text.cc | 2 +- selfdrive/ui/qt/util.cc | 2 +- selfdrive/ui/qt/widgets/input.cc | 2 +- selfdrive/ui/qt/widgets/offroad_alerts.cc | 2 +- selfdrive/ui/qt/widgets/ssh_keys.h | 2 +- selfdrive/ui/qt/window.cc | 2 +- selfdrive/ui/replay/replay.cc | 2 +- selfdrive/ui/replay/route.cc | 2 +- selfdrive/ui/soundd/sound.h | 2 +- selfdrive/ui/tests/test_soundd.py | 2 +- selfdrive/ui/ui.cc | 2 +- selfdrive/updated.py | 6 ++-- {selfdrive/hardware/pc => system}/__init__.py | 0 {selfdrive => system}/hardware/.gitignore | 0 {selfdrive => system}/hardware/__init__.py | 6 ++-- {selfdrive => system}/hardware/base.h | 0 {selfdrive => system}/hardware/base.py | 0 {selfdrive => system}/hardware/hw.h | 4 +-- .../tici => system/hardware/pc}/__init__.py | 0 {selfdrive => system}/hardware/pc/hardware.py | 2 +- system/hardware/tici/__init__.py | 0 .../hardware/tici/agnos.json | 0 {selfdrive => system}/hardware/tici/agnos.py | 0 .../hardware/tici/amplifier.py | 0 .../hardware/tici/hardware.h | 2 +- .../hardware/tici/hardware.py | 8 ++--- {selfdrive => system}/hardware/tici/iwlist.py | 0 {selfdrive => system}/hardware/tici/pins.py | 0 .../hardware/tici/power_draw_test.py | 4 +-- .../hardware/tici/power_monitor.py | 0 .../hardware/tici/precise_power_measure.py | 2 +- .../hardware/tici/restart_modem.sh | 0 .../hardware/tici/test_agnos_updater.py | 0 .../hardware/tici/test_power_draw.py | 4 +-- {selfdrive => system}/hardware/tici/updater | Bin tools/lib/auth_config.py | 2 +- 91 files changed, 102 insertions(+), 101 deletions(-) create mode 120000 selfdrive/hardware rename {selfdrive/hardware/pc => system}/__init__.py (100%) rename {selfdrive => system}/hardware/.gitignore (100%) rename {selfdrive => system}/hardware/__init__.py (56%) rename {selfdrive => system}/hardware/base.h (100%) rename {selfdrive => system}/hardware/base.py (100%) rename {selfdrive => system}/hardware/hw.h (92%) rename {selfdrive/hardware/tici => system/hardware/pc}/__init__.py (100%) rename {selfdrive => system}/hardware/pc/hardware.py (96%) create mode 100644 system/hardware/tici/__init__.py rename {selfdrive => system}/hardware/tici/agnos.json (100%) rename {selfdrive => system}/hardware/tici/agnos.py (100%) rename {selfdrive => system}/hardware/tici/amplifier.py (100%) rename {selfdrive => system}/hardware/tici/hardware.h (97%) rename {selfdrive => system}/hardware/tici/hardware.py (98%) rename {selfdrive => system}/hardware/tici/iwlist.py (100%) rename {selfdrive => system}/hardware/tici/pins.py (100%) rename {selfdrive => system}/hardware/tici/power_draw_test.py (97%) rename {selfdrive => system}/hardware/tici/power_monitor.py (100%) rename {selfdrive => system}/hardware/tici/precise_power_measure.py (77%) rename {selfdrive => system}/hardware/tici/restart_modem.sh (100%) rename {selfdrive => system}/hardware/tici/test_agnos_updater.py (100%) rename {selfdrive => system}/hardware/tici/test_power_draw.py (92%) rename {selfdrive => system}/hardware/tici/updater (100%) diff --git a/Jenkinsfile b/Jenkinsfile index fed587bd2148b9..250893d1af2906 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -123,7 +123,7 @@ pipeline { steps { phone_steps("tici2", [ ["build", "cd selfdrive/manager && ./build.py"], - ["test power draw", "python selfdrive/hardware/tici/test_power_draw.py"], + ["test power draw", "python system/hardware/tici/test_power_draw.py"], ["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"], ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], diff --git a/common/basedir.py b/common/basedir.py index 8be1cf6afa729d..371b54d3ef34e4 100644 --- a/common/basedir.py +++ b/common/basedir.py @@ -1,7 +1,7 @@ import os from pathlib import Path -from selfdrive.hardware import PC +from system.hardware import PC BASEDIR = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")) diff --git a/common/modeldata.h b/common/modeldata.h index 06d9398031f1c1..aee9fdfd8376ee 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -2,7 +2,7 @@ #include #include "common/mat.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" const int TRAJECTORY_SIZE = 33; const int LAT_MPC_N = 16; diff --git a/common/params.cc b/common/params.cc index 330eddc1a94a17..b740e5a71efc05 100644 --- a/common/params.cc +++ b/common/params.cc @@ -8,7 +8,7 @@ #include "common/swaglog.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" namespace { diff --git a/common/realtime.py b/common/realtime.py index 03051c6f67caae..8a79d8d39fb099 100644 --- a/common/realtime.py +++ b/common/realtime.py @@ -8,7 +8,7 @@ from setproctitle import getproctitle # pylint: disable=no-name-in-module from common.clock import sec_since_boot # pylint: disable=no-name-in-module, import-error -from selfdrive.hardware import PC +from system.hardware import PC # time step for each process diff --git a/common/swaglog.cc b/common/swaglog.cc index 75223854fe0c2e..6b0028326a7872 100644 --- a/common/swaglog.cc +++ b/common/swaglog.cc @@ -15,7 +15,7 @@ #include "common/util.h" #include "common/version.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" class SwaglogState : public LogState { public: diff --git a/common/tests/test_swaglog.cc b/common/tests/test_swaglog.cc index b54d1d6338cf91..20455ec74c7bbb 100644 --- a/common/tests/test_swaglog.cc +++ b/common/tests/test_swaglog.cc @@ -7,7 +7,7 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/version.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" const char *SWAGLOG_ADDR = "ipc:///tmp/logmessage"; std::string daemon_name = "testy"; diff --git a/launch_chffrplus.sh b/launch_chffrplus.sh index a37e27c4fe4dd4..911774a4eba1df 100755 --- a/launch_chffrplus.sh +++ b/launch_chffrplus.sh @@ -22,12 +22,12 @@ function agnos_init { # Check if AGNOS update is required if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then - AGNOS_PY="$DIR/selfdrive/hardware/tici/agnos.py" - MANIFEST="$DIR/selfdrive/hardware/tici/agnos.json" + AGNOS_PY="$DIR/system/hardware/tici/agnos.py" + MANIFEST="$DIR/system/hardware/tici/agnos.json" if $AGNOS_PY --verify $MANIFEST; then sudo reboot fi - $DIR/selfdrive/hardware/tici/updater $AGNOS_PY $MANIFEST + $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST fi } diff --git a/release/files_common b/release/files_common index 5230511ddd1e62..e89163aba0bd7c 100644 --- a/release/files_common +++ b/release/files_common @@ -182,21 +182,21 @@ selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore selfdrive/controls/lib/lateral_mpc_lib/* selfdrive/controls/lib/longitudinal_mpc_lib/* -selfdrive/hardware/__init__.py -selfdrive/hardware/base.h -selfdrive/hardware/base.py -selfdrive/hardware/hw.h -selfdrive/hardware/tici/__init__.py -selfdrive/hardware/tici/hardware.h -selfdrive/hardware/tici/hardware.py -selfdrive/hardware/tici/pins.py -selfdrive/hardware/tici/agnos.py -selfdrive/hardware/tici/agnos.json -selfdrive/hardware/tici/amplifier.py -selfdrive/hardware/tici/updater -selfdrive/hardware/tici/iwlist.py -selfdrive/hardware/pc/__init__.py -selfdrive/hardware/pc/hardware.py +system/hardware/__init__.py +system/hardware/base.h +system/hardware/base.py +system/hardware/hw.h +system/hardware/tici/__init__.py +system/hardware/tici/hardware.h +system/hardware/tici/hardware.py +system/hardware/tici/pins.py +system/hardware/tici/agnos.py +system/hardware/tici/agnos.json +system/hardware/tici/amplifier.py +system/hardware/tici/updater +system/hardware/tici/iwlist.py +system/hardware/pc/__init__.py +system/hardware/pc/hardware.py selfdrive/locationd/__init__.py selfdrive/locationd/.gitignore diff --git a/scripts/disable-powersave.py b/scripts/disable-powersave.py index f651bc87f1a169..93688504f38c1d 100755 --- a/scripts/disable-powersave.py +++ b/scripts/disable-powersave.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE if __name__ == "__main__": HARDWARE.set_power_save(False) diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index ba86e02cc61d63..0dc7d704a8ccbe 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -32,7 +32,7 @@ from common.file_helpers import CallbackReader from common.params import Params from common.realtime import sec_since_boot, set_core_affinity -from selfdrive.hardware import HARDWARE, PC, AGNOS +from system.hardware import HARDWARE, PC, AGNOS from selfdrive.loggerd.config import ROOT from selfdrive.loggerd.xattr_cache import getxattr, setxattr from selfdrive.statsd import STATS_DIR diff --git a/selfdrive/athena/registration.py b/selfdrive/athena/registration.py index 2748934361617e..c44ca4c28b8bb2 100755 --- a/selfdrive/athena/registration.py +++ b/selfdrive/athena/registration.py @@ -11,7 +11,7 @@ from common.spinner import Spinner from common.basedir import PERSIST from selfdrive.controls.lib.alertmanager import set_offroad_alert -from selfdrive.hardware import HARDWARE, PC +from system.hardware import HARDWARE, PC from selfdrive.swaglog import cloudlog diff --git a/selfdrive/boardd/boardd.cc b/selfdrive/boardd/boardd.cc index 2745c037b6d15b..f47b5936bee421 100644 --- a/selfdrive/boardd/boardd.cc +++ b/selfdrive/boardd/boardd.cc @@ -27,7 +27,7 @@ #include "common/swaglog.h" #include "common/timing.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/boardd/pigeon.h" diff --git a/selfdrive/boardd/main.cc b/selfdrive/boardd/main.cc index 6f1f83685ba49d..cb17a584bdb580 100644 --- a/selfdrive/boardd/main.cc +++ b/selfdrive/boardd/main.cc @@ -3,7 +3,7 @@ #include "selfdrive/boardd/boardd.h" #include "common/swaglog.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" int main(int argc, char *argv[]) { LOGW("starting boardd"); diff --git a/selfdrive/boardd/pandad.py b/selfdrive/boardd/pandad.py index 306d764e590b5d..51bfb92dd97183 100755 --- a/selfdrive/boardd/pandad.py +++ b/selfdrive/boardd/pandad.py @@ -10,7 +10,7 @@ from panda import DEFAULT_FW_FN, DEFAULT_H7_FW_FN, MCU_TYPE_H7, Panda, PandaDFU from common.basedir import BASEDIR from common.params import Params -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE from selfdrive.swaglog import cloudlog diff --git a/selfdrive/boardd/tests/test_boardd_loopback.py b/selfdrive/boardd/tests/test_boardd_loopback.py index 631b4c987ff0b6..e9bbcb4586b45b 100755 --- a/selfdrive/boardd/tests/test_boardd_loopback.py +++ b/selfdrive/boardd/tests/test_boardd_loopback.py @@ -12,7 +12,7 @@ from common.timeout import Timeout from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.car import make_can_msg -from selfdrive.hardware import TICI +from system.hardware import TICI from selfdrive.test.helpers import phone_only, with_processes diff --git a/selfdrive/camerad/cameras/camera_common.cc b/selfdrive/camerad/cameras/camera_common.cc index 049324f0856a8b..a94cfedf1a0a88 100644 --- a/selfdrive/camerad/cameras/camera_common.cc +++ b/selfdrive/camerad/cameras/camera_common.cc @@ -15,7 +15,7 @@ #include "common/modeldata.h" #include "common/swaglog.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "msm_media_info.h" #ifdef QCOM2 diff --git a/selfdrive/camerad/cameras/camera_common.h b/selfdrive/camerad/cameras/camera_common.h index 74d6a6eb3f9372..e9c7ccd757520c 100644 --- a/selfdrive/camerad/cameras/camera_common.h +++ b/selfdrive/camerad/cameras/camera_common.h @@ -13,7 +13,7 @@ #include "common/mat.h" #include "common/queue.h" #include "common/swaglog.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #define CAMERA_ID_IMX298 0 #define CAMERA_ID_IMX179 1 diff --git a/selfdrive/camerad/main.cc b/selfdrive/camerad/main.cc index c86543265c8e1f..32899a8547d29f 100644 --- a/selfdrive/camerad/main.cc +++ b/selfdrive/camerad/main.cc @@ -4,7 +4,7 @@ #include "common/params.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" int main(int argc, char *argv[]) { if (!Hardware::PC()) { diff --git a/selfdrive/camerad/snapshot/snapshot.py b/selfdrive/camerad/snapshot/snapshot.py index 9f9072c96288f7..fa88849b69f317 100755 --- a/selfdrive/camerad/snapshot/snapshot.py +++ b/selfdrive/camerad/snapshot/snapshot.py @@ -9,7 +9,7 @@ from cereal.visionipc import VisionIpcClient, VisionStreamType from common.params import Params from common.realtime import DT_MDL -from selfdrive.hardware import PC +from system.hardware import PC from selfdrive.controls.lib.alertmanager import set_offroad_alert from selfdrive.manager.process_config import managed_processes diff --git a/selfdrive/camerad/test/test_camerad.py b/selfdrive/camerad/test/test_camerad.py index c311c17169ac57..1a2e365a8f2390 100755 --- a/selfdrive/camerad/test/test_camerad.py +++ b/selfdrive/camerad/test/test_camerad.py @@ -4,7 +4,7 @@ import unittest import cereal.messaging as messaging -from selfdrive.hardware import TICI +from system.hardware import TICI from selfdrive.test.helpers import with_processes TEST_TIMESPAN = 30 # random.randint(60, 180) # seconds diff --git a/selfdrive/camerad/test/test_exposure.py b/selfdrive/camerad/test/test_exposure.py index 31bcc286819f63..f42d0bfbe3e60c 100755 --- a/selfdrive/camerad/test/test_exposure.py +++ b/selfdrive/camerad/test/test_exposure.py @@ -6,7 +6,7 @@ from selfdrive.test.helpers import with_processes from selfdrive.camerad.snapshot.snapshot import get_snapshots -from selfdrive.hardware import TICI +from system.hardware import TICI TEST_TIME = 45 REPEAT = 5 diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index edb1538c3135c4..31807b0187f978 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -27,7 +27,7 @@ from selfdrive.controls.lib.alertmanager import AlertManager, set_offroad_alert from selfdrive.controls.lib.vehicle_model import VehicleModel from selfdrive.locationd.calibrationd import Calibration -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE from selfdrive.manager.process_config import managed_processes SOFT_DISABLE_TIME = 3 # seconds diff --git a/selfdrive/hardware b/selfdrive/hardware new file mode 120000 index 00000000000000..02a42c502ffc1b --- /dev/null +++ b/selfdrive/hardware @@ -0,0 +1 @@ +../system/hardware/ \ No newline at end of file diff --git a/selfdrive/loggerd/config.py b/selfdrive/loggerd/config.py index 6cd20a68ab40e7..168c9fba91c56b 100644 --- a/selfdrive/loggerd/config.py +++ b/selfdrive/loggerd/config.py @@ -1,6 +1,6 @@ import os from pathlib import Path -from selfdrive.hardware import PC +from system.hardware import PC if os.environ.get('LOG_ROOT', False): ROOT = os.environ['LOG_ROOT'] diff --git a/selfdrive/loggerd/logger.h b/selfdrive/loggerd/logger.h index ca08e64717e22a..e7594cee881b8d 100644 --- a/selfdrive/loggerd/logger.h +++ b/selfdrive/loggerd/logger.h @@ -13,7 +13,7 @@ #include "cereal/messaging/messaging.h" #include "common/util.h" #include "common/swaglog.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" const std::string LOG_ROOT = Path::log_root(); diff --git a/selfdrive/loggerd/loggerd.h b/selfdrive/loggerd/loggerd.h index 3b9b01a0fc9838..7e13e90e63117e 100644 --- a/selfdrive/loggerd/loggerd.h +++ b/selfdrive/loggerd/loggerd.h @@ -20,7 +20,7 @@ #include "common/swaglog.h" #include "common/timing.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/loggerd/encoder/encoder.h" #include "selfdrive/loggerd/logger.h" diff --git a/selfdrive/loggerd/tests/test_encoder.py b/selfdrive/loggerd/tests/test_encoder.py index 49baa8a6e372d9..1b9bcef2d7c2ef 100755 --- a/selfdrive/loggerd/tests/test_encoder.py +++ b/selfdrive/loggerd/tests/test_encoder.py @@ -13,7 +13,7 @@ from common.params import Params from common.timeout import Timeout -from selfdrive.hardware import TICI +from system.hardware import TICI from selfdrive.loggerd.config import ROOT from selfdrive.manager.process_config import managed_processes from tools.lib.logreader import LogReader diff --git a/selfdrive/loggerd/uploader.py b/selfdrive/loggerd/uploader.py index b7d8df861ae627..9de1c421bd4b50 100644 --- a/selfdrive/loggerd/uploader.py +++ b/selfdrive/loggerd/uploader.py @@ -15,7 +15,7 @@ from common.api import Api from common.params import Params from common.realtime import set_core_affinity -from selfdrive.hardware import TICI +from system.hardware import TICI from selfdrive.loggerd.xattr_cache import getxattr, setxattr from selfdrive.loggerd.config import ROOT from selfdrive.swaglog import cloudlog diff --git a/selfdrive/manager/build.py b/selfdrive/manager/build.py index 8421605a552001..f0a5aec927e5a5 100755 --- a/selfdrive/manager/build.py +++ b/selfdrive/manager/build.py @@ -10,7 +10,7 @@ from common.basedir import BASEDIR from common.spinner import Spinner from common.text_window import TextWindow -from selfdrive.hardware import AGNOS +from system.hardware import AGNOS from selfdrive.swaglog import cloudlog, add_file_handler from selfdrive.version import is_dirty diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index cd7817fa970b87..94af397c7df8d3 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -13,7 +13,7 @@ from common.params import Params, ParamKeyType from common.text_window import TextWindow from selfdrive.boardd.set_time import set_time -from selfdrive.hardware import HARDWARE, PC +from system.hardware import HARDWARE, PC from selfdrive.manager.helpers import unblock_stdout from selfdrive.manager.process import ensure_running from selfdrive.manager.process_config import managed_processes diff --git a/selfdrive/manager/process.py b/selfdrive/manager/process.py index e2bb41c217a819..22f55f3b362f51 100644 --- a/selfdrive/manager/process.py +++ b/selfdrive/manager/process.py @@ -17,7 +17,7 @@ from common.params import Params from common.realtime import sec_since_boot from selfdrive.swaglog import cloudlog -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE from cereal import log WATCHDOG_FN = "/dev/shm/wd_" diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 5d996d1169fc72..443ebbb41937fa 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -2,7 +2,7 @@ from cereal import car from common.params import Params -from selfdrive.hardware import PC +from system.hardware import PC from selfdrive.manager.process import PythonProcess, NativeProcess, DaemonProcess WEBCAM = os.getenv("USE_WEBCAM") is not None diff --git a/selfdrive/manager/test/test_manager.py b/selfdrive/manager/test/test_manager.py index 31949de0b9b404..e4c3ef7c4c214b 100755 --- a/selfdrive/manager/test/test_manager.py +++ b/selfdrive/manager/test/test_manager.py @@ -5,7 +5,7 @@ import unittest import selfdrive.manager.manager as manager -from selfdrive.hardware import AGNOS, HARDWARE +from system.hardware import AGNOS, HARDWARE from selfdrive.manager.process import DaemonProcess from selfdrive.manager.process_config import managed_processes diff --git a/selfdrive/modeld/modeld.cc b/selfdrive/modeld/modeld.cc index 10cc2fe56ebd92..0aac9b3c4a6184 100644 --- a/selfdrive/modeld/modeld.cc +++ b/selfdrive/modeld/modeld.cc @@ -11,7 +11,7 @@ #include "common/params.h" #include "common/swaglog.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/modeld/models/driving.h" ExitHandler do_exit; diff --git a/selfdrive/modeld/models/dmonitoring.cc b/selfdrive/modeld/models/dmonitoring.cc index e134ad3a5af5c4..71da8dad53921b 100644 --- a/selfdrive/modeld/models/dmonitoring.cc +++ b/selfdrive/modeld/models/dmonitoring.cc @@ -6,7 +6,7 @@ #include "common/modeldata.h" #include "common/params.h" #include "common/timing.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/modeld/models/dmonitoring.h" diff --git a/selfdrive/modeld/thneed/compile.cc b/selfdrive/modeld/thneed/compile.cc index 8698ce482e224b..f76c63b2b917cd 100644 --- a/selfdrive/modeld/thneed/compile.cc +++ b/selfdrive/modeld/thneed/compile.cc @@ -3,7 +3,7 @@ #include "selfdrive/modeld/runners/snpemodel.h" #include "selfdrive/modeld/thneed/thneed.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #define TEMPORAL_SIZE 512 #define DESIRE_LEN 8 diff --git a/selfdrive/sensord/test/test_sensord.py b/selfdrive/sensord/test/test_sensord.py index 96d8891265311c..9fd918c9716139 100755 --- a/selfdrive/sensord/test/test_sensord.py +++ b/selfdrive/sensord/test/test_sensord.py @@ -4,7 +4,7 @@ import unittest import cereal.messaging as messaging -from selfdrive.hardware import TICI +from system.hardware import TICI from selfdrive.test.helpers import with_processes TEST_TIMESPAN = 10 diff --git a/selfdrive/sentry.py b/selfdrive/sentry.py index 5f22bf18e0f706..10e43ee03c5521 100644 --- a/selfdrive/sentry.py +++ b/selfdrive/sentry.py @@ -5,7 +5,7 @@ from common.params import Params from selfdrive.athena.registration import is_registered_device -from selfdrive.hardware import HARDWARE, PC +from system.hardware import HARDWARE, PC from selfdrive.swaglog import cloudlog from selfdrive.version import get_branch, get_commit, get_origin, get_version, \ is_comma_remote, is_dirty, is_tested_branch diff --git a/selfdrive/statsd.py b/selfdrive/statsd.py index 5755e5111bd35d..a48ba56003e982 100755 --- a/selfdrive/statsd.py +++ b/selfdrive/statsd.py @@ -10,7 +10,7 @@ from common.params import Params from cereal.messaging import SubMaster from selfdrive.swaglog import cloudlog -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE from common.file_helpers import atomic_write_in_dir from selfdrive.version import get_normalized_origin, get_short_branch, get_short_version, is_dirty from selfdrive.loggerd.config import STATS_DIR, STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/selfdrive/swaglog.py b/selfdrive/swaglog.py index a987bfe72c00a1..68664330a56a66 100644 --- a/selfdrive/swaglog.py +++ b/selfdrive/swaglog.py @@ -7,7 +7,7 @@ import zmq from common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter -from selfdrive.hardware import PC +from system.hardware import PC if PC: SWAGLOG_DIR = os.path.join(str(Path.home()), ".comma", "log") diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index 5abc0d964fb62a..cc8efd6e4669cb 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -2,7 +2,7 @@ import time from functools import wraps -from selfdrive.hardware import PC +from system.hardware import PC from selfdrive.manager.process_config import managed_processes from selfdrive.version import training_version, terms_version diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index b83629c76a8347..b466aa8193d0b7 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -11,7 +11,7 @@ from common.spinner import Spinner from common.timeout import Timeout from common.transformations.camera import get_view_frame_from_road_frame, tici_f_frame_size, tici_d_frame_size -from selfdrive.hardware import PC +from system.hardware import PC from selfdrive.manager.process_config import managed_processes from selfdrive.test.openpilotci import BASE_URL, get_url from selfdrive.test.process_replay.compare_logs import compare_logs, save_log diff --git a/selfdrive/test/process_replay/test_debayer.py b/selfdrive/test/process_replay/test_debayer.py index a17c71b6029948..cf939e3672a628 100755 --- a/selfdrive/test/process_replay/test_debayer.py +++ b/selfdrive/test/process_replay/test_debayer.py @@ -6,7 +6,7 @@ import pyopencl as cl # install with `PYOPENCL_CL_PRETEND_VERSION=2.0 pip install pyopencl` -from selfdrive.hardware import PC, TICI +from system.hardware import PC, TICI from common.basedir import BASEDIR from selfdrive.test.openpilotci import BASE_URL, get_url from selfdrive.version import get_commit diff --git a/selfdrive/thermald/power_monitoring.py b/selfdrive/thermald/power_monitoring.py index d7a1f2a360e34f..acc2fd90bc9190 100644 --- a/selfdrive/thermald/power_monitoring.py +++ b/selfdrive/thermald/power_monitoring.py @@ -4,7 +4,7 @@ from cereal import log from common.params import Params, put_nonblocking from common.realtime import sec_since_boot -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE from selfdrive.swaglog import cloudlog from selfdrive.statsd import statlog diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 4df1e072b8eaad..84663e8eb0d61e 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -17,7 +17,7 @@ from common.params import Params from common.realtime import DT_TRML, sec_since_boot from selfdrive.controls.lib.alertmanager import set_offroad_alert -from selfdrive.hardware import HARDWARE, TICI, AGNOS +from system.hardware import HARDWARE, TICI, AGNOS from selfdrive.loggerd.config import get_available_percent from selfdrive.statsd import statlog from selfdrive.swaglog import cloudlog diff --git a/selfdrive/timezoned.py b/selfdrive/timezoned.py index fc1ca92cdf8e94..c1d5676db631ee 100755 --- a/selfdrive/timezoned.py +++ b/selfdrive/timezoned.py @@ -9,7 +9,7 @@ from timezonefinder import TimezoneFinder from common.params import Params -from selfdrive.hardware import AGNOS +from system.hardware import AGNOS from selfdrive.swaglog import cloudlog diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index cffa4596225227..1eecd78b1951e1 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -2,7 +2,7 @@ #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/window.h" diff --git a/selfdrive/ui/qt/api.cc b/selfdrive/ui/qt/api.cc index 94beb1218677a6..84e1a4032e88c2 100644 --- a/selfdrive/ui/qt/api.cc +++ b/selfdrive/ui/qt/api.cc @@ -14,7 +14,7 @@ #include "common/params.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" namespace CommaApi { diff --git a/selfdrive/ui/qt/maps/map_helpers.cc b/selfdrive/ui/qt/maps/map_helpers.cc index 83576eb630ea2a..2b2c27418eae36 100644 --- a/selfdrive/ui/qt/maps/map_helpers.cc +++ b/selfdrive/ui/qt/maps/map_helpers.cc @@ -4,7 +4,7 @@ #include #include "common/params.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/api.h" QString get_mapbox_token() { diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index ac6f1f1ba019b9..175e6cf5a3d32d 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -14,7 +14,7 @@ #include "common/params.h" #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/input.h" #include "selfdrive/ui/qt/widgets/scrollview.h" diff --git a/selfdrive/ui/qt/qt_window.h b/selfdrive/ui/qt/qt_window.h index 2c9a24e55b7eb5..02d127e7ff8905 100644 --- a/selfdrive/ui/qt/qt_window.h +++ b/selfdrive/ui/qt/qt_window.h @@ -12,7 +12,7 @@ #include #endif -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" const QString ASSET_PATH = ":/"; diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc index 0da1752fed1d4c..10a2d05f348baf 100644 --- a/selfdrive/ui/qt/setup/setup.cc +++ b/selfdrive/ui/qt/setup/setup.cc @@ -11,7 +11,7 @@ #include #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/api.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/offroad/networking.h" diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc index b906b5739da73f..6e8189f4bab1ce 100644 --- a/selfdrive/ui/qt/setup/updater.cc +++ b/selfdrive/ui/qt/setup/updater.cc @@ -2,7 +2,7 @@ #include #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/setup/updater.h" diff --git a/selfdrive/ui/qt/spinner.cc b/selfdrive/ui/qt/spinner.cc index e0b263227b6da3..8f13576fb26b5f 100644 --- a/selfdrive/ui/qt/spinner.cc +++ b/selfdrive/ui/qt/spinner.cc @@ -10,7 +10,7 @@ #include #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/util.h" diff --git a/selfdrive/ui/qt/text.cc b/selfdrive/ui/qt/text.cc index 04fda35483aba8..ac8e7bcd18e531 100644 --- a/selfdrive/ui/qt/text.cc +++ b/selfdrive/ui/qt/text.cc @@ -5,7 +5,7 @@ #include #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/widgets/scrollview.h" diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 7ce7a267df906f..600885fb373c53 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -6,7 +6,7 @@ #include "common/params.h" #include "common/swaglog.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" QString getVersion() { static QString version = QString::fromStdString(Params().get("Version")); diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 1122faf4c75d0e..8a5d4928041032 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -2,7 +2,7 @@ #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" #include "selfdrive/ui/qt/widgets/scrollview.h" diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc index 53f7eb677cfacc..433193df5d9c9e 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -5,7 +5,7 @@ #include #include "common/util.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/widgets/scrollview.h" AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent) { diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 0614e8c1d8b2b0..596d1d83b9b6d9 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -2,7 +2,7 @@ #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/widgets/controls.h" // SSH enable toggle diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index fb21f6f7afc620..d9613df0d0235c 100644 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -2,7 +2,7 @@ #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" MainWindow::MainWindow(QWidget *parent) : QWidget(parent) { main_layout = new QStackedLayout(this); diff --git a/selfdrive/ui/replay/replay.cc b/selfdrive/ui/replay/replay.cc index 4036086e1bff4f..5eb7469c92b37e 100644 --- a/selfdrive/ui/replay/replay.cc +++ b/selfdrive/ui/replay/replay.cc @@ -7,7 +7,7 @@ #include "cereal/services.h" #include "common/params.h" #include "common/timing.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/replay/util.h" Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *sm_, uint32_t flags, QString data_dir, QObject *parent) diff --git a/selfdrive/ui/replay/route.cc b/selfdrive/ui/replay/route.cc index ad93263ae9d55b..3d595141cdbba3 100644 --- a/selfdrive/ui/replay/route.cc +++ b/selfdrive/ui/replay/route.cc @@ -9,7 +9,7 @@ #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/qt/api.h" #include "selfdrive/ui/replay/replay.h" #include "selfdrive/ui/replay/util.h" diff --git a/selfdrive/ui/soundd/sound.h b/selfdrive/ui/soundd/sound.h index 82b360fd3850ef..7e009d28ad5df4 100644 --- a/selfdrive/ui/soundd/sound.h +++ b/selfdrive/ui/soundd/sound.h @@ -2,7 +2,7 @@ #include #include -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #include "selfdrive/ui/ui.h" const std::tuple sound_list[] = { diff --git a/selfdrive/ui/tests/test_soundd.py b/selfdrive/ui/tests/test_soundd.py index cba0f6dbd18493..8cc9215b74a8ec 100755 --- a/selfdrive/ui/tests/test_soundd.py +++ b/selfdrive/ui/tests/test_soundd.py @@ -8,7 +8,7 @@ from selfdrive.test.helpers import phone_only, with_processes # TODO: rewrite for unittest from common.realtime import DT_CTRL -from selfdrive.hardware import HARDWARE +from system.hardware import HARDWARE AudibleAlert = car.CarControl.HUDControl.AudibleAlert diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index c1af4ed6f42253..4d1e1ab7462593 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -10,7 +10,7 @@ #include "common/swaglog.h" #include "common/util.h" #include "common/watchdog.h" -#include "selfdrive/hardware/hw.h" +#include "system/hardware/hw.h" #define BACKLIGHT_DT 0.05 #define BACKLIGHT_TS 10.00 diff --git a/selfdrive/updated.py b/selfdrive/updated.py index f835522cdb980c..33e2d5d41813be 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -37,7 +37,7 @@ from common.basedir import BASEDIR from common.params import Params -from selfdrive.hardware import AGNOS, HARDWARE +from system.hardware import AGNOS, HARDWARE from selfdrive.swaglog import cloudlog from selfdrive.controls.lib.alertmanager import set_offroad_alert from selfdrive.version import is_tested_branch @@ -265,7 +265,7 @@ def finalize_update(wait_helper: WaitTimeHelper) -> None: def handle_agnos_update(wait_helper: WaitTimeHelper) -> None: - from selfdrive.hardware.tici.agnos import flash_agnos_update, get_target_slot_number + from system.hardware.tici.agnos import flash_agnos_update, get_target_slot_number cur_version = HARDWARE.get_os_version() updated_version = run(["bash", "-c", r"unset AGNOS_VERSION && source launch_env.sh && \ @@ -281,7 +281,7 @@ def handle_agnos_update(wait_helper: WaitTimeHelper) -> None: cloudlog.info(f"Beginning background installation for AGNOS {updated_version}") set_offroad_alert("Offroad_NeosUpdate", True) - manifest_path = os.path.join(OVERLAY_MERGED, "selfdrive/hardware/tici/agnos.json") + manifest_path = os.path.join(OVERLAY_MERGED, "system/hardware/tici/agnos.json") target_slot_number = get_target_slot_number() flash_agnos_update(manifest_path, target_slot_number, cloudlog) set_offroad_alert("Offroad_NeosUpdate", False) diff --git a/selfdrive/hardware/pc/__init__.py b/system/__init__.py similarity index 100% rename from selfdrive/hardware/pc/__init__.py rename to system/__init__.py diff --git a/selfdrive/hardware/.gitignore b/system/hardware/.gitignore similarity index 100% rename from selfdrive/hardware/.gitignore rename to system/hardware/.gitignore diff --git a/selfdrive/hardware/__init__.py b/system/hardware/__init__.py similarity index 56% rename from selfdrive/hardware/__init__.py rename to system/hardware/__init__.py index a1bf2e912fcc8b..780a20f9c0547d 100644 --- a/selfdrive/hardware/__init__.py +++ b/system/hardware/__init__.py @@ -1,9 +1,9 @@ import os from typing import cast -from selfdrive.hardware.base import HardwareBase -from selfdrive.hardware.tici.hardware import Tici -from selfdrive.hardware.pc.hardware import Pc +from system.hardware.base import HardwareBase +from system.hardware.tici.hardware import Tici +from system.hardware.pc.hardware import Pc TICI = os.path.isfile('/TICI') AGNOS = TICI diff --git a/selfdrive/hardware/base.h b/system/hardware/base.h similarity index 100% rename from selfdrive/hardware/base.h rename to system/hardware/base.h diff --git a/selfdrive/hardware/base.py b/system/hardware/base.py similarity index 100% rename from selfdrive/hardware/base.py rename to system/hardware/base.py diff --git a/selfdrive/hardware/hw.h b/system/hardware/hw.h similarity index 92% rename from selfdrive/hardware/hw.h rename to system/hardware/hw.h index 364a1c5c33ed87..f50e94abe1013e 100644 --- a/selfdrive/hardware/hw.h +++ b/system/hardware/hw.h @@ -1,10 +1,10 @@ #pragma once -#include "selfdrive/hardware/base.h" +#include "system/hardware/base.h" #include "common/util.h" #if QCOM2 -#include "selfdrive/hardware/tici/hardware.h" +#include "system/hardware/tici/hardware.h" #define Hardware HardwareTici #else class HardwarePC : public HardwareNone { diff --git a/selfdrive/hardware/tici/__init__.py b/system/hardware/pc/__init__.py similarity index 100% rename from selfdrive/hardware/tici/__init__.py rename to system/hardware/pc/__init__.py diff --git a/selfdrive/hardware/pc/hardware.py b/system/hardware/pc/hardware.py similarity index 96% rename from selfdrive/hardware/pc/hardware.py rename to system/hardware/pc/hardware.py index 2f5db925a984dd..b2c4a4343b2b43 100644 --- a/selfdrive/hardware/pc/hardware.py +++ b/system/hardware/pc/hardware.py @@ -1,7 +1,7 @@ import random from cereal import log -from selfdrive.hardware.base import HardwareBase, ThermalConfig +from system.hardware.base import HardwareBase, ThermalConfig NetworkType = log.DeviceState.NetworkType NetworkStrength = log.DeviceState.NetworkStrength diff --git a/system/hardware/tici/__init__.py b/system/hardware/tici/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/selfdrive/hardware/tici/agnos.json b/system/hardware/tici/agnos.json similarity index 100% rename from selfdrive/hardware/tici/agnos.json rename to system/hardware/tici/agnos.json diff --git a/selfdrive/hardware/tici/agnos.py b/system/hardware/tici/agnos.py similarity index 100% rename from selfdrive/hardware/tici/agnos.py rename to system/hardware/tici/agnos.py diff --git a/selfdrive/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py similarity index 100% rename from selfdrive/hardware/tici/amplifier.py rename to system/hardware/tici/amplifier.py diff --git a/selfdrive/hardware/tici/hardware.h b/system/hardware/tici/hardware.h similarity index 97% rename from selfdrive/hardware/tici/hardware.h rename to system/hardware/tici/hardware.h index b11a0a5bb0892b..dcccb9f3d1f6fb 100644 --- a/selfdrive/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -5,7 +5,7 @@ #include "common/params.h" #include "common/util.h" -#include "selfdrive/hardware/base.h" +#include "system/hardware/base.h" class HardwareTici : public HardwareNone { public: diff --git a/selfdrive/hardware/tici/hardware.py b/system/hardware/tici/hardware.py similarity index 98% rename from selfdrive/hardware/tici/hardware.py rename to system/hardware/tici/hardware.py index 0a923405989b44..a69d3eb74350fd 100644 --- a/selfdrive/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -9,10 +9,10 @@ from cereal import log from common.gpio import gpio_set, gpio_init -from selfdrive.hardware.base import HardwareBase, ThermalConfig -from selfdrive.hardware.tici import iwlist -from selfdrive.hardware.tici.pins import GPIO -from selfdrive.hardware.tici.amplifier import Amplifier +from system.hardware.base import HardwareBase, ThermalConfig +from system.hardware.tici import iwlist +from system.hardware.tici.pins import GPIO +from system.hardware.tici.amplifier import Amplifier NM = 'org.freedesktop.NetworkManager' NM_CON_ACT = NM + '.Connection.Active' diff --git a/selfdrive/hardware/tici/iwlist.py b/system/hardware/tici/iwlist.py similarity index 100% rename from selfdrive/hardware/tici/iwlist.py rename to system/hardware/tici/iwlist.py diff --git a/selfdrive/hardware/tici/pins.py b/system/hardware/tici/pins.py similarity index 100% rename from selfdrive/hardware/tici/pins.py rename to system/hardware/tici/pins.py diff --git a/selfdrive/hardware/tici/power_draw_test.py b/system/hardware/tici/power_draw_test.py similarity index 97% rename from selfdrive/hardware/tici/power_draw_test.py rename to system/hardware/tici/power_draw_test.py index 1af51fc01847de..bde92ae4a5856e 100755 --- a/selfdrive/hardware/tici/power_draw_test.py +++ b/system/hardware/tici/power_draw_test.py @@ -2,8 +2,8 @@ import os import time import numpy as np -from selfdrive.hardware.tici.hardware import Tici -from selfdrive.hardware.tici.pins import GPIO +from system.hardware.tici.hardware import Tici +from system.hardware.tici.pins import GPIO from common.gpio import gpio_init, gpio_set def read_power(): diff --git a/selfdrive/hardware/tici/power_monitor.py b/system/hardware/tici/power_monitor.py similarity index 100% rename from selfdrive/hardware/tici/power_monitor.py rename to system/hardware/tici/power_monitor.py diff --git a/selfdrive/hardware/tici/precise_power_measure.py b/system/hardware/tici/precise_power_measure.py similarity index 77% rename from selfdrive/hardware/tici/precise_power_measure.py rename to system/hardware/tici/precise_power_measure.py index c66936aef871df..5d6885136792db 100755 --- a/selfdrive/hardware/tici/precise_power_measure.py +++ b/system/hardware/tici/precise_power_measure.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import numpy as np -from selfdrive.hardware.tici.power_monitor import sample_power +from system.hardware.tici.power_monitor import sample_power if __name__ == '__main__': print("measuring for 5 seconds") diff --git a/selfdrive/hardware/tici/restart_modem.sh b/system/hardware/tici/restart_modem.sh similarity index 100% rename from selfdrive/hardware/tici/restart_modem.sh rename to system/hardware/tici/restart_modem.sh diff --git a/selfdrive/hardware/tici/test_agnos_updater.py b/system/hardware/tici/test_agnos_updater.py similarity index 100% rename from selfdrive/hardware/tici/test_agnos_updater.py rename to system/hardware/tici/test_agnos_updater.py diff --git a/selfdrive/hardware/tici/test_power_draw.py b/system/hardware/tici/test_power_draw.py similarity index 92% rename from selfdrive/hardware/tici/test_power_draw.py rename to system/hardware/tici/test_power_draw.py index ab2d691a09217d..31b8471328229f 100755 --- a/selfdrive/hardware/tici/test_power_draw.py +++ b/system/hardware/tici/test_power_draw.py @@ -4,8 +4,8 @@ import math from dataclasses import dataclass -from selfdrive.hardware import HARDWARE, TICI -from selfdrive.hardware.tici.power_monitor import get_power +from system.hardware import HARDWARE, TICI +from system.hardware.tici.power_monitor import get_power from selfdrive.manager.process_config import managed_processes from selfdrive.manager.manager import manager_cleanup diff --git a/selfdrive/hardware/tici/updater b/system/hardware/tici/updater similarity index 100% rename from selfdrive/hardware/tici/updater rename to system/hardware/tici/updater diff --git a/tools/lib/auth_config.py b/tools/lib/auth_config.py index 1699d94e53c871..7952fee4951eb2 100644 --- a/tools/lib/auth_config.py +++ b/tools/lib/auth_config.py @@ -1,7 +1,7 @@ import json import os from common.file_helpers import mkdirs_exists_ok -from selfdrive.hardware import PC +from system.hardware import PC class MissingAuthConfigError(Exception): From 6856c2d4efda54708be3790bb094b5c377dee575 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 17:43:34 -0700 Subject: [PATCH 050/436] jenkins: remove unnecessary workstation clean --- Jenkinsfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 250893d1af2906..9d1ab337611207 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -154,11 +154,6 @@ pipeline { } } - post { - always { - cleanWs() - } - } } } } From 83c5d2ede6c058a8eb723933a45bd3abc810bf51 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 17:56:59 -0700 Subject: [PATCH 051/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index a197e38c0cef00..78870ba0561743 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit a197e38c0cef00ececf4c7500438d37659d9199e +Subproject commit 78870ba056174352218988610e5010fde4eca956 From 9cda2d63380dc9b554212ba15e82ca0621ab1945 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 18:43:24 -0700 Subject: [PATCH 052/436] set AGNOS from /AGNOS file --- system/hardware/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hardware/__init__.py b/system/hardware/__init__.py index 780a20f9c0547d..77bb0e5e2a4731 100644 --- a/system/hardware/__init__.py +++ b/system/hardware/__init__.py @@ -6,7 +6,7 @@ from system.hardware.pc.hardware import Pc TICI = os.path.isfile('/TICI') -AGNOS = TICI +AGNOS = os.path.isfile('/AGNOS') PC = not TICI From fbd98b0e541b89f477061bab34dfc41495ac9399 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 22:51:09 -0700 Subject: [PATCH 053/436] CI: add build job for latest Ubuntu (#24637) * CI: add build job for latest Ubuntu * source * source env * scons cache * cache pyenv * fix key * source --- .github/workflows/selfdrive_tests.yaml | 2 +- .github/workflows/tools_tests.yaml | 40 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 567fc05aa56e56..5bd9acc200ce91 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -94,7 +94,7 @@ jobs: run: | ${{ env.RUN }} "scons -j$(nproc) --extras --test && \ rm -rf /tmp/scons_cache/* && \ - scons -j$(nproc) --cache-populate" + scons -j$(nproc) --extras --test --cache-populate" #build_mac: # name: build macos diff --git a/.github/workflows/tools_tests.yaml b/.github/workflows/tools_tests.yaml index 1e4ce7a4ae43e7..b2433fba8f7fce 100644 --- a/.github/workflows/tools_tests.yaml +++ b/.github/workflows/tools_tests.yaml @@ -21,6 +21,46 @@ env: GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/sh -c jobs: + build_latest_ubuntu: + name: build latest ubuntu + runs-on: ubuntu-20.04 + timeout-minutes: 60 + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - name: Cache pyenv + id: ubuntu-latest-pyenv + uses: actions/cache@v3 + with: + path: | + ~/.pyenv + ~/.local/share/virtualenvs/ + key: ubuntu-latest-python-${{ hashFiles('tools/ubuntu_setup.sh') }}- + - name: Cache scons + id: ubuntu-latest-scons + uses: actions/cache@v3 + with: + path: /tmp/scons_cache + key: ubuntu-latest-scons-${{ hashFiles('.github/workflows/tools_test.yaml') }}- + restore-keys: | + ubuntu-latest-scons-${{ hashFiles('.github/workflows/tools_test.yaml') }}- + ubuntu-latest-scons- + + - name: tools/ubuntu_setup.sh + run: | + source tools/openpilot_env.sh + tools/ubuntu_setup.sh + - name: Build openpilot + run: | + source tools/openpilot_env.sh + pipenv run scons -j$(nproc) --extras --test + - name: Cleanup scons cache + run: | + source tools/openpilot_env.sh + rm -rf /tmp/scons_cache/* + pipenv run scons -j$(nproc) --extras --test --cache-populate + plotjuggler: name: plotjuggler runs-on: ubuntu-20.04 From 0fce5d90459b77bf2cfa70f55f322f0e1fb8d01c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 11 Jun 2022 23:19:27 -0700 Subject: [PATCH 054/436] Move a bunch of stuff to system/ part 3 (#24829) * move swaglog.py * timezoned * logmessaged * version.py * fix linter --- .pylintrc | 2 +- common/api/__init__.py | 2 +- docs/conf.py | 6 ++++-- release/files_common | 8 ++++---- release/files_tici | 2 +- selfdrive/athena/athenad.py | 4 ++-- selfdrive/athena/manage_athenad.py | 4 ++-- selfdrive/athena/registration.py | 2 +- selfdrive/athena/tests/test_athenad.py | 2 +- selfdrive/boardd/pandad.py | 2 +- selfdrive/boardd/tests/boardd_old.py | 2 +- selfdrive/car/car_helpers.py | 4 ++-- selfdrive/car/disable_ecu.py | 2 +- selfdrive/car/fw_versions.py | 2 +- selfdrive/car/isotp_parallel_query.py | 2 +- selfdrive/car/mock/interface.py | 2 +- selfdrive/car/vin.py | 2 +- selfdrive/controls/controlsd.py | 2 +- selfdrive/controls/lib/events.py | 2 +- selfdrive/controls/lib/lane_planner.py | 2 +- selfdrive/controls/lib/lateral_planner.py | 2 +- .../controls/lib/longitudinal_mpc_lib/long_mpc.py | 2 +- selfdrive/controls/lib/longitudinal_planner.py | 2 +- selfdrive/controls/plannerd.py | 2 +- selfdrive/controls/radard.py | 2 +- selfdrive/debug/disable_ecu.py | 2 +- selfdrive/locationd/calibrationd.py | 2 +- selfdrive/locationd/laikad.py | 2 +- selfdrive/locationd/models/car_kf.py | 2 +- selfdrive/locationd/paramsd.py | 2 +- selfdrive/loggerd/deleter.py | 2 +- selfdrive/loggerd/tests/test_loggerd.py | 2 +- selfdrive/loggerd/tests/test_uploader.py | 2 +- selfdrive/loggerd/uploader.py | 2 +- selfdrive/manager/build.py | 4 ++-- selfdrive/manager/manager.py | 4 ++-- selfdrive/manager/process.py | 2 +- selfdrive/manager/process_config.py | 11 ++++++----- selfdrive/navd/navd.py | 2 +- selfdrive/sensord/rawgps/rawgpsd.py | 2 +- selfdrive/sentry.py | 4 ++-- selfdrive/statsd.py | 4 ++-- selfdrive/test/helpers.py | 2 +- selfdrive/test/process_replay/model_replay.py | 2 +- selfdrive/test/process_replay/test_debayer.py | 2 +- selfdrive/test/process_replay/test_processes.py | 2 +- selfdrive/test/test_onroad.py | 2 +- selfdrive/thermald/fan_controller.py | 2 +- selfdrive/thermald/power_monitoring.py | 2 +- selfdrive/thermald/thermald.py | 4 ++-- selfdrive/tombstoned.py | 4 ++-- selfdrive/updated.py | 4 ++-- {selfdrive => system}/logmessaged.py | 2 +- {selfdrive => system}/swaglog.py | 0 {selfdrive => system}/timezoned.py | 2 +- {selfdrive => system}/version.py | 2 +- 56 files changed, 76 insertions(+), 73 deletions(-) rename {selfdrive => system}/logmessaged.py (95%) rename {selfdrive => system}/swaglog.py (100%) rename {selfdrive => system}/timezoned.py (98%) rename {selfdrive => system}/version.py (99%) diff --git a/.pylintrc b/.pylintrc index 27539b743c63ba..58988c5d74f8a1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,7 +3,7 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-whitelist=scipy,cereal.messaging.messaging_pyx,PyQt5 +extension-pkg-whitelist=scipy,cereal.messaging.messaging_pyx,PyQt5,av # Add files or directories to the blacklist. They should be base names, not # paths. diff --git a/common/api/__init__.py b/common/api/__init__.py index fd2e70910e3487..c1fa635bd6808a 100644 --- a/common/api/__init__.py +++ b/common/api/__init__.py @@ -3,7 +3,7 @@ import requests from datetime import datetime, timedelta from common.basedir import PERSIST -from selfdrive.version import get_version +from system.version import get_version API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') diff --git a/docs/conf.py b/docs/conf.py index c11d17455d7bc6..fea921de1f0e54 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,10 +14,12 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os -from os.path import exists import sys -from selfdrive.version import get_version +from os.path import exists + from common.basedir import BASEDIR +from system.version import get_version + sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) diff --git a/release/files_common b/release/files_common index e89163aba0bd7c..76d440c402e4fd 100644 --- a/release/files_common +++ b/release/files_common @@ -61,17 +61,17 @@ release/* tools/lib/* tools/joystick/* -selfdrive/version.py - selfdrive/__init__.py selfdrive/sentry.py -selfdrive/swaglog.py -selfdrive/logmessaged.py selfdrive/tombstoned.py selfdrive/updated.py selfdrive/rtshield.py selfdrive/statsd.py +system/logmessaged.py +system/swaglog.py +system/version.py + selfdrive/athena/__init__.py selfdrive/athena/athenad.py selfdrive/athena/manage_athenad.py diff --git a/release/files_tici b/release/files_tici index ee1ac63c343627..485a898799ed4b 100644 --- a/release/files_tici +++ b/release/files_tici @@ -2,7 +2,7 @@ third_party/snpe/larch64** third_party/snpe/aarch64-ubuntu-gcc7.5/* third_party/mapbox-gl-native-qt/include/* -selfdrive/timezoned.py +system/timezoned.py selfdrive/assets/navigation/* selfdrive/assets/training_wide/* diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index 0dc7d704a8ccbe..26220dfa99d430 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -36,8 +36,8 @@ from selfdrive.loggerd.config import ROOT from selfdrive.loggerd.xattr_cache import getxattr, setxattr from selfdrive.statsd import STATS_DIR -from selfdrive.swaglog import SWAGLOG_DIR, cloudlog -from selfdrive.version import get_commit, get_origin, get_short_branch, get_version +from system.swaglog import SWAGLOG_DIR, cloudlog +from system.version import get_commit, get_origin, get_short_branch, get_version ATHENA_HOST = os.getenv('ATHENA_HOST', 'wss://athena.comma.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) diff --git a/selfdrive/athena/manage_athenad.py b/selfdrive/athena/manage_athenad.py index 58ad58310ffb59..6bbb03a79937c3 100755 --- a/selfdrive/athena/manage_athenad.py +++ b/selfdrive/athena/manage_athenad.py @@ -5,8 +5,8 @@ from common.params import Params from selfdrive.manager.process import launcher -from selfdrive.swaglog import cloudlog -from selfdrive.version import get_version, is_dirty +from system.swaglog import cloudlog +from system.version import get_version, is_dirty ATHENA_MGR_PID_PARAM = "AthenadPid" diff --git a/selfdrive/athena/registration.py b/selfdrive/athena/registration.py index c44ca4c28b8bb2..32bc92059cf231 100755 --- a/selfdrive/athena/registration.py +++ b/selfdrive/athena/registration.py @@ -12,7 +12,7 @@ from common.basedir import PERSIST from selfdrive.controls.lib.alertmanager import set_offroad_alert from system.hardware import HARDWARE, PC -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog UNREGISTERED_DONGLE_ID = "UnregisteredDevice" diff --git a/selfdrive/athena/tests/test_athenad.py b/selfdrive/athena/tests/test_athenad.py index a0b308c216c53a..382b549c1bbf5a 100755 --- a/selfdrive/athena/tests/test_athenad.py +++ b/selfdrive/athena/tests/test_athenad.py @@ -16,7 +16,7 @@ from websocket import ABNF from websocket._exceptions import WebSocketConnectionClosedException -from selfdrive import swaglog +from system import swaglog from selfdrive.athena import athenad from selfdrive.athena.athenad import MAX_RETRY_COUNT, dispatcher from selfdrive.athena.tests.helpers import MockWebsocket, MockParams, MockApi, EchoSocket, with_http_server diff --git a/selfdrive/boardd/pandad.py b/selfdrive/boardd/pandad.py index 51bfb92dd97183..54a28a6782c246 100755 --- a/selfdrive/boardd/pandad.py +++ b/selfdrive/boardd/pandad.py @@ -11,7 +11,7 @@ from common.basedir import BASEDIR from common.params import Params from system.hardware import HARDWARE -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog def get_expected_signature(panda: Panda) -> bytes: diff --git a/selfdrive/boardd/tests/boardd_old.py b/selfdrive/boardd/tests/boardd_old.py index 5e740fbcd82eff..fad29f6f34ae37 100755 --- a/selfdrive/boardd/tests/boardd_old.py +++ b/selfdrive/boardd/tests/boardd_old.py @@ -13,7 +13,7 @@ import cereal.messaging as messaging from common.realtime import Ratekeeper -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.boardd.boardd import can_capnp_to_can_list from cereal import car diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 7863f0d882318c..3bfd99f88def6f 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -4,12 +4,12 @@ from cereal import car from common.params import Params from common.basedir import BASEDIR -from selfdrive.version import is_comma_remote, is_tested_branch +from system.version import is_comma_remote, is_tested_branch from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from selfdrive.car.vin import get_vin, VIN_UNKNOWN from selfdrive.car.fw_versions import get_fw_versions, match_fw_to_car -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog import cereal.messaging as messaging from selfdrive.car import gen_empty_fingerprint diff --git a/selfdrive/car/disable_ecu.py b/selfdrive/car/disable_ecu.py index 3a06cc06f1a553..ac5c6c9f8fd19d 100644 --- a/selfdrive/car/disable_ecu.py +++ b/selfdrive/car/disable_ecu.py @@ -1,5 +1,5 @@ from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog EXT_DIAG_REQUEST = b'\x10\x03' EXT_DIAG_RESPONSE = b'\x50\x03' diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 69b7c5f452f3ed..c7253d794dcb4b 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -12,7 +12,7 @@ from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery from selfdrive.car.toyota.values import CAR as TOYOTA -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog Ecu = car.CarParams.Ecu diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index dc79babb138ac9..0e807512cfba99 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -4,7 +4,7 @@ from typing import Optional import cereal.messaging as messaging -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.boardd.boardd import can_list_to_can_capnp from panda.python.uds import CanClient, IsoTpMessage, FUNCTIONAL_ADDRS, get_rx_addr_for_tx_addr diff --git a/selfdrive/car/mock/interface.py b/selfdrive/car/mock/interface.py index 65e886751dbfbc..715850c2274443 100755 --- a/selfdrive/car/mock/interface.py +++ b/selfdrive/car/mock/interface.py @@ -2,7 +2,7 @@ import math from cereal import car from common.conversions import Conversions as CV -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog import cereal.messaging as messaging from selfdrive.car import gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index 2c5b623b065452..7413c3f2350564 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -6,7 +6,7 @@ import panda.python.uds as uds from panda.python.uds import FUNCTIONAL_ADDRS from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog OBD_VIN_REQUEST = b'\x09\x02' OBD_VIN_RESPONSE = b'\x49\x02\x01' diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 31807b0187f978..033072aa731e2e 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -11,7 +11,7 @@ import cereal.messaging as messaging from common.conversions import Conversions as CV from panda import ALTERNATIVE_EXPERIENCE -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can from selfdrive.controls.lib.lane_planner import CAMERA_OFFSET diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index 7ca05cc7446bc3..cc63d4995d7071 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -8,7 +8,7 @@ from common.conversions import Conversions as CV from common.realtime import DT_CTRL from selfdrive.locationd.calibrationd import MIN_SPEED_FILTER -from selfdrive.version import get_short_branch +from system.version import get_short_branch AlertSize = log.ControlsState.AlertSize AlertStatus = log.ControlsState.AlertStatus diff --git a/selfdrive/controls/lib/lane_planner.py b/selfdrive/controls/lib/lane_planner.py index 9bbad59084e620..1facb66d6390dd 100644 --- a/selfdrive/controls/lib/lane_planner.py +++ b/selfdrive/controls/lib/lane_planner.py @@ -3,7 +3,7 @@ from common.filter_simple import FirstOrderFilter from common.numpy_fast import interp from common.realtime import DT_MDL -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog TRAJECTORY_SIZE = 33 diff --git a/selfdrive/controls/lib/lateral_planner.py b/selfdrive/controls/lib/lateral_planner.py index f3f59ee308d2a8..ec21c16e91366c 100644 --- a/selfdrive/controls/lib/lateral_planner.py +++ b/selfdrive/controls/lib/lateral_planner.py @@ -1,7 +1,7 @@ import numpy as np from common.realtime import sec_since_boot, DT_MDL from common.numpy_fast import interp -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.controls.lib.lateral_mpc_lib.lat_mpc import LateralMpc from selfdrive.controls.lib.drive_helpers import CONTROL_N, MPC_COST_LAT, LAT_MPC_N, CAR_ROTATION_RADIUS from selfdrive.controls.lib.lane_planner import LanePlanner, TRAJECTORY_SIZE diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index b99ee9e9ac3480..bc0fc9fea6c04b 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -4,7 +4,7 @@ from common.realtime import sec_since_boot from common.numpy_fast import clip, interp -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.modeld.constants import index_function from selfdrive.controls.lib.radar_helpers import _LEAD_ACCEL_TAU diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 0d21c519a3534b..d4a6aaef8f7a68 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -12,7 +12,7 @@ from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, CONTROL_N -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog LON_MPC_STEP = 0.2 # first step is 0.2s AWARENESS_DECEL = -0.2 # car smoothly decel at .2m/s^2 when user is distracted diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index 083aeb79ccd1cb..06807b2a5cb84c 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -2,7 +2,7 @@ from cereal import car from common.params import Params from common.realtime import Priority, config_realtime_process -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.controls.lib.longitudinal_planner import Planner from selfdrive.controls.lib.lateral_planner import LateralPlanner import cereal.messaging as messaging diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index 0e514193dc3ec9..b2c99144573b55 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -10,7 +10,7 @@ from common.realtime import Ratekeeper, Priority, config_realtime_process from selfdrive.controls.lib.cluster.fastcluster_py import cluster_points_centroid from selfdrive.controls.lib.radar_helpers import Cluster, Track, RADAR_TO_CAMERA -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog class KalmanParams(): diff --git a/selfdrive/debug/disable_ecu.py b/selfdrive/debug/disable_ecu.py index f0faf400177469..af007207eb2771 100644 --- a/selfdrive/debug/disable_ecu.py +++ b/selfdrive/debug/disable_ecu.py @@ -3,7 +3,7 @@ import cereal.messaging as messaging from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog EXT_DIAG_REQUEST = b'\x10\x03' EXT_DIAG_RESPONSE = b'\x50\x03' diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index d7bf36fb268be7..e092c939ae3126 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -20,7 +20,7 @@ from common.transformations.model import model_height from common.transformations.camera import get_view_frame_from_road_frame from common.transformations.orientation import rot_from_euler, euler_from_rot -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog MIN_SPEED_FILTER = 15 * CV.MPH_TO_MS MAX_VEL_ANGLE_STD = np.radians(0.25) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 4ec159cd254140..01457f46818c51 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -17,7 +17,7 @@ from selfdrive.locationd.models.gnss_kf import GNSSKalman from selfdrive.locationd.models.gnss_kf import States as GStates import common.transformations.coordinates as coord -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog MAX_TIME_GAP = 10 diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 75534efa5a701b..fe7d2e650bd267 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -7,7 +7,7 @@ from selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY from selfdrive.locationd.models.constants import ObservationKind -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from rednose.helpers.kalmanfilter import KalmanFilter diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index 0e83728e5c87f3..ae67dc28ab5fb7 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -10,7 +10,7 @@ from common.numpy_fast import clip from selfdrive.locationd.models.car_kf import CarKalman, ObservationKind, States from selfdrive.locationd.models.constants import GENERATED_DIR -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog MAX_ANGLE_OFFSET_DELTA = 20 * DT_MDL # Max 20 deg/s diff --git a/selfdrive/loggerd/deleter.py b/selfdrive/loggerd/deleter.py index d745e91fb5fae1..5606288024d2b4 100644 --- a/selfdrive/loggerd/deleter.py +++ b/selfdrive/loggerd/deleter.py @@ -2,7 +2,7 @@ import os import shutil import threading -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.loggerd.config import ROOT, get_available_bytes, get_available_percent from selfdrive.loggerd.uploader import listdir_by_creation diff --git a/selfdrive/loggerd/tests/test_loggerd.py b/selfdrive/loggerd/tests/test_loggerd.py index 54f7aaaa2bd97c..9dbc7ac332af94 100755 --- a/selfdrive/loggerd/tests/test_loggerd.py +++ b/selfdrive/loggerd/tests/test_loggerd.py @@ -17,7 +17,7 @@ from common.timeout import Timeout from selfdrive.loggerd.config import ROOT from selfdrive.manager.process_config import managed_processes -from selfdrive.version import get_version +from system.version import get_version from tools.lib.logreader import LogReader from cereal.visionipc import VisionIpcServer, VisionStreamType from common.transformations.camera import tici_f_frame_size, tici_d_frame_size, tici_e_frame_size diff --git a/selfdrive/loggerd/tests/test_uploader.py b/selfdrive/loggerd/tests/test_uploader.py index b8c01776aedd45..6090bbe2aae6d1 100755 --- a/selfdrive/loggerd/tests/test_uploader.py +++ b/selfdrive/loggerd/tests/test_uploader.py @@ -6,7 +6,7 @@ import logging import json -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog import selfdrive.loggerd.uploader as uploader from selfdrive.loggerd.tests.loggerd_tests_common import UploaderTestCase diff --git a/selfdrive/loggerd/uploader.py b/selfdrive/loggerd/uploader.py index 9de1c421bd4b50..f97bafecb90e75 100644 --- a/selfdrive/loggerd/uploader.py +++ b/selfdrive/loggerd/uploader.py @@ -18,7 +18,7 @@ from system.hardware import TICI from selfdrive.loggerd.xattr_cache import getxattr, setxattr from selfdrive.loggerd.config import ROOT -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog NetworkType = log.DeviceState.NetworkType UPLOAD_ATTR_NAME = 'user.upload' diff --git a/selfdrive/manager/build.py b/selfdrive/manager/build.py index f0a5aec927e5a5..10b4c0a4b8766b 100755 --- a/selfdrive/manager/build.py +++ b/selfdrive/manager/build.py @@ -11,8 +11,8 @@ from common.spinner import Spinner from common.text_window import TextWindow from system.hardware import AGNOS -from selfdrive.swaglog import cloudlog, add_file_handler -from selfdrive.version import is_dirty +from system.swaglog import cloudlog, add_file_handler +from system.version import is_dirty MAX_CACHE_SIZE = 4e9 if "CI" in os.environ else 2e9 CACHE_DIR = Path("/data/scons_cache" if AGNOS else "/tmp/scons_cache") diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 94af397c7df8d3..140c7f1d442680 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -18,8 +18,8 @@ from selfdrive.manager.process import ensure_running from selfdrive.manager.process_config import managed_processes from selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID -from selfdrive.swaglog import cloudlog, add_file_handler -from selfdrive.version import is_dirty, get_commit, get_version, get_origin, get_short_branch, \ +from system.swaglog import cloudlog, add_file_handler +from system.version import is_dirty, get_commit, get_version, get_origin, get_short_branch, \ terms_version, training_version diff --git a/selfdrive/manager/process.py b/selfdrive/manager/process.py index 22f55f3b362f51..dabfbe4ee0b1f6 100644 --- a/selfdrive/manager/process.py +++ b/selfdrive/manager/process.py @@ -16,7 +16,7 @@ from common.basedir import BASEDIR from common.params import Params from common.realtime import sec_since_boot -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from system.hardware import HARDWARE from cereal import log diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 443ebbb41937fa..b3efe7e2deebc6 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -18,16 +18,19 @@ def logging(started, params, CP: car.CarParams) -> bool: return started and run procs = [ + NativeProcess("clocksd", "system/clocksd", ["./clocksd"]), + NativeProcess("logcatd", "system/logcatd", ["./logcatd"]), + NativeProcess("proclogd", "system/proclogd", ["./proclogd"]), + PythonProcess("logmessaged", "system.logmessaged", offroad=True), + PythonProcess("timezoned", "system.timezoned", enabled=not PC, offroad=True), + DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"), # due to qualcomm kernel bugs SIGKILLing camerad sometimes causes page table corruption NativeProcess("camerad", "selfdrive/camerad", ["./camerad"], unkillable=True, callback=driverview), - NativeProcess("clocksd", "system/clocksd", ["./clocksd"]), NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], enabled=(not PC or WEBCAM), callback=driverview), - NativeProcess("logcatd", "system/logcatd", ["./logcatd"]), NativeProcess("encoderd", "selfdrive/loggerd", ["./encoderd"]), NativeProcess("loggerd", "selfdrive/loggerd", ["./loggerd"], onroad=False, callback=logging), NativeProcess("modeld", "selfdrive/modeld", ["./modeld"]), - NativeProcess("proclogd", "system/proclogd", ["./proclogd"]), NativeProcess("sensord", "selfdrive/sensord", ["./sensord"], enabled=not PC), NativeProcess("ubloxd", "selfdrive/locationd", ["./ubloxd"], enabled=(not PC or WEBCAM)), NativeProcess("ui", "selfdrive/ui", ["./ui"], offroad=True, watchdog_max_dt=(5 if not PC else None)), @@ -38,14 +41,12 @@ def logging(started, params, CP: car.CarParams) -> bool: PythonProcess("controlsd", "selfdrive.controls.controlsd"), PythonProcess("deleter", "selfdrive.loggerd.deleter", offroad=True), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", enabled=(not PC or WEBCAM), callback=driverview), - PythonProcess("logmessaged", "selfdrive.logmessaged", offroad=True), PythonProcess("navd", "selfdrive.navd.navd"), PythonProcess("pandad", "selfdrive.boardd.pandad", offroad=True), PythonProcess("paramsd", "selfdrive.locationd.paramsd"), PythonProcess("plannerd", "selfdrive.controls.plannerd"), PythonProcess("radard", "selfdrive.controls.radard"), PythonProcess("thermald", "selfdrive.thermald.thermald", offroad=True), - PythonProcess("timezoned", "selfdrive.timezoned", enabled=not PC, offroad=True), PythonProcess("tombstoned", "selfdrive.tombstoned", enabled=not PC, offroad=True), PythonProcess("updated", "selfdrive.updated", enabled=not PC, onroad=False, offroad=True), PythonProcess("uploader", "selfdrive.loggerd.uploader", offroad=True), diff --git a/selfdrive/navd/navd.py b/selfdrive/navd/navd.py index 52db9b1d084200..fcd53d8b60cd02 100755 --- a/selfdrive/navd/navd.py +++ b/selfdrive/navd/navd.py @@ -14,7 +14,7 @@ distance_along_geometry, maxspeed_to_ms, minimum_distance, parse_banner_instructions) -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog REROUTE_DISTANCE = 25 MANEUVER_TRANSITION_THRESHOLD = 10 diff --git a/selfdrive/sensord/rawgps/rawgpsd.py b/selfdrive/sensord/rawgps/rawgpsd.py index e0861c0d9ae8df..3aa6d4b07210ac 100755 --- a/selfdrive/sensord/rawgps/rawgpsd.py +++ b/selfdrive/sensord/rawgps/rawgpsd.py @@ -10,7 +10,7 @@ import cereal.messaging as messaging from cereal import log -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.sensord.rawgps.modemdiag import ModemDiag, DIAG_LOG_F, setup_logs, send_recv from selfdrive.sensord.rawgps.structs import dict_unpacker diff --git a/selfdrive/sentry.py b/selfdrive/sentry.py index 10e43ee03c5521..aa409ea394a5c0 100644 --- a/selfdrive/sentry.py +++ b/selfdrive/sentry.py @@ -6,8 +6,8 @@ from common.params import Params from selfdrive.athena.registration import is_registered_device from system.hardware import HARDWARE, PC -from selfdrive.swaglog import cloudlog -from selfdrive.version import get_branch, get_commit, get_origin, get_version, \ +from system.swaglog import cloudlog +from system.version import get_branch, get_commit, get_origin, get_version, \ is_comma_remote, is_dirty, is_tested_branch diff --git a/selfdrive/statsd.py b/selfdrive/statsd.py index a48ba56003e982..7dc002727e2ccd 100755 --- a/selfdrive/statsd.py +++ b/selfdrive/statsd.py @@ -9,10 +9,10 @@ from common.params import Params from cereal.messaging import SubMaster -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from system.hardware import HARDWARE from common.file_helpers import atomic_write_in_dir -from selfdrive.version import get_normalized_origin, get_short_branch, get_short_version, is_dirty +from system.version import get_normalized_origin, get_short_branch, get_short_version, is_dirty from selfdrive.loggerd.config import STATS_DIR, STATS_DIR_FILE_LIMIT, STATS_SOCKET, STATS_FLUSH_TIME_S diff --git a/selfdrive/test/helpers.py b/selfdrive/test/helpers.py index cc8efd6e4669cb..8cc996c28dbf35 100644 --- a/selfdrive/test/helpers.py +++ b/selfdrive/test/helpers.py @@ -4,7 +4,7 @@ from system.hardware import PC from selfdrive.manager.process_config import managed_processes -from selfdrive.version import training_version, terms_version +from system.version import training_version, terms_version def set_params_enabled(): diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index b466aa8193d0b7..f3bb28663537b2 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -16,7 +16,7 @@ from selfdrive.test.openpilotci import BASE_URL, get_url from selfdrive.test.process_replay.compare_logs import compare_logs, save_log from selfdrive.test.process_replay.test_processes import format_diff -from selfdrive.version import get_commit +from system.version import get_commit from tools.lib.framereader import FrameReader from tools.lib.logreader import LogReader diff --git a/selfdrive/test/process_replay/test_debayer.py b/selfdrive/test/process_replay/test_debayer.py index cf939e3672a628..eff77fc479d591 100755 --- a/selfdrive/test/process_replay/test_debayer.py +++ b/selfdrive/test/process_replay/test_debayer.py @@ -9,7 +9,7 @@ from system.hardware import PC, TICI from common.basedir import BASEDIR from selfdrive.test.openpilotci import BASE_URL, get_url -from selfdrive.version import get_commit +from system.version import get_commit from selfdrive.camerad.snapshot.snapshot import yuv_to_rgb from tools.lib.logreader import LogReader from tools.lib.filereader import FileReader diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index c1b5b8539179a3..9f83a08e23355b 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -11,7 +11,7 @@ from selfdrive.test.openpilotci import get_url, upload_file from selfdrive.test.process_replay.compare_logs import compare_logs, save_log from selfdrive.test.process_replay.process_replay import CONFIGS, PROC_REPLAY_DIR, FAKEDATA, check_enabled, replay_process -from selfdrive.version import get_commit +from system.version import get_commit from tools.lib.filereader import FileReader from tools.lib.logreader import LogReader diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 6cf53a046e340b..79c3a2ebc0429f 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -39,7 +39,7 @@ "./_soundd": 1.0, "selfdrive.monitoring.dmonitoringd": 1.90, "./proclogd": 1.54, - "selfdrive.logmessaged": 0.2, + "system.logmessaged": 0.2, "./clocksd": 0.02, "./ubloxd": 0.02, "selfdrive.tombstoned": 0, diff --git a/selfdrive/thermald/fan_controller.py b/selfdrive/thermald/fan_controller.py index b1c7013297f5c4..2094faeaa73c04 100644 --- a/selfdrive/thermald/fan_controller.py +++ b/selfdrive/thermald/fan_controller.py @@ -3,7 +3,7 @@ from common.realtime import DT_TRML from common.numpy_fast import interp -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.controls.lib.pid import PIDController class BaseFanController(ABC): diff --git a/selfdrive/thermald/power_monitoring.py b/selfdrive/thermald/power_monitoring.py index acc2fd90bc9190..9f009d32657044 100644 --- a/selfdrive/thermald/power_monitoring.py +++ b/selfdrive/thermald/power_monitoring.py @@ -5,7 +5,7 @@ from common.params import Params, put_nonblocking from common.realtime import sec_since_boot from system.hardware import HARDWARE -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.statsd import statlog CAR_VOLTAGE_LOW_PASS_K = 0.091 # LPF gain for 5s tau (dt/tau / (dt/tau + 1)) diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 84663e8eb0d61e..403e9cb23286d7 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -20,10 +20,10 @@ from system.hardware import HARDWARE, TICI, AGNOS from selfdrive.loggerd.config import get_available_percent from selfdrive.statsd import statlog -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.thermald.power_monitoring import PowerMonitoring from selfdrive.thermald.fan_controller import TiciFanController -from selfdrive.version import terms_version, training_version +from system.version import terms_version, training_version ThermalStatus = log.DeviceState.ThermalStatus NetworkType = log.DeviceState.NetworkType diff --git a/selfdrive/tombstoned.py b/selfdrive/tombstoned.py index 1d0f8532db3118..0045e0766c0895 100755 --- a/selfdrive/tombstoned.py +++ b/selfdrive/tombstoned.py @@ -12,8 +12,8 @@ from common.file_helpers import mkdirs_exists_ok from selfdrive.loggerd.config import ROOT import selfdrive.sentry as sentry -from selfdrive.swaglog import cloudlog -from selfdrive.version import get_commit +from system.swaglog import cloudlog +from system.version import get_commit MAX_SIZE = 1_000_000 * 100 # allow up to 100M MAX_TOMBSTONE_FN_LEN = 62 # 85 - 23 ("/crash/") diff --git a/selfdrive/updated.py b/selfdrive/updated.py index 33e2d5d41813be..bdec383f52b824 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -38,9 +38,9 @@ from common.basedir import BASEDIR from common.params import Params from system.hardware import AGNOS, HARDWARE -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from selfdrive.controls.lib.alertmanager import set_offroad_alert -from selfdrive.version import is_tested_branch +from system.version import is_tested_branch LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock") STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging") diff --git a/selfdrive/logmessaged.py b/system/logmessaged.py similarity index 95% rename from selfdrive/logmessaged.py rename to system/logmessaged.py index 1d1fab51620092..280a23cf1d6b40 100755 --- a/selfdrive/logmessaged.py +++ b/system/logmessaged.py @@ -4,7 +4,7 @@ import cereal.messaging as messaging from common.logging_extra import SwagLogFileFormatter -from selfdrive.swaglog import get_file_handler +from system.swaglog import get_file_handler def main() -> NoReturn: diff --git a/selfdrive/swaglog.py b/system/swaglog.py similarity index 100% rename from selfdrive/swaglog.py rename to system/swaglog.py diff --git a/selfdrive/timezoned.py b/system/timezoned.py similarity index 98% rename from selfdrive/timezoned.py rename to system/timezoned.py index c1d5676db631ee..884a5c38122843 100755 --- a/selfdrive/timezoned.py +++ b/system/timezoned.py @@ -10,7 +10,7 @@ from common.params import Params from system.hardware import AGNOS -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog def set_timezone(valid_timezones, timezone): diff --git a/selfdrive/version.py b/system/version.py similarity index 99% rename from selfdrive/version.py rename to system/version.py index 08ab0973443cf1..f0817b3a9ff8dc 100644 --- a/selfdrive/version.py +++ b/system/version.py @@ -5,7 +5,7 @@ from functools import lru_cache from common.basedir import BASEDIR -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog TESTED_BRANCHES = ['devel', 'release3-staging', 'dashcam3-staging', 'release3', 'dashcam3'] From 1dd52ba27b6ec2121c5054c80f1820aeade40cb2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 11 Jun 2022 23:38:32 -0700 Subject: [PATCH 055/436] acados build script improvements for mac (#24634) * add Darwin build w/ universal2 libs * add rust for acados rebuilds * just build script fixes Co-authored-by: Adeeb Shihadeh --- third_party/acados/build.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/third_party/acados/build.sh b/third_party/acados/build.sh index 0b14a4cec9a01d..a4246fbda6d7fd 100755 --- a/third_party/acados/build.sh +++ b/third_party/acados/build.sh @@ -1,4 +1,5 @@ -#!/usr/bin/bash -e +#!/usr/bin/env bash +set -e DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" @@ -9,6 +10,13 @@ if [ -f /TICI ]; then BLAS_TARGET="ARMV8A_ARM_CORTEX_A57" fi +ACADOS_FLAGS="-DACADOS_WITH_QPOASES=ON -UBLASFEO_TARGET -DBLASFEO_TARGET=$BLAS_TARGET" + +if [[ "$OSTYPE" == "darwin"* ]]; then + ACADOS_FLAGS="$ACADOS_FLAGS -DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" + ARCHNAME="Darwin" +fi + if [ ! -d acados_repo/ ]; then git clone https://github.com/acados/acados.git $DIR/acados_repo # git clone https://github.com/commaai/acados.git $DIR/acados_repo @@ -21,7 +29,7 @@ git submodule update --recursive --init # build mkdir -p build cd build -cmake -DACADOS_WITH_QPOASES=ON -UBLASFEO_TARGET -DBLASFEO_TARGET=$BLAS_TARGET .. +cmake $ACADOS_FLAGS .. make -j20 install INSTALL_DIR="$DIR/$ARCHNAME" From 3f60088f432f0bf8e7805cdb2223817c249ec7fa Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Sun, 12 Jun 2022 17:27:46 +0100 Subject: [PATCH 056/436] Ford: disable radar for now (#24832) The newer Ford vehicles require a different radar parser. --- selfdrive/car/ford/radar_interface.py | 6 ++++++ selfdrive/car/ford/values.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/ford/radar_interface.py b/selfdrive/car/ford/radar_interface.py index 1dfc27e672b09c..866602cf09888b 100644 --- a/selfdrive/car/ford/radar_interface.py +++ b/selfdrive/car/ford/radar_interface.py @@ -9,6 +9,9 @@ def _create_radar_can_parser(car_fingerprint): + if DBC[car_fingerprint]['radar'] is None: + return None + msg_n = len(RADAR_MSGS) signals = list(zip(['X_Rel'] * msg_n + ['Angle'] * msg_n + ['V_Rel'] * msg_n, RADAR_MSGS * 3)) @@ -27,6 +30,9 @@ def __init__(self, CP): self.updated_messages = set() def update(self, can_strings): + if self.rcp is None: + return super().update(None) + vls = self.rcp.update_strings(can_strings) self.updated_messages.update(vls) diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index 96ec87d757075e..aae011ad3dd9c8 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -79,6 +79,6 @@ class CAR: DBC = { - CAR.ESCAPE_MK4: dbc_dict('ford_lincoln_base_pt', 'ford_fusion_2018_adas'), - CAR.FOCUS_MK4: dbc_dict('ford_lincoln_base_pt', 'ford_fusion_2018_adas'), + CAR.ESCAPE_MK4: dbc_dict('ford_lincoln_base_pt', None), + CAR.FOCUS_MK4: dbc_dict('ford_lincoln_base_pt', None), } From 1c29b20e72141c2beae6e32cf87dabded3095fda Mon Sep 17 00:00:00 2001 From: Jeroen <33349469+jeroenlammersma@users.noreply.github.com> Date: Sun, 12 Jun 2022 18:32:18 +0200 Subject: [PATCH 057/436] Updated CARLA to v0.9.13 (#24575) * Updated CARLA to v0.9.13 * pipenv lock Co-authored-by: Adeeb Shihadeh --- Pipfile | 2 +- Pipfile.lock | 24 ++++++++++++------------ tools/sim/start_carla.sh | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Pipfile b/Pipfile index 3958c890fec1f0..2ffe60e0fe0900 100644 --- a/Pipfile +++ b/Pipfile @@ -39,7 +39,7 @@ breathe = "*" subprocess32 = "*" tenacity = "*" mpld3 = "*" -carla = {version = "==0.9.12", markers="platform_system != 'Darwin'"} +carla = {version = "==0.9.13", markers="platform_system != 'Darwin'"} [packages] atomicwrites = "*" diff --git a/Pipfile.lock b/Pipfile.lock index df2b3bb8532678..25a97890d36811 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4dbfaf9d7a532e0e9a126f828512a5383a817722a1580ef64b99e2427f366a72" + "sha256": "eb1eeeaabf1266fab353532d10e4d9ae36306a7577f248513909c2d6096ab1c9" }, "pipfile-spec": 6, "requires": { @@ -1155,11 +1155,11 @@ }, "setuptools": { "hashes": [ - "sha256:d1746e7fd520e83bbe210d02fff1aa1a425ad671c7a9da7d246ec2401a087198", - "sha256:e7d11f3db616cda0751372244c2ba798e8e56a28e096ec4529010b803485f3fe" + "sha256:1f5d3a1502812025cdb2e5609b6af2d207332e3f50febe6db10ed3a59b2f155f", + "sha256:30b6b0fbacc459c90d27a63e6173facfc8b8c99a48fb24b5044f459ba63cd6cf" ], "markers": "python_version >= '3.7'", - "version": "==62.3.3" + "version": "==62.3.4" }, "six": { "hashes": [ @@ -1452,17 +1452,17 @@ }, "carla": { "hashes": [ - "sha256:1ed11b56c781cd15cbd540cacfb59ad348e0a021d898cfd0ff89a585f144da0b", - "sha256:20c1e1b72034175824d89b2d86b09ae72b4aca09ea25874dc6251f239297251d", - "sha256:6d1122c24af4f6375dc6858fbb0309b61c219b101d8c5a540def4c36c4563fe1", - "sha256:9c19ebf6cbbc535bde4baf9e18c72ab349657b34c4202b9751541e4c0d20b3cc", - "sha256:a69f6d84b59e2f805b2a417de98f977fe9efe0cfa733da8d75e20d28892da915", - "sha256:c3ae0dce3f1354b6311fee21a365947b0ff169249993a913904f676046d2d69f", - "sha256:dd392a267e14b785a8f65dafef86e05a92201253e9fb4a01e1e262834f20bed2" + "sha256:1210cce213e968a644effd4e2e48458a072481459d073424b05725056ba3d77d", + "sha256:339fcb1e392f3ade1be82b7258de19c533e2efae111e954a6eb174efb296903d", + "sha256:5f065825ce812343bf27a80a19d647b3200b31b44a9e80cea0340e3bd20cdf81", + "sha256:954ca34d5bdd4516ceca353db907fee8cec6630d6b31a732b17dd1554e0f0f94", + "sha256:a64ee78fe91137fa7d4828c7fc06d5824bd7312e29e4ea4f31a5d74dd28bff40", + "sha256:a95d2d4218ea388c863c66b7c2ab3fe49ffefe53999305cfcb6a8107042f79af", + "sha256:d2bfaea2d6824a2d758cbe813856c69420494f5c97d2a2dfb45653ccf976f1ce" ], "index": "pypi", "markers": "platform_system != 'Darwin'", - "version": "==0.9.12" + "version": "==0.9.13" }, "certifi": { "hashes": [ diff --git a/tools/sim/start_carla.sh b/tools/sim/start_carla.sh index abdaee4ea8a44c..67ced7eb21dc3f 100755 --- a/tools/sim/start_carla.sh +++ b/tools/sim/start_carla.sh @@ -15,7 +15,7 @@ if ! $(apt list --installed | grep -q nvidia-container-toolkit); then fi fi -docker pull carlasim/carla:0.9.12 +docker pull carlasim/carla:0.9.13 EXTRA_ARGS="-it" if [[ "$DETACH" ]]; then @@ -29,5 +29,5 @@ docker run \ --net=host \ -v /tmp/.X11-unix:/tmp/.X11-unix:rw \ $EXTRA_ARGS \ - carlasim/carla:0.9.12 \ + carlasim/carla:0.9.13 \ /bin/bash ./CarlaUE4.sh -opengl -nosound -RenderOffScreen -benchmark -fps=20 -quality-level=Low From 39da6912ea0b8db16d3f96f289f723452e540ab7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 12 Jun 2022 18:00:00 -0700 Subject: [PATCH 058/436] misc jenkins fixups (#24840) * bump cereal * remove that * pull cl image * lil docker cleanup --- Dockerfile.openpilot | 7 +------ cereal | 2 +- selfdrive/manager/test/test_manager.py | 8 ++------ tools/sim/Dockerfile.sim | 10 +++++----- tools/sim/build_container.sh | 2 +- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/Dockerfile.openpilot b/Dockerfile.openpilot index 66b9f18a11041a..102da78d7d01ee 100644 --- a/Dockerfile.openpilot +++ b/Dockerfile.openpilot @@ -8,11 +8,6 @@ ENV PYTHONPATH ${OPENPILOT_PATH}:${PYTHONPATH} RUN mkdir -p ${OPENPILOT_PATH} WORKDIR ${OPENPILOT_PATH} -COPY Pipfile Pipfile.lock $OPENPILOT_PATH -RUN pip install --no-cache-dir pipenv==2021.5.29 pip==21.3.1 && \ - pipenv install --system --deploy --dev --clear && \ - pip uninstall -y pipenv - COPY SConstruct ${OPENPILOT_PATH} COPY ./pyextra ${OPENPILOT_PATH}/pyextra @@ -30,4 +25,4 @@ COPY ./panda ${OPENPILOT_PATH}/panda COPY ./selfdrive ${OPENPILOT_PATH}/selfdrive COPY ./system ${OPENPILOT_PATH}/system -RUN scons -j$(nproc) +RUN scons --cache-readonly -j$(nproc) diff --git a/cereal b/cereal index 78870ba0561743..98f795fd733561 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 78870ba056174352218988610e5010fde4eca956 +Subproject commit 98f795fd73356198f1fc8d5ea5a8f25e6f7c57e0 diff --git a/selfdrive/manager/test/test_manager.py b/selfdrive/manager/test/test_manager.py index e4c3ef7c4c214b..a84ff264d26af3 100755 --- a/selfdrive/manager/test/test_manager.py +++ b/selfdrive/manager/test/test_manager.py @@ -5,14 +5,13 @@ import unittest import selfdrive.manager.manager as manager -from system.hardware import AGNOS, HARDWARE from selfdrive.manager.process import DaemonProcess from selfdrive.manager.process_config import managed_processes +from system.hardware import AGNOS, HARDWARE os.environ['FAKEUPLOAD'] = "1" -# TODO: make eon fast -MAX_STARTUP_TIME = 15 +MAX_STARTUP_TIME = 3 ALL_PROCESSES = [p.name for p in managed_processes.values() if (type(p) is not DaemonProcess) and p.enabled and (p.name not in ['updated', 'pandad'])] @@ -54,9 +53,6 @@ def test_clean_exit(self): # TODO: make Qt UI exit gracefully continue - # Make sure the process is actually dead - managed_processes[p].stop() - # TODO: interrupted blocking read exits with 1 in cereal. use a more unique return code exit_codes = [0, 1] if managed_processes[p].sigkill: diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index d838efed133e95..0d6e8e584c120b 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -1,9 +1,9 @@ FROM ghcr.io/commaai/openpilot-base-cl:latest -RUN apt-get update && apt-get install -y --no-install-recommends\ - tmux \ - vim \ - && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + tmux \ + vim \ + && rm -rf /var/lib/apt/lists/* # get same tmux config used on NEOS for debugging RUN cd $HOME && \ @@ -27,6 +27,6 @@ COPY ./system $HOME/openpilot/system COPY ./tools $HOME/openpilot/tools WORKDIR $HOME/openpilot -RUN scons -j12 +RUN scons --cache-readonly -j12 RUN python -c "from selfdrive.test.helpers import set_params_enabled; set_params_enabled()" diff --git a/tools/sim/build_container.sh b/tools/sim/build_container.sh index 451277d590b1a5..81afb82d839437 100755 --- a/tools/sim/build_container.sh +++ b/tools/sim/build_container.sh @@ -3,7 +3,7 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $DIR/../../ -docker pull ghcr.io/commaai/openpilot-base:latest +docker pull ghcr.io/commaai/openpilot-base-cl:latest docker build \ --cache-from ghcr.io/commaai/openpilot-sim:latest \ -t ghcr.io/commaai/openpilot-sim:latest \ From e7cad559a659d47466645290e00ee6205bb938c6 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Mon, 13 Jun 2022 05:39:36 +0100 Subject: [PATCH 059/436] Ford: remove Fusion DBC from release (#24841) --- release/files_common | 1 - 1 file changed, 1 deletion(-) diff --git a/release/files_common b/release/files_common index 76d440c402e4fd..6731ae4a120451 100644 --- a/release/files_common +++ b/release/files_common @@ -473,7 +473,6 @@ opendbc/gm_global_a_powertrain_generated.dbc opendbc/gm_global_a_object.dbc opendbc/gm_global_a_chassis.dbc -opendbc/ford_fusion_2018_adas.dbc opendbc/ford_lincoln_base_pt.dbc opendbc/honda_accord_2018_can_generated.dbc From 3db36a1958b24023a8af2a10419c0c7e4112bef7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 12 Jun 2022 21:59:58 -0700 Subject: [PATCH 060/436] jenkins: lock simulator --- Jenkinsfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9d1ab337611207..ebc26a5920c3cf 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -89,8 +89,10 @@ pipeline { sh "git config --global --add safe.directory ${WORKSPACE}" sh "git lfs pull" sh "${WORKSPACE}/tools/sim/build_container.sh" - sh "DETACH=1 ${WORKSPACE}/tools/sim/start_carla.sh" - sh "${WORKSPACE}/tools/sim/start_openpilot_docker.sh" + lock(resource: "", label: "simulator", inversePrecedence: true, quantity: 1) { + sh "DETACH=1 ${WORKSPACE}/tools/sim/start_carla.sh" + sh "${WORKSPACE}/tools/sim/start_openpilot_docker.sh" + } } post { From 84c741ecf6ac0727a29aeebb5f2a6539c2d76dd2 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 13 Jun 2022 09:37:12 +0200 Subject: [PATCH 061/436] SConstruct: set AGNOS from /AGNOS --- SConstruct | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SConstruct b/SConstruct index 0aeb382c23400e..edf14b4725d8b9 100644 --- a/SConstruct +++ b/SConstruct @@ -6,7 +6,7 @@ import platform import numpy as np TICI = os.path.isfile('/TICI') -AGNOS = TICI +AGNOS = os.path.isfile('/AGNOS') Decider('MD5-timestamp') From d71295e0451260acdcfdfb14477c0e21bc4ed311 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 13 Jun 2022 09:50:40 +0200 Subject: [PATCH 062/436] Revert "SConstruct: set AGNOS from /AGNOS" until Jenkins devices are updated This reverts commit 84c741ecf6ac0727a29aeebb5f2a6539c2d76dd2. --- SConstruct | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SConstruct b/SConstruct index edf14b4725d8b9..0aeb382c23400e 100644 --- a/SConstruct +++ b/SConstruct @@ -6,7 +6,7 @@ import platform import numpy as np TICI = os.path.isfile('/TICI') -AGNOS = os.path.isfile('/AGNOS') +AGNOS = TICI Decider('MD5-timestamp') From 724b322909152d58f626b6e76fd34d7d03d67250 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Mon, 13 Jun 2022 11:45:35 +0200 Subject: [PATCH 063/436] Laikad: Use filter for correcting measurements (#24824) * Update laikad. * Update log error --- SConstruct | 2 +- selfdrive/locationd/laikad.py | 65 ++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/SConstruct b/SConstruct index 0aeb382c23400e..b5c4edc99bff0d 100644 --- a/SConstruct +++ b/SConstruct @@ -359,6 +359,7 @@ Export('cereal', 'messaging', 'visionipc') rednose_config = { 'generated_folder': '#selfdrive/locationd/models/generated', 'to_build': { + 'gnss': ('#selfdrive/locationd/models/gnss_kf.py', True, []), 'live': ('#selfdrive/locationd/models/live_kf.py', True, ['live_kf_constants.h']), 'car': ('#selfdrive/locationd/models/car_kf.py', True, []), }, @@ -366,7 +367,6 @@ rednose_config = { if arch != "larch64": rednose_config['to_build'].update({ - 'gnss': ('#selfdrive/locationd/models/gnss_kf.py', True, []), 'loc_4': ('#selfdrive/locationd/models/loc_kf.py', True, []), 'pos_computer_4': ('#rednose/helpers/lst_sq_computer.py', False, []), 'pos_computer_5': ('#rednose/helpers/lst_sq_computer.py', False, []), diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 01457f46818c51..42c4156795bad8 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -6,6 +6,8 @@ import numpy as np from collections import defaultdict +from numpy.linalg import linalg + from cereal import log, messaging from laika import AstroDog from laika.constants import SECS_IN_MIN @@ -37,24 +39,33 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) new_meas = read_raw_ublox(report) + processed_measurements = process_measurements(new_meas, self.astro_dog) + pos_fix = calc_pos_fix(processed_measurements, min_measurements=4) - measurements = process_measurements(new_meas, self.astro_dog) - pos_fix = calc_pos_fix(measurements, min_measurements=4) - # To get a position fix a minimum of 5 measurements are needed. - # Each report can contain less and some measurements can't be processed. + t = ublox_mono_time * 1e-9 + kf_pos_std = None + if all(self.kf_valid(t)): + self.gnss_kf.predict(t) + kf_pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())) + # If localizer is valid use its position to correct measurements + if kf_pos_std is not None and linalg.norm(kf_pos_std) < 100: + est_pos = self.gnss_kf.x[GStates.ECEF_POS] + elif len(pos_fix) > 0 and abs(np.array(pos_fix[1])).mean() < 1000: + est_pos = pos_fix[0][:3] + else: + est_pos = None corrected_measurements = [] - if len(pos_fix) > 0 and abs(np.array(pos_fix[1])).mean() < 1000: - corrected_measurements = correct_measurements(measurements, pos_fix[0][:3], self.astro_dog) + if est_pos is not None: + corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) - t = ublox_mono_time * 1e-9 self.update_localizer(pos_fix, t, corrected_measurements) - localizer_valid = self.localizer_valid(t) + kf_valid = all(self.kf_valid(t)) ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() - pos_std = self.gnss_kf.P[GStates.ECEF_POS].flatten().tolist() - vel_std = self.gnss_kf.P[GStates.ECEF_VELOCITY].flatten().tolist() + pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())).tolist() + vel_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_VELOCITY].diagonal())).tolist() bearing_deg, bearing_std = get_bearing_from_gnss(ecef_pos, ecef_vel, vel_std) @@ -62,9 +73,9 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): dat = messaging.new_message("gnssMeasurements") measurement_msg = log.LiveLocationKalman.Measurement.new_message dat.gnssMeasurements = { - "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=localizer_valid), - "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=localizer_valid), - "bearingDeg": measurement_msg(value=[bearing_deg], std=[bearing_std], valid=localizer_valid), + "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), + "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), + "bearingDeg": measurement_msg(value=[bearing_deg], std=[bearing_std], valid=kf_valid), "ubloxMonoTime": ublox_mono_time, "correctedMeasurements": meas_msgs } @@ -77,18 +88,21 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement]): # Check time and outputs are valid - if not self.localizer_valid(t): - # A position fix is needed when resetting the kalman filter. - if len(pos_fix) == 0: - return - post_est = pos_fix[0][:3].tolist() - filter_time = self.gnss_kf.filter.filter_time - if filter_time is None: + valid = self.kf_valid(t) + if not all(valid): + if not valid[0]: cloudlog.info("Init gnss kalman filter") - elif abs(t - filter_time) > MAX_TIME_GAP: + elif not valid[1]: cloudlog.error("Time gap of over 10s detected, gnss kalman reset") - else: + elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") + else: + cloudlog.error("Gnss kalman std too far") + + if len(pos_fix) == 0: + cloudlog.error("Position fix not available when resetting kalman filter") + return + post_est = pos_fix[0][:3].tolist() self.init_gnss_localizer(post_est) if len(measurements) > 0: kf_add_observations(self.gnss_kf, t, measurements) @@ -96,9 +110,12 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement # Ensure gnss filter is updated even with no new measurements self.gnss_kf.predict(t) - def localizer_valid(self, t: float): + def kf_valid(self, t: float): filter_time = self.gnss_kf.filter.filter_time - return filter_time is not None and (t - filter_time) < MAX_TIME_GAP and all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS])) + return [filter_time is not None, + filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, + all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS])), + linalg.norm(self.gnss_kf.P[GStates.ECEF_POS]) < 1e5] def init_gnss_localizer(self, est_pos): x_initial, p_initial_diag = np.copy(GNSSKalman.x_initial), np.copy(np.diagonal(GNSSKalman.P_initial)) From 1221aef233180a7d290ff813d2e2e53dbf87d586 Mon Sep 17 00:00:00 2001 From: Joost Wooning Date: Mon, 13 Jun 2022 12:27:47 +0200 Subject: [PATCH 064/436] ui: skip texture frame copy (#24700) * ui_blit working * simpler and working * more believable that it's real * working on device * build on pc * use hardware pc * reduce cpu usage * yuv conversion to EGL * move everything to cameraview * some cleanup * more cleanup * init array * init images with std::map * dont destroy images * do destroy images Co-authored-by: Comma Device --- selfdrive/test/test_onroad.py | 2 +- selfdrive/ui/SConscript | 3 ++ selfdrive/ui/qt/widgets/cameraview.cc | 72 ++++++++++++++++++++++++--- selfdrive/ui/qt/widgets/cameraview.h | 17 ++++++- 4 files changed, 84 insertions(+), 10 deletions(-) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 79c3a2ebc0429f..3586f86f12e749 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -27,7 +27,7 @@ "./camerad": 16.5, "./locationd": 9.1, "selfdrive.controls.plannerd": 11.7, - "./_ui": 26.4, + "./_ui": 19.2, "selfdrive.locationd.paramsd": 9.0, "./_sensord": 6.17, "selfdrive.controls.radard": 4.5, diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 47c4ac2a51273f..28fcc5f56f95e5 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -5,6 +5,9 @@ Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] +if arch == 'larch64': + base_libs.append('EGL') + maps = arch in ['larch64', 'x86_64'] if maps and arch == 'x86_64': diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index f16e8e4e0def08..000ac4847566a8 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -26,6 +26,18 @@ const char frame_vertex_shader[] = " vTexCoord = aTexCoord;\n" "}\n"; +#ifdef QCOM2 +const char frame_fragment_shader[] = + "#version 300 es\n" + "#extension GL_OES_EGL_image_external_essl3 : enable\n" + "precision mediump float;\n" + "uniform samplerExternalOES uTexture;\n" + "in vec2 vTexCoord;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " colorOut = texture(uTexture, vTexCoord);\n" + "}\n"; +#else const char frame_fragment_shader[] = #ifdef __APPLE__ "#version 330 core\n" @@ -45,6 +57,7 @@ const char frame_fragment_shader[] = " float b = y + 1.772 * uv.x;\n" " colorOut = vec4(r, g, b, 1.0);\n" "}\n"; +#endif const mat4 device_transform = {{ 1.0, 0.0, 0.0, 0.0, @@ -99,7 +112,7 @@ CameraViewWidget::~CameraViewWidget() { glDeleteVertexArrays(1, &frame_vao); glDeleteBuffers(1, &frame_vbo); glDeleteBuffers(1, &frame_ibo); - glDeleteBuffers(3, textures); + glDeleteBuffers(2, textures); } doneCurrent(); } @@ -143,10 +156,15 @@ void CameraViewWidget::initializeGL() { glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); - glGenTextures(3, textures); glUseProgram(program->programId()); + +#ifdef QCOM2 + glUniform1i(program->uniformLocation("uTexture"), 0); +#else + glGenTextures(2, textures); glUniform1i(program->uniformLocation("uTextureY"), 0); glUniform1i(program->uniformLocation("uTextureUV"), 1); +#endif } void CameraViewWidget::showEvent(QShowEvent *event) { @@ -207,29 +225,33 @@ void CameraViewWidget::paintGL() { for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) { if (frames[frame_idx].first == draw_frame_id) break; } - VisionBuf *frame = frames[frame_idx].second; - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); glViewport(0, 0, width(), height()); glBindVertexArray(frame_vao); - glUseProgram(program->programId()); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + VisionBuf *frame = frames[frame_idx].second; + +#ifdef QCOM2 + glActiveTexture(GL_TEXTURE0); + glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_images[frame->idx]); + assert(glGetError() == GL_NO_ERROR); +#else + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textures[0]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, frame->y); assert(glGetError() == GL_NO_ERROR); glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); - glActiveTexture(GL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_2D, textures[1]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, frame->uv); assert(glGetError() == GL_NO_ERROR); +#endif glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v); - assert(glGetError() == GL_NO_ERROR); glEnableVertexAttribArray(0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0); glDisableVertexAttribArray(0); @@ -247,6 +269,32 @@ void CameraViewWidget::vipcConnected(VisionIpcClient *vipc_client) { stream_height = vipc_client->buffers[0].height; stream_stride = vipc_client->buffers[0].stride; +#ifdef QCOM2 + egl_display = eglGetCurrentDisplay(); + + for (auto &pair : egl_images) { + eglDestroyImageKHR(egl_display, pair.second); + } + egl_images.clear(); + + for (int i = 0; i < vipc_client->num_buffers; i++) { // import buffers into OpenGL + int fd = dup(vipc_client->buffers[i].fd); // eglDestroyImageKHR will close, so duplicate + EGLint img_attrs[] = { + EGL_WIDTH, (int)vipc_client->buffers[i].width, + EGL_HEIGHT, (int)vipc_client->buffers[i].height, + EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12, + EGL_DMA_BUF_PLANE0_FD_EXT, fd, + EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)vipc_client->buffers[i].stride, + EGL_DMA_BUF_PLANE1_FD_EXT, fd, + EGL_DMA_BUF_PLANE1_OFFSET_EXT, (int)vipc_client->buffers[i].uv_offset, + EGL_DMA_BUF_PLANE1_PITCH_EXT, (int)vipc_client->buffers[i].stride, + EGL_NONE + }; + egl_images[i] = eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, img_attrs); + assert(eglGetError() == EGL_SUCCESS); + } +#else glBindTexture(GL_TEXTURE_2D, textures[0]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -262,6 +310,7 @@ void CameraViewWidget::vipcConnected(VisionIpcClient *vipc_client) { glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); assert(glGetError() == GL_NO_ERROR); +#endif updateFrameMat(width(), height()); } @@ -297,4 +346,11 @@ void CameraViewWidget::vipcThread() { emit vipcThreadFrameReceived(buf, meta_main.frame_id); } } + +#ifdef QCOM2 + for (auto &pair : egl_images) { + eglDestroyImageKHR(egl_display, pair.second); + } + egl_images.clear(); +#endif } diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 953cbed00b5cfc..ddc3fc253b3a41 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -6,6 +6,16 @@ #include #include #include + +#ifdef QCOM2 +#define EGL_EGLEXT_PROTOTYPES +#define EGL_NO_X11 +#define GL_TEXTURE_EXTERNAL_OES 0x8D65 +#include +#include +#include +#endif + #include "cereal/visionipc/visionipc_client.h" #include "selfdrive/camerad/cameras/camera_common.h" #include "selfdrive/ui/ui.h" @@ -41,11 +51,16 @@ class CameraViewWidget : public QOpenGLWidget, protected QOpenGLFunctions { bool zoomed_view; GLuint frame_vao, frame_vbo, frame_ibo; - GLuint textures[3]; + GLuint textures[2]; mat4 frame_mat; std::unique_ptr program; QColor bg = QColor("#000000"); +#ifdef QCOM2 + EGLDisplay egl_display; + std::map egl_images; +#endif + std::string stream_name; int stream_width = 0; int stream_height = 0; From a2d2378ee147f2db59aecd3401677cb2427d1ced Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Mon, 13 Jun 2022 14:02:31 +0200 Subject: [PATCH 065/436] Laikad: process executor to fetch orbits (#24843) * Use ProcessPoolExecutor to fetch orbits * update laika repo * Minor --- laika_repo | 2 +- selfdrive/locationd/laikad.py | 64 ++++++++++++------------- selfdrive/locationd/test/test_laikad.py | 33 +++++++++---- 3 files changed, 57 insertions(+), 42 deletions(-) diff --git a/laika_repo b/laika_repo index d87194613455b4..36f2621fc53484 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit d87194613455b42af19ff2b5a3f7d1cae5852885 +Subproject commit 36f2621fc5348487bb2cd606c37c8c15de0e32cd diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 42c4156795bad8..33e41398a0b440 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import time -from multiprocessing import Process, Queue +from concurrent.futures import Future, ProcessPoolExecutor from typing import List, Optional import numpy as np @@ -10,7 +10,7 @@ from cereal import log, messaging from laika import AstroDog -from laika.constants import SECS_IN_MIN +from laika.constants import SECS_IN_HR, SECS_IN_MIN from laika.ephemeris import EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId @@ -29,8 +29,9 @@ class Laikad: def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) self.gnss_kf = GNSSKalman(GENERATED_DIR) - self.orbit_p: Optional[Process] = None - self.orbit_q = Queue() + self.orbit_fetch_executor = ProcessPoolExecutor() + self.orbit_fetch_future: Optional[Future] = None + self.last_fetch_orbits_t = None def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': @@ -82,7 +83,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): return dat elif ublox_msg.which == 'ephemeris': ephem = convert_ublox_ephem(ublox_msg.ephemeris) - self.astro_dog.add_ephems([ephem], self.astro_dog.nav) + self.astro_dog.add_navs([ephem]) # elif ublox_msg.which == 'ionoData': # todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them. @@ -100,7 +101,7 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement cloudlog.error("Gnss kalman std too far") if len(pos_fix) == 0: - cloudlog.error("Position fix not available when resetting kalman filter") + cloudlog.warning("Position fix not available when resetting kalman filter") return post_est = pos_fix[0][:3].tolist() self.init_gnss_localizer(post_est) @@ -124,36 +125,33 @@ def init_gnss_localizer(self, est_pos): self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) - def get_orbit_data(self, t: GPSTime, queue): - cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") - start_time = time.monotonic() - try: - self.astro_dog.get_orbit_data(t, only_predictions=True) - except RuntimeError as e: - cloudlog.info(f"No orbit data found. {e}") - return - cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.2f}s") - if queue is not None: - queue.put((self.astro_dog.orbits, self.astro_dog.orbit_fetched_times)) - def fetch_orbits(self, t: GPSTime, block): - if t not in self.astro_dog.orbit_fetched_times: - if block: - self.get_orbit_data(t, None) - return - if self.orbit_p is None: - self.orbit_p = Process(target=self.get_orbit_data, args=(t, self.orbit_q)) - self.orbit_p.start() - if not self.orbit_q.empty(): - ret = self.orbit_q.get() + if t not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or t - self.last_fetch_orbits_t > SECS_IN_HR): + astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types + if self.orbit_fetch_future is None: + self.orbit_fetch_future = self.orbit_fetch_executor.submit(get_orbit_data, t, *astro_dog_vars) + if block: + self.orbit_fetch_future.result() + if self.orbit_fetch_future.done(): + ret = self.orbit_fetch_future.result() if ret: self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret - self.orbit_p.join() - self.orbit_p = None - - def __del__(self): - if self.orbit_p is not None: - self.orbit_p.kill() + self.orbit_fetch_future = None + self.last_fetch_orbits_t = t + + +def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): + astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) + cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") + start_time = time.monotonic() + data = None + try: + astro_dog.get_orbit_data(t, only_predictions=True) + data = (astro_dog.orbits, astro_dog.orbit_fetched_times) + except RuntimeError as e: + cloudlog.info(f"No orbit data found. {e}") + cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") + return data def create_measurement_msg(meas: GNSSMeasurement): diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 7a8c1ecb13564d..7dc803d79919e1 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -69,29 +69,46 @@ def test_laika_offline(self, downloader_mock): self.assertEqual(256, len(correct_msgs)) self.assertEqual(256, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) - def test_laika_get_orbits(self): - laikad = Laikad(auto_update=False) - first_gps_time = None + def get_first_gps_time(self): for m in self.logs: if m.ubloxGnss.which == 'measurementReport': new_meas = read_raw_ublox(m.ubloxGnss.measurementReport) if len(new_meas) != 0: - first_gps_time = new_meas[0].recv_time - break + return new_meas[0].recv_time + + def test_laika_get_orbits(self): + laikad = Laikad(auto_update=False) + first_gps_time = self.get_first_gps_time() # Pretend process has loaded the orbits on startup by using the time of the first gps message. laikad.fetch_orbits(first_gps_time, block=True) - self.assertEqual(29, len(laikad.astro_dog.orbits.keys())) + self.assertEqual(29, len(laikad.astro_dog.orbits.values())) + self.assertGreater(min([len(v) for v in laikad.astro_dog.orbits.values()]), 0) @unittest.skip("Use to debug live data") def test_laika_get_orbits_now(self): laikad = Laikad(auto_update=False) laikad.fetch_orbits(GPSTime.from_datetime(datetime.utcnow()), block=True) prn = "G01" - self.assertLess(0, len(laikad.astro_dog.orbits[prn])) + self.assertGreater(len(laikad.astro_dog.orbits[prn]), 0) prn = "R01" - self.assertLess(0, len(laikad.astro_dog.orbits[prn])) + self.assertGreater(len(laikad.astro_dog.orbits[prn]), 0) print(min(laikad.astro_dog.orbits[prn], key=lambda e: e.epoch).epoch.as_datetime()) + def test_get_orbits_in_process(self): + laikad = Laikad(auto_update=False) + has_orbits = False + for m in self.logs: + laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime, block=False) + if laikad.orbit_fetch_future is not None: + laikad.orbit_fetch_future.result() + vals = laikad.astro_dog.orbits.values() + has_orbits = len(vals) > 0 and max([len(v) for v in vals]) > 0 + if has_orbits: + break + self.assertTrue(has_orbits) + self.assertGreater(len(laikad.astro_dog.orbit_fetched_times._ranges), 0) + self.assertEqual(None, laikad.orbit_fetch_future) + if __name__ == "__main__": unittest.main() From fb068f04f50fcde6906072b4963114146852c51d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 13 Jun 2022 16:43:50 +0200 Subject: [PATCH 066/436] camerad: remove unused SubMaster (#24844) --- selfdrive/camerad/cameras/camera_qcom2.cc | 7 ++----- selfdrive/camerad/cameras/camera_qcom2.h | 1 - selfdrive/camerad/test/camera_test.h | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/selfdrive/camerad/cameras/camera_qcom2.cc b/selfdrive/camerad/cameras/camera_qcom2.cc index d43beb0921e0f0..c1ffc1275a178d 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.cc +++ b/selfdrive/camerad/cameras/camera_qcom2.cc @@ -837,7 +837,6 @@ void cameras_init(VisionIpcServer *v, MultiCameraState *s, cl_device_id device_i s->road_cam.camera_init(s, v, CAMERA_ID_AR0231, 1, 20, device_id, ctx, VISION_STREAM_RGB_ROAD, VISION_STREAM_ROAD, !env_disable_road); s->wide_road_cam.camera_init(s, v, CAMERA_ID_AR0231, 0, 20, device_id, ctx, VISION_STREAM_RGB_WIDE_ROAD, VISION_STREAM_WIDE_ROAD, !env_disable_wide_road); - s->sm = new SubMaster({"driverState"}); s->pm = new PubMaster({"roadCameraState", "driverCameraState", "wideRoadCameraState", "thumbnail"}); } @@ -948,7 +947,6 @@ void cameras_close(MultiCameraState *s) { s->road_cam.camera_close(); s->wide_road_cam.camera_close(); - delete s->sm; delete s->pm; } @@ -1221,7 +1219,7 @@ static void ar0231_process_registers(MultiCameraState *s, CameraState *c, cereal framed.setTemperaturesC({temp_0, temp_1}); } -static void driver_cam_auto_exposure(CameraState *c, SubMaster &sm) { +static void driver_cam_auto_exposure(CameraState *c) { struct ExpRect {int x1, x2, x_skip, y1, y2, y_skip;}; const CameraBuf *b = &c->buf; static ExpRect rect = {96, 1832, 2, 242, 1148, 4}; @@ -1229,8 +1227,7 @@ static void driver_cam_auto_exposure(CameraState *c, SubMaster &sm) { } static void process_driver_camera(MultiCameraState *s, CameraState *c, int cnt) { - s->sm->update(0); - driver_cam_auto_exposure(c, *(s->sm)); + driver_cam_auto_exposure(c); MessageBuilder msg; auto framed = msg.initEvent().initDriverCameraState(); diff --git a/selfdrive/camerad/cameras/camera_qcom2.h b/selfdrive/camerad/cameras/camera_qcom2.h index d869620e9ae7d4..88766a68e99e6a 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.h +++ b/selfdrive/camerad/cameras/camera_qcom2.h @@ -106,6 +106,5 @@ typedef struct MultiCameraState { CameraState wide_road_cam; CameraState driver_cam; - SubMaster *sm; PubMaster *pm; } MultiCameraState; diff --git a/selfdrive/camerad/test/camera_test.h b/selfdrive/camerad/test/camera_test.h index cc521db39769b2..a9f213c923a40c 100644 --- a/selfdrive/camerad/test/camera_test.h +++ b/selfdrive/camerad/test/camera_test.h @@ -19,7 +19,6 @@ typedef struct MultiCameraState { CameraState road_cam; CameraState driver_cam; - SubMaster *sm = nullptr; PubMaster *pm = nullptr; } MultiCameraState; From 7bca95dbb89dc6b969871cc7f6bf4306d91f60cc Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 13 Jun 2022 16:44:38 +0200 Subject: [PATCH 067/436] navd: speed limits only when localizer is valid (#24845) --- selfdrive/navd/navd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/selfdrive/navd/navd.py b/selfdrive/navd/navd.py index fcd53d8b60cd02..509653a46daaf6 100755 --- a/selfdrive/navd/navd.py +++ b/selfdrive/navd/navd.py @@ -32,6 +32,7 @@ def __init__(self, sm, pm): self.last_bearing = None self.gps_ok = False + self.localizer_valid = False self.nav_destination = None self.step_idx = None @@ -73,9 +74,9 @@ def update_location(self): location = self.sm['liveLocationKalman'] self.gps_ok = location.gpsOK - localizer_valid = (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid + self.localizer_valid = (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid - if localizer_valid: + if self.localizer_valid: self.last_bearing = math.degrees(location.calibratedOrientationNED.value[2]) self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1]) @@ -202,7 +203,7 @@ def send_instruction(self): if along_geometry < distance_along_geometry(geometry, geometry[closest_idx]): closest = geometry[closest_idx - 1] - if 'maxspeed' in closest.annotations: + if ('maxspeed' in closest.annotations) and self.localizer_valid: msg.navInstruction.speedLimit = closest.annotations['maxspeed'] # Speed limit sign type From 566de12671f7fbf299306339c0ca901754f7f3ca Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 13 Jun 2022 09:29:28 -0700 Subject: [PATCH 068/436] Toyota Camry TSS2: update torque control params (#24819) Use updated accel and friction values for TSS2 Camry --- selfdrive/car/toyota/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 83eed611c31f42..53accecfe0e6cb 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -31,6 +31,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.stoppingControl = False # Toyota starts braking more when it thinks you want to stop stop_and_go = False + torque_params = CarInterfaceBase.get_torque_params(candidate) if candidate == CAR.PRIUS: stop_and_go = True @@ -91,8 +92,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl tire_stiffness_factor = 0.7933 ret.mass = 3400. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid if candidate in (CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2): - ret.maxLateralAccel = 2.4 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.05) + set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) else: set_lat_tune(ret.lateralTuning, LatTunes.PID_C) From f05166ae26eb8165f35e8205b59c36e618d179b8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 13 Jun 2022 13:58:22 -0700 Subject: [PATCH 069/436] ui.py: update for nv12 --- tools/replay/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 1807fb77476a85..bbcd49061e7a5d 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -118,7 +118,7 @@ def ui_thread(addr): imgff = np.frombuffer(yuv_img_raw, dtype=np.uint8).reshape((vipc_client.height * 3 // 2, vipc_client.width)) num_px = vipc_client.width * vipc_client.height - bgr = cv2.cvtColor(imgff, cv2.COLOR_YUV2RGB_I420) + bgr = cv2.cvtColor(imgff, cv2.COLOR_YUV2RGB_NV12) zoom_matrix = _BB_TO_FULL_FRAME[num_px] cv2.warpAffine(bgr, zoom_matrix[:2], (img.shape[1], img.shape[0]), dst=img, flags=cv2.WARP_INVERSE_MAP) From cbd404b954b5b80b65f1ad8363b0245c3c7c7911 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 13 Jun 2022 16:38:32 -0700 Subject: [PATCH 070/436] Revert "thermald: consider pmic in component temp management (#24708)" This reverts commit c8c21baf50865b0db2cc9345901bb98904cfbaaf. --- selfdrive/thermald/thermald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 403e9cb23286d7..b909d1198cdf49 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -249,7 +249,7 @@ def thermald_thread(end_event, hw_queue): current_filter.update(msg.deviceState.batteryCurrent / 1e6) max_comp_temp = temp_filter.update( - max(max(msg.deviceState.cpuTempC), msg.deviceState.memoryTempC, max(msg.deviceState.gpuTempC), max(msg.deviceState.pmicTempC)) + max(max(msg.deviceState.cpuTempC), msg.deviceState.memoryTempC, max(msg.deviceState.gpuTempC)) ) if fan_controller is not None: From be13fc71f1237eb56a9d1cf2d7a0feb11b55d0b7 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Mon, 13 Jun 2022 16:54:32 -0700 Subject: [PATCH 071/436] Couple more cars to torque tune (#24848) * try sonata on torque tune * Couple known cars to torque control * fix * more fix --- selfdrive/car/hyundai/interface.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 08af65499646fb..6fd75ebfabc084 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -55,7 +55,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.stopAccel = 0.0 ret.longitudinalActuatorDelayUpperBound = 1.0 # s - + torque_params = CarInterfaceBase.get_torque_params(candidate) if candidate in (CAR.SANTA_FE, CAR.SANTA_FE_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022): ret.lateralTuning.pid.kf = 0.00005 ret.mass = 3982. * CV.LB_TO_KG + STD_CARGO_KG @@ -66,13 +66,11 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[9., 22.], [9., 22.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.35], [0.05, 0.09]] elif candidate in (CAR.SONATA, CAR.SONATA_HYBRID): - ret.lateralTuning.pid.kf = 0.00005 ret.mass = 1513. + STD_CARGO_KG ret.wheelbase = 2.84 ret.steerRatio = 13.27 * 1.15 # 15% higher at the center seems reasonable tire_stiffness_factor = 0.65 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) elif candidate == CAR.SONATA_LF: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 4497. * CV.LB_TO_KG @@ -98,21 +96,17 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] ret.minSteerSpeed = 32 * CV.MPH_TO_MS elif candidate == CAR.ELANTRA_2021: - ret.lateralTuning.pid.kf = 0.00005 ret.mass = (2800. * CV.LB_TO_KG) + STD_CARGO_KG ret.wheelbase = 2.72 ret.steerRatio = 12.9 tire_stiffness_factor = 0.65 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) elif candidate == CAR.ELANTRA_HEV_2021: - ret.lateralTuning.pid.kf = 0.00005 ret.mass = (3017. * CV.LB_TO_KG) + STD_CARGO_KG ret.wheelbase = 2.72 ret.steerRatio = 12.9 tire_stiffness_factor = 0.65 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) elif candidate == CAR.HYUNDAI_GENESIS: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 2060. + STD_CARGO_KG @@ -210,13 +204,12 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralTuning.indi.actuatorEffectivenessBP = [0.] ret.lateralTuning.indi.actuatorEffectivenessV = [1.8] elif candidate in (CAR.KIA_OPTIMA, CAR.KIA_OPTIMA_H): - ret.lateralTuning.pid.kf = 0.00005 ret.mass = 3558. * CV.LB_TO_KG ret.wheelbase = 2.80 ret.steerRatio = 13.75 tire_stiffness_factor = 0.5 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.25], [0.05]] + torque_params = CarInterfaceBase.get_torque_params(CAR.KIA_OPTIMA) + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) elif candidate == CAR.KIA_STINGER: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 1825. + STD_CARGO_KG @@ -258,7 +251,8 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl tire_stiffness_factor = 0.65 ret.maxLateralAccel = 2. - set_torque_tune(ret.lateralTuning, ret.maxLateralAccel, 0.01) + # TODO override until there is more data + set_torque_tune(ret.lateralTuning, 2.0, 0.05) # Genesis elif candidate == CAR.GENESIS_G70: From 25eafa96264aa2f8851b2f0d633de0c7cce3e743 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 13 Jun 2022 18:42:47 -0700 Subject: [PATCH 072/436] Honda Bosch long: fix ACC fault (#24851) Fix Honda bosch long Co-authored-by: redacid95 Co-authored-by: redacid95 --- selfdrive/car/honda/hondacan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 9c9e873e18ba63..3d8c79c80911e9 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -115,7 +115,7 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc } if CP.carFingerprint in HONDA_BOSCH: - acc_hud_values['ACC_ON'] = hud.car != 0 + acc_hud_values['ACC_ON'] = int(enabled) acc_hud_values['FCM_OFF'] = 1 acc_hud_values['FCM_OFF_2'] = 1 else: From dbfe923ecc0e14a781a7f03646ccf885ab2fa47a Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Mon, 13 Jun 2022 19:08:09 -0700 Subject: [PATCH 073/436] Move couple toyotas to torque table values (#24849) * Move couple toyotas to torque table values * Dont set for all cars yet * Dont regress docs * update ref --- selfdrive/car/toyota/interface.py | 24 ++++++++++++------------ selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 53accecfe0e6cb..9ca9e4e11b5108 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -2,6 +2,7 @@ from cereal import car from common.conversions import Conversions as CV from panda import Panda +from selfdrive.controls.lib.latcontrol_torque import set_torque_tune from selfdrive.car.toyota.tunes import LatTunes, LongTunes, set_long_tune, set_lat_tune from selfdrive.car.toyota.values import Ecu, CAR, ToyotaFlags, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, MIN_ACC_SPEED, EPS_SCALE, EV_HYBRID_CAR, CarControllerParams from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config @@ -32,6 +33,8 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl stop_and_go = False torque_params = CarInterfaceBase.get_torque_params(candidate) + steering_angle_deadzone_deg = 0.0 + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) if candidate == CAR.PRIUS: stop_and_go = True @@ -39,8 +42,11 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 15.74 # unknown end-to-end spec tire_stiffness_factor = 0.6371 # hand-tune ret.mass = 3045. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 1.7 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.06, steering_angle_deadzone_deg=1.0) + # Only give steer angle deadzone to for bad angle sensor prius + for fw in car_fw: + if fw.ecu == "eps" and not fw.fwVersion == b'8965B47060\x00\x00\x00\x00\x00\x00': + steering_angle_deadzone_deg = 1.0 + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) elif candidate == CAR.PRIUS_V: stop_and_go = True @@ -48,8 +54,10 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 17.4 tire_stiffness_factor = 0.5533 ret.mass = 3340. * CV.LB_TO_KG + STD_CARGO_KG + # TODO override until there is enough data ret.maxLateralAccel = 1.8 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.06) + torque_params = CarInterfaceBase.get_torque_params(CAR.PRIUS) + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) elif candidate in (CAR.RAV4, CAR.RAV4H): stop_and_go = True if (candidate in CAR.RAV4H) else False @@ -57,16 +65,12 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 16.88 # 14.5 is spec end-to-end tire_stiffness_factor = 0.5533 ret.mass = 3650. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid - ret.maxLateralAccel = 1.8 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.06) elif candidate == CAR.COROLLA: ret.wheelbase = 2.70 ret.steerRatio = 18.27 tire_stiffness_factor = 0.444 # not optimized yet ret.mass = 2860. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid - ret.maxLateralAccel = 2.8 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.024) elif candidate in (CAR.LEXUS_RX, CAR.LEXUS_RXH, CAR.LEXUS_RX_TSS2, CAR.LEXUS_RXH_TSS2): stop_and_go = True @@ -91,9 +95,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 13.7 tire_stiffness_factor = 0.7933 ret.mass = 3400. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid - if candidate in (CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2): - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) - else: + if candidate not in (CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2): set_lat_tune(ret.lateralTuning, LatTunes.PID_C) elif candidate in (CAR.HIGHLANDER_TSS2, CAR.HIGHLANDERH_TSS2): @@ -143,8 +145,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 13.9 tire_stiffness_factor = 0.444 # not optimized yet ret.mass = 3060. * CV.LB_TO_KG + STD_CARGO_KG - ret.maxLateralAccel = 2.0 - set_lat_tune(ret.lateralTuning, LatTunes.TORQUE, MAX_LAT_ACCEL=ret.maxLateralAccel, FRICTION=0.07) elif candidate in (CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.LEXUS_ESH): stop_and_go = True diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 613b7f26aef06b..61bbd5cdd13400 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -123506cad1877e93bfe5c91ecdce654ef339959b +fe2da24194e3def1823681cc18e7879f24edfc6e From a78197ab1af40dcdc0fe5a8f764b476da3837e87 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 13 Jun 2022 21:49:50 -0700 Subject: [PATCH 074/436] Move RAV4 2022 out of bronze --- docs/CARS.md | 6 +++--- selfdrive/car/toyota/interface.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 479c519dabca51..9b3322ac8eb0db 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -68,7 +68,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 75 cars +# Silver - 76 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -130,6 +130,7 @@ How We Rate The Cars |Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| |Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| |Toyota|Prius Prime 2017-20|All|[3](#footnotes)||||| +|Toyota|RAV4 2022|All|||||| |Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 Hybrid 2022|All|||||| |Toyota|Sienna 2018-20|All|[3](#footnotes)||||| @@ -148,7 +149,7 @@ How We Rate The Cars |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 70 cars +# Bronze - 69 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -214,7 +215,6 @@ How We Rate The Cars |Toyota|Corolla 2017-19|All|[3](#footnotes)||||| |Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|RAV4 2022|All|||||| |Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 9ca9e4e11b5108..7969775a89bfb8 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -132,6 +132,10 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 3585. * CV.LB_TO_KG + STD_CARGO_KG # Average between ICE and Hybrid set_lat_tune(ret.lateralTuning, LatTunes.PID_D) + # TODO: remove once there's data + if candidate == CAR.RAV4_TSS2_2022: + ret.maxLateralAccel = CarInterfaceBase.get_torque_params(CAR.RAV4H_TSS2_2022)['MAX_LAT_ACCEL_MEASURED'] + # 2019+ RAV4 TSS2 uses two different steering racks and specific tuning seems to be necessary. # See https://github.com/commaai/openpilot/pull/21429#issuecomment-873652891 for fw in car_fw: From 1ba8022c184a7e643c2714aaad1aa45e5e68501c Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Mon, 13 Jun 2022 22:26:31 -0700 Subject: [PATCH 075/436] pin protobuf and hypothesis versions (#24852) pin hypothesis and protobuf --- Pipfile | 3 ++- Pipfile.lock | 58 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Pipfile b/Pipfile index 2ffe60e0fe0900..672692fe40b8f0 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ coverage = "*" dictdiffer = "*" fastcluster = "*" hexdump = "*" -hypothesis = "*" +hypothesis = "==6.46.7" inputs = "*" lru-dict = "*" markdown-it-py = "*" @@ -58,6 +58,7 @@ json-rpc = "*" libusb1 = "*" nose = "*" numpy = "*" +protobuf = "==3.20.1" onnx = "*" onnxruntime-gpu = {version = "*", markers="platform_system != 'Darwin'"} pillow = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 25a97890d36811..ee0fbc7e843bfe 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "eb1eeeaabf1266fab353532d10e4d9ae36306a7577f248513909c2d6096ab1c9" + "sha256": "7af354a7ae976b12da1b5f15f0cebdc77595199c11b0bfeb8be9c2827f335c48" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "astroid": { "hashes": [ - "sha256:14ffbb4f6aa2cf474a0834014005487f7ecd8924996083ab411e7fa0b508ce0b", - "sha256:f4e4ec5294c4b07ac38bab9ca5ddd3914d4bf46f9006eb5c0ae755755061044e" + "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6", + "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9" ], "markers": "python_full_version >= '3.6.2'", - "version": "==2.11.5" + "version": "==2.11.6" }, "atomicwrites": { "hashes": [ @@ -371,7 +371,7 @@ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], - "markers": "python_version < '4' and python_full_version >= '3.6.1'", + "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "itsdangerous": { @@ -687,23 +687,47 @@ }, "protobuf": { "hashes": [ + "sha256:06059eb6953ff01e56a25cd02cca1a9649a75a7e65397b5b9b4e929ed71d10cf", + "sha256:097c5d8a9808302fb0da7e20edf0b8d4703274d140fd25c5edabddcde43e081f", "sha256:0d4719e724472e296062ba8e82a36d64693fcfdb550d9dff98af70ca79eafe3d", + "sha256:284f86a6207c897542d7e956eb243a36bb8f9564c1742b253462386e96c6b78f", "sha256:2b35602cb65d53c168c104469e714bf68670335044c38eee3c899d6a8af03ffc", + "sha256:32ca378605b41fd180dfe4e14d3226386d8d1b002ab31c969c366549e66a2bb7", "sha256:32fff501b6df3050936d1839b80ea5899bf34db24792d223d7640611f67de15a", "sha256:34400fd76f85bdae9a2e9c1444ea4699c0280962423eff4418765deceebd81b5", "sha256:3767c64593a49c7ac0accd08ed39ce42744405f0989d468f0097a17496fdbe84", + "sha256:3cc797c9d15d7689ed507b165cd05913acb992d78b379f6014e013f9ecb20996", "sha256:3f2ed842e8ca43b790cb4a101bcf577226e0ded98a6a6ba2d5e12095a08dc4da", "sha256:52c1e44e25f2949be7ffa7c66acbfea940b0945dd416920231f7cb30ea5ac6db", "sha256:5d9b5c8270461706973c3871c6fbdd236b51dfe9dab652f1fb6a36aa88287e47", + "sha256:62f1b5c4cd6c5402b4e2d63804ba49a327e0c386c99b1675c8a0fefda23b2067", + "sha256:69ccfdf3657ba59569c64295b7d51325f91af586f8d5793b734260dfe2e94e2c", + "sha256:6f50601512a3d23625d8a85b1638d914a0970f17920ff39cec63aaef80a93fb7", "sha256:72d357cc4d834cc85bd957e8b8e1f4b64c2eac9ca1a942efeb8eb2e723fca852", + "sha256:7403941f6d0992d40161aa8bb23e12575637008a5a02283a930addc0508982f9", + "sha256:755f3aee41354ae395e104d62119cb223339a8f3276a0cd009ffabfcdd46bb0c", + "sha256:77053d28427a29987ca9caf7b72ccafee011257561259faba8dd308fda9a8739", "sha256:79cd8d0a269b714f6b32641f86928c718e8d234466919b3f552bfb069dbb159b", + "sha256:7e371f10abe57cee5021797126c93479f59fccc9693dafd6bd5633ab67808a91", + "sha256:9016d01c91e8e625141d24ec1b20fed584703e527d28512aa8c8707f105a683c", + "sha256:9be73ad47579abc26c12024239d3540e6b765182a91dbc88e23658ab71767153", "sha256:a4c0c6f2f95a559e59a0258d3e4b186f340cbdc5adec5ce1bc06d01972527c88", + "sha256:adc31566d027f45efe3f44eeb5b1f329da43891634d61c75a5944e9be6dd42c9", + "sha256:adfc6cf69c7f8c50fd24c793964eef18f0ac321315439d94945820612849c388", + "sha256:af0ebadc74e281a517141daad9d0f2c5d93ab78e9d455113719a45a49da9db4e", "sha256:b309fda192850ac4184ca1777aab9655564bc8d10a9cc98f10e1c8bf11295c22", "sha256:b3d7d4b4945fe3c001403b6c24442901a5e58c0a3059290f5a63523ed4435f82", - "sha256:c8829092c5aeb61619161269b2f8a2e36fd7cb26abbd9282d3bc453f02769146" + "sha256:c8829092c5aeb61619161269b2f8a2e36fd7cb26abbd9282d3bc453f02769146", + "sha256:cb29edb9eab15742d791e1025dd7b6a8f6fcb53802ad2f6e3adcb102051063ab", + "sha256:cd68be2559e2a3b84f517fb029ee611546f7812b1fdd0aa2ecc9bc6ec0e4fdde", + "sha256:cdee09140e1cd184ba9324ec1df410e7147242b94b5f8b0c64fc89e38a8ba531", + "sha256:db977c4ca738dd9ce508557d4fce0f5aebd105e158c725beec86feb1f6bc20d8", + "sha256:dd5789b2948ca702c17027c84c2accb552fc30f4622a98ab5c51fcfe8c50d3e7", + "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20", + "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3" ], - "markers": "python_version >= '3.7'", - "version": "==4.21.1" + "index": "pypi", + "version": "==3.20.1" }, "psutil": { "hashes": [ @@ -1155,11 +1179,11 @@ }, "setuptools": { "hashes": [ - "sha256:1f5d3a1502812025cdb2e5609b6af2d207332e3f50febe6db10ed3a59b2f155f", - "sha256:30b6b0fbacc459c90d27a63e6173facfc8b8c99a48fb24b5044f459ba63cd6cf" + "sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77", + "sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91" ], "markers": "python_version >= '3.7'", - "version": "==62.3.4" + "version": "==62.4.0" }, "six": { "hashes": [ @@ -1206,7 +1230,7 @@ "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1", "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd" ], - "markers": "python_version >= '3.6' and python_version < '4'", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==0.11.0" }, "tqdm": { @@ -1222,7 +1246,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "urllib3": { @@ -1733,11 +1757,13 @@ }, "hypothesis": { "hashes": [ + "sha256:2696cdb9005946bf1d2b215cc91d3fc01625e3342eb8743ddd04b667b2f1882b", "sha256:4ad26c5d434171ffc02aba569dd52255573d615554c062bc30734dbe6f318c61", - "sha256:69978811f1d9c19710c7d2bf8233dc43c80efa964251b72efbe8274044e073b4" + "sha256:69978811f1d9c19710c7d2bf8233dc43c80efa964251b72efbe8274044e073b4", + "sha256:967009fa561b3a3f8363a73d71923357271c37dc7fa27b30c2d21a1b6092b240" ], "index": "pypi", - "version": "==6.47.1" + "version": "==6.46.7" }, "identify": { "hashes": [ @@ -2546,7 +2572,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "urllib3": { From a9038bc3fd0ed12b441804923c46927b5f6bbbfe Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 13 Jun 2022 22:27:44 -0700 Subject: [PATCH 076/436] bump opendbc (#24853) --- opendbc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc b/opendbc index 58a2c9b2fc5c60..a7b391cce88908 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 58a2c9b2fc5c60ca68d3dced09f6c4c72ca72415 +Subproject commit a7b391cce88908824f1417f0c7abd35e3ae16f96 From 170ed3d761a58222a626c86b9706f9be4900d058 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 14 Jun 2022 12:03:30 -0700 Subject: [PATCH 077/436] process replay: clean up common code (#24855) * regen and process replay clean up * test_fuzzy actually uses fingerprint hardcoding fix * revert * revert * this can be a url or path so just print full variable --- .../test/process_replay/process_replay.py | 35 ++++++++----------- selfdrive/test/process_replay/regen.py | 19 +--------- selfdrive/test/process_replay/regen_all.py | 14 ++++---- .../test/process_replay/test_processes.py | 3 +- 4 files changed, 24 insertions(+), 47 deletions(-) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 1eda9bc7f59c89..363035ab224ec6 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -347,7 +347,7 @@ def replay_process(cfg, lr, fingerprint=None): else: return cpp_replay_process(cfg, lr, fingerprint) -def setup_env(simulation=False): +def setup_env(simulation=False, CP=None): params = Params() params.clear_all() params.put_bool("OpenpilotEnabledToggle", True) @@ -358,12 +358,22 @@ def setup_env(simulation=False): os.environ["NO_RADAR_SLEEP"] = "1" os.environ["REPLAY"] = "1" + os.environ['SKIP_FW_QUERY'] = "" + os.environ['FINGERPRINT'] = "" if simulation: os.environ["SIMULATION"] = "1" elif "SIMULATION" in os.environ: del os.environ["SIMULATION"] + # Regen or python process + if CP is not None: + if CP.fingerprintSource == "fw" and CP.carFingerprint in FW_VERSIONS: + params.put("CarParamsCache", CP.as_builder().to_bytes()) + else: + os.environ['SKIP_FW_QUERY'] = "1" + os.environ['FINGERPRINT'] = CP.carFingerprint + def python_replay_process(cfg, lr, fingerprint=None): sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] pub_sockets = [s for s in cfg.pub_sub.keys() if s != 'can'] @@ -378,30 +388,13 @@ def python_replay_process(cfg, lr, fingerprint=None): all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) pub_msgs = [msg for msg in all_msgs if msg.which() in list(cfg.pub_sub.keys())] - setup_env() - - # TODO: remove after getting new route for civic & accord - migration = { - "HONDA CIVIC 2016 TOURING": "HONDA CIVIC 2016", - "HONDA ACCORD 2018 SPORT 2T": "HONDA ACCORD 2018", - "HONDA ACCORD 2T 2018": "HONDA ACCORD 2018", - "Mazda CX-9 2021": "MAZDA CX-9 2021", - } - if fingerprint is not None: os.environ['SKIP_FW_QUERY'] = "1" os.environ['FINGERPRINT'] = fingerprint + setup_env() else: - os.environ['SKIP_FW_QUERY'] = "" - os.environ['FINGERPRINT'] = "" - for msg in lr: - if msg.which() == 'carParams': - car_fingerprint = migration.get(msg.carParams.carFingerprint, msg.carParams.carFingerprint) - if msg.carParams.fingerprintSource == "fw" and (car_fingerprint in FW_VERSIONS): - Params().put("CarParamsCache", msg.carParams.as_builder().to_bytes()) - else: - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['FINGERPRINT'] = car_fingerprint + CP = [m for m in lr if m.which() == 'carParams'][0].carParams + setup_env(CP=CP) assert(type(managed_processes[cfg.proc_name]) is PythonProcess) managed_processes[cfg.proc_name].prepare() diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index 653efaf32cfee2..dfc62a4558e0b9 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -15,7 +15,6 @@ from common.params import Params from common.realtime import Ratekeeper, DT_MDL, DT_DMON, sec_since_boot from common.transformations.camera import eon_f_frame_size, eon_d_frame_size, tici_f_frame_size, tici_d_frame_size -from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.manager.process import ensure_running from selfdrive.manager.process_config import managed_processes from selfdrive.test.process_replay.process_replay import FAKEDATA, setup_env, check_enabled @@ -181,28 +180,12 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): if frs is None: frs = dict() - setup_env() params = Params() - os.environ["LOG_ROOT"] = outdir - os.environ['SKIP_FW_QUERY'] = "" - os.environ['FINGERPRINT'] = "" - - # TODO: remove after getting new route for Mazda - fp_migration = { - "Mazda CX-9 2021": "MAZDA CX-9 2021", - } - # TODO: remove after getting new route for Subaru - fingerprint_problem = ["SUBARU IMPREZA LIMITED 2019"] for msg in lr: if msg.which() == 'carParams': - car_fingerprint = fp_migration.get(msg.carParams.carFingerprint, msg.carParams.carFingerprint) - if len(msg.carParams.carFw) and (car_fingerprint in FW_VERSIONS) and (car_fingerprint not in fingerprint_problem): - params.put("CarParamsCache", msg.carParams.as_builder().to_bytes()) - else: - os.environ['SKIP_FW_QUERY'] = "1" - os.environ['FINGERPRINT'] = car_fingerprint + setup_env(CP=msg.carParams) elif msg.which() == 'liveCalibration': params.put("CalibrationParams", msg.as_builder().to_bytes()) diff --git a/selfdrive/test/process_replay/regen_all.py b/selfdrive/test/process_replay/regen_all.py index 765e5c3b682c90..c3ea1d41e1a8b9 100755 --- a/selfdrive/test/process_replay/regen_all.py +++ b/selfdrive/test/process_replay/regen_all.py @@ -8,26 +8,28 @@ from selfdrive.test.process_replay.helpers import OpenpilotPrefix from selfdrive.test.process_replay.regen import regen_and_save from selfdrive.test.process_replay.test_processes import FAKEDATA, original_segments as segments +from tools.lib.route import SegmentName -def regen_job(segment): + +def regen_job(segment, disable_tqdm): with OpenpilotPrefix(): - route = segment[1].rsplit('--', 1)[0] - sidx = int(segment[1].rsplit('--', 1)[1]) - fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for i in range(11)) + sn = SegmentName(segment[1]) + fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for _ in range(11)) try: - relr = regen_and_save(route, sidx, upload=True, use_route_meta=False, outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=True) + relr = regen_and_save(sn.route_name.canonical_name, sn.segment_num, upload=True, use_route_meta=False, outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm) relr = '|'.join(relr.split('/')[-2:]) return f' ("{segment[0]}", "{relr}"), ' except Exception as e: return f" {segment} failed: {str(e)}" + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Generate new segments from old ones") parser.add_argument("-j", "--jobs", type=int, default=1) args = parser.parse_args() with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: - p = list(pool.map(regen_job, segments)) + p = list(pool.map(regen_job, segments, [args.jobs > 1] * args.jobs)) msg = "Copy these new segments into test_processes.py:" for seg in tqdm(p, desc="Generating segments"): msg += "\n" + str(seg) diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 9f83a08e23355b..25fbd210cc0a72 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -93,8 +93,7 @@ def test_process(cfg, lr, ref_log_path, ignore_fields=None, ignore_msgs=None): # check to make sure openpilot is engaged in the route if cfg.proc_name == "controlsd": if not check_enabled(log_msgs): - segment = os.path.basename(ref_log_path).split("/")[-1].split("_")[0] - raise Exception(f"Route never enabled: {segment}") + raise Exception(f"Route never enabled: {ref_log_path}") try: return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs From 7a6f57a28e6efda72319fc0b2a4873bcf7e06829 Mon Sep 17 00:00:00 2001 From: George Hotz <72895+geohot@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:46:03 -0700 Subject: [PATCH 078/436] remove weights fixup with new SNPE (#24254) * remove weights fixup with new SNPE * Update ref Co-authored-by: Comma Device Co-authored-by: Harald Schafer --- selfdrive/modeld/SConscript | 12 ++---------- .../test/process_replay/model_replay_ref_commit | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 1f1c661c8be3e9..3e9738d8648b3b 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -65,22 +65,14 @@ common_model = lenv.Object(common_src) if use_thneed and arch == "larch64": fn = File("models/supercombo").abspath compiler = lenv.Program('thneed/compile', ["thneed/compile.cc"]+common_model, LIBS=libs) - cmd = f"cd {Dir('.').abspath} && {compiler[0].abspath} --in {fn}.dlc --out {fn}_badweights.thneed --binary --optimize" + cmd = f"cd {Dir('.').abspath} && {compiler[0].abspath} --in {fn}.dlc --out {fn}.thneed --binary --optimize" lib_paths = ':'.join(Dir(p).abspath for p in lenv["LIBPATH"]) kernel_path = os.path.join(Dir('.').abspath, "thneed", "kernels") cenv = Environment(ENV={'LD_LIBRARY_PATH': f"{lib_paths}:{lenv['ENV']['LD_LIBRARY_PATH']}", 'KERNEL_PATH': kernel_path}) kernels = [os.path.join(kernel_path, x) for x in os.listdir(kernel_path) if x.endswith(".cl")] - cenv.Command(fn + "_badweights.thneed", [fn + ".dlc", kernels, compiler], cmd) - - from selfdrive.modeld.thneed.weights_fixup import weights_fixup - def weights_fixup_action(target, source, env): - weights_fixup(target[0].abspath, source[0].abspath, source[1].abspath) - - env = Environment(BUILDERS = {'WeightFixup' : Builder(action = weights_fixup_action)}) - env.WeightFixup(target=fn + ".thneed", source=[fn+"_badweights.thneed", fn+".dlc"]) - + cenv.Command(fn + ".thneed", [fn + ".dlc", kernels, compiler], cmd) lenv.Program('_dmonitoringmodeld', [ "dmonitoringmodeld.cc", diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 90520f2619e4d4..a7a909d2a4db24 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -f74ab97371be93fdc28333e5ea12bbb78c3a32d0 +512a9d4596c8faba304d6f7ded2ce77837357b65 From f8f4337fb3c50d6c125f59a8f59c93818fe71994 Mon Sep 17 00:00:00 2001 From: Jason Shuler Date: Tue, 14 Jun 2022 20:28:15 -0400 Subject: [PATCH 079/436] GM: add support for vehicles with manual parking brakes (#24766) Switch to general park brake signal --- selfdrive/car/gm/carstate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index e6a1c08ddab117..48b9f25444189f 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -64,7 +64,7 @@ def update(self, pt_cp, loopback_cp): ret.leftBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 1 ret.rightBlinker = pt_cp.vl["BCMTurnSignals"]["TurnSignals"] == 2 - ret.parkingBrake = pt_cp.vl["EPBStatus"]["EPBClosed"] == 1 + ret.parkingBrake = pt_cp.vl["VehicleIgnitionAlt"]["ParkBrake"] == 1 ret.cruiseState.available = pt_cp.vl["ECMEngineStatus"]["CruiseMainOn"] != 0 ret.espDisabled = pt_cp.vl["ESPStatus"]["TractionControlOn"] != 1 ret.accFaulted = pt_cp.vl["AcceleratorPedal2"]["CruiseState"] == AccState.FAULTED @@ -100,7 +100,7 @@ def get_can_parser(CP): ("LKATorqueDelivered", "PSCMStatus"), ("LKATorqueDeliveredStatus", "PSCMStatus"), ("TractionControlOn", "ESPStatus"), - ("EPBClosed", "EPBStatus"), + ("ParkBrake", "VehicleIgnitionAlt"), ("CruiseMainOn", "ECMEngineStatus"), ] @@ -110,7 +110,7 @@ def get_can_parser(CP): ("PSCMStatus", 10), ("ESPStatus", 10), ("BCMDoorBeltStatus", 10), - ("EPBStatus", 20), + ("VehicleIgnitionAlt", 10), ("EBCMWheelSpdFront", 20), ("EBCMWheelSpdRear", 20), ("AcceleratorPedal2", 33), From 6757e235f92b72741f2f4375548fde883ede324d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 14 Jun 2022 20:38:25 -0700 Subject: [PATCH 080/436] little zookeeper fixes (#24861) * little zookeeper fixes * bump that up --- Pipfile | 1 + Pipfile.lock | 135 ++++++++++++++++++------------- tools/zookeeper/__init__.py | 7 +- tools/zookeeper/power_monitor.py | 28 +++---- tools/zookeeper/requirements.txt | 1 - 5 files changed, 98 insertions(+), 74 deletions(-) mode change 100755 => 100644 tools/zookeeper/__init__.py delete mode 100644 tools/zookeeper/requirements.txt diff --git a/Pipfile b/Pipfile index 672692fe40b8f0..b8545b1a2190ad 100644 --- a/Pipfile +++ b/Pipfile @@ -40,6 +40,7 @@ subprocess32 = "*" tenacity = "*" mpld3 = "*" carla = {version = "==0.9.13", markers="platform_system != 'Darwin'"} +ft4222 = "*" [packages] atomicwrites = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ee0fbc7e843bfe..cc347a60f45c67 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7af354a7ae976b12da1b5f15f0cebdc77595199c11b0bfeb8be9c2827f335c48" + "sha256": "d2629168f477b3a14f68f26e3b63ea9797b20599c066b2aa23a027bcee2ca40a" }, "pipfile-spec": 6, "requires": { @@ -146,7 +146,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "click": { @@ -347,7 +347,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "importlib-metadata": { @@ -1246,7 +1246,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.2.0" }, "urllib3": { @@ -1443,11 +1443,11 @@ }, "babel": { "hashes": [ - "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2", - "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13" + "sha256:7aed055f0c04c9e7f51a2f75261e41e1c804efa724cb65b60a970dd4448d469d", + "sha256:81a3beca4d0cd40a9cfb9e2adb2cf39261c2f959b92e7a74750befe5d79afd7b" ], "markers": "python_version >= '3.6'", - "version": "==2.10.1" + "version": "==2.10.2" }, "bcrypt": { "hashes": [ @@ -1565,7 +1565,7 @@ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==2.0.12" }, "control": { @@ -1748,6 +1748,33 @@ "markers": "python_version >= '3.7'", "version": "==4.33.3" }, + "ft4222": { + "hashes": [ + "sha256:1489b08e4042cb2b24894495c1c8514fa115122e9f8a739f665f0e4d8c53d3f4", + "sha256:1c71913f9fb862634fb77eb6efeea38b2b839e09f739aee5864b9549bcf1c4a9", + "sha256:1f9be0961bd7f3e0c1729c9692f602244e93898610611602ce1fa16059ac5bfa", + "sha256:208cde748fc2bf4a753e217ee2844e75d7d1b6d370a8937a2055f9de8906f933", + "sha256:372be3d7d04c0f6dfa62ff66b3d4be5c39d57c258ab562d9eb0d7cdd3a90ed0f", + "sha256:3ebf9380315c6af8f9f3bc5c5c7a5ae06498349c14a7b2e65d5067b20462d21b", + "sha256:422740efac86f3fdd971bb9a94d9974e56cfe062284a2be1a85db96dd0e77e5e", + "sha256:4414ef82a6321c5205ab0e3dab2bf072f14b4dd3262e5f392560adf107210d41", + "sha256:459894dcca7db71a0d58e6f3abb4c9dd67654c92ad9d934f1e32dc08bed87645", + "sha256:4b7e4c8b5e4a8fe769127d3db99b7b410468ffe2718a07f7b317e7dc1ad9ca4f", + "sha256:68e6b357ea2287fe0e8dac287efbac2f0ce9632d65e828d422d3a0604555c174", + "sha256:800a26a4a8fc854f41e4de41e45b6deafdeb78ae5eff48818f710bb2d94a8c11", + "sha256:8790e96b4c3f8d1fb768f689f1c437a59b20889e4726ed7457ff3d188f0a3274", + "sha256:9369b55393b2e1f58f8b8516bcf9cd6fd34c05e77377c4cc48be39a49cad7be5", + "sha256:a6236f05b8948cd9c113c7159e3fdde202a0f73dce6440da078fd2fc9e411123", + "sha256:b7f58cd16213a5c92503c530b301f071d67283c80c1e8cb43d6f3ec5c186df35", + "sha256:cc368b06b92529a11add37d14196d2e2aaae19c6ffc51b9457513a0d05ceae1a", + "sha256:d479c037b417ff289727112f1d4725563b78448d3765f457c8bf6884e4d83abd", + "sha256:dce00d513be811f738954c5593c52b533a02331e8a26983280dea3f4d864962f", + "sha256:eadcbc1dd20ed6d7d07dc53354ea137bcea30fb0889d74bedcd189cdb4f61c84", + "sha256:ec984bd7e9300e5f2e50823dcd5874d54aa3e7503e69afe7eb3b4cea77071084" + ], + "index": "pypi", + "version": "==1.4.1" + }, "hexdump": { "hashes": [ "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db" @@ -1778,7 +1805,7 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_version >= '3.5'", + "markers": "python_full_version >= '3.5.0'", "version": "==3.3" }, "imagesize": { @@ -1822,52 +1849,52 @@ }, "kiwisolver": { "hashes": [ - "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b", - "sha256:0c380bb5ae20d829c1a5473cfcae64267b73aaa4060adc091f6df1743784aae0", - "sha256:0d98dca86f77b851350c250f0149aa5852b36572514d20feeadd3c6b1efe38d0", - "sha256:0e45e780a74416ef2f173189ef4387e44b5494f45e290bcb1f03735faa6779bf", - "sha256:0e8afdf533b613122e4bbaf3c1e42c2a5e9e2d1dd3a0a017749a7658757cb377", - "sha256:1008346a7741620ab9cc6c96e8ad9b46f7a74ce839dbb8805ddf6b119d5fc6c2", - "sha256:1d1078ba770d6165abed3d9a1be1f9e79b61515de1dd00d942fa53bba79f01ae", - "sha256:1dcade8f6fe12a2bb4efe2cbe22116556e3b6899728d3b2a0d3b367db323eacc", - "sha256:240009fdf4fa87844f805e23f48995537a8cb8f8c361e35fda6b5ac97fcb906f", - "sha256:240c2d51d098395c012ddbcb9bd7b3ba5de412a1d11840698859f51d0e643c4f", - "sha256:262c248c60f22c2b547683ad521e8a3db5909c71f679b93876921549107a0c24", - "sha256:2e6cda72db409eefad6b021e8a4f964965a629f577812afc7860c69df7bdb84a", - "sha256:3c032c41ae4c3a321b43a3650e6ecc7406b99ff3e5279f24c9b310f41bc98479", - "sha256:42f6ef9b640deb6f7d438e0a371aedd8bef6ddfde30683491b2e6f568b4e884e", - "sha256:484f2a5f0307bc944bc79db235f41048bae4106ffa764168a068d88b644b305d", - "sha256:69b2d6c12f2ad5f55104a36a356192cfb680c049fe5e7c1f6620fc37f119cdc2", - "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8", - "sha256:6ece2e12e4b57bc5646b354f436416cd2a6f090c1dadcd92b0ca4542190d7190", - "sha256:71469b5845b9876b8d3d252e201bef6f47bf7456804d2fbe9a1d6e19e78a1e65", - "sha256:7f606d91b8a8816be476513a77fd30abe66227039bd6f8b406c348cb0247dcc9", - "sha256:7f88c4b8e449908eeddb3bbd4242bd4dc2c7a15a7aa44bb33df893203f02dc2d", - "sha256:81237957b15469ea9151ec8ca08ce05656090ffabc476a752ef5ad7e2644c526", - "sha256:89b57c2984f4464840e4b768affeff6b6809c6150d1166938ade3e22fbe22db8", - "sha256:8a830a03970c462d1a2311c90e05679da56d3bd8e78a4ba9985cb78ef7836c9f", - "sha256:8ae5a071185f1a93777c79a9a1e67ac46544d4607f18d07131eece08d415083a", - "sha256:8b6086aa6936865962b2cee0e7aaecf01ab6778ce099288354a7229b4d9f1408", - "sha256:8ec2e55bf31b43aabe32089125dca3b46fdfe9f50afbf0756ae11e14c97b80ca", - "sha256:8ff3033e43e7ca1389ee59fb7ecb8303abb8713c008a1da49b00869e92e3dd7c", - "sha256:91eb4916271655dfe3a952249cb37a5c00b6ba68b4417ee15af9ba549b5ba61d", - "sha256:9d2bb56309fb75a811d81ed55fbe2208aa77a3a09ff5f546ca95e7bb5fac6eff", - "sha256:a4e8f072db1d6fb7a7cc05a6dbef8442c93001f4bb604f1081d8c2db3ca97159", - "sha256:b1605c7c38cc6a85212dfd6a641f3905a33412e49f7c003f35f9ac6d71f67720", - "sha256:b3e251e5c38ac623c5d786adb21477f018712f8c6fa54781bd38aa1c60b60fc2", - "sha256:b978afdb913ca953cf128d57181da2e8798e8b6153be866ae2a9c446c6162f40", - "sha256:be9a650890fb60393e60aacb65878c4a38bb334720aa5ecb1c13d0dac54dd73b", - "sha256:c222f91a45da9e01a9bc4f760727ae49050f8e8345c4ff6525495f7a164c8973", - "sha256:c839bf28e45d7ddad4ae8f986928dbf5a6d42ff79760d54ec8ada8fb263e097c", - "sha256:cbb5eb4a2ea1ffec26268d49766cafa8f957fe5c1b41ad00733763fae77f9436", - "sha256:e348f1904a4fab4153407f7ccc27e43b2a139752e8acf12e6640ba683093dd96", - "sha256:e677cc3626287f343de751e11b1e8a5b915a6ac897e8aecdbc996cd34de753a0", - "sha256:f74f2a13af201559e3d32b9ddfc303c94ae63d63d7f4326d06ce6fe67e7a8255", - "sha256:fa4d97d7d2b2c082e67907c0b8d9f31b85aa5d3ba0d33096b7116f03f8061261", - "sha256:ffbdb9a96c536f0405895b5e21ee39ec579cb0ed97bdbd169ae2b55f41d73219" + "sha256:007799c7fa934646318fc128b033bb6e6baabe7fbad521bfb2279aac26225cd7", + "sha256:130c6c35eded399d3967cf8a542c20b671f5ba85bd6f210f8b939f868360e9eb", + "sha256:1858ad3cb686eccc7c6b7c5eac846a1cfd45aacb5811b2cf575e80b208f5622a", + "sha256:1ae7aa0784aeadfbd693c27993727792fbe1455b84d49970bad5886b42976b18", + "sha256:1d2c744aeedce22c122bb42d176b4aa6d063202a05a4abdacb3e413c214b3694", + "sha256:21a3a98f0a21fc602663ca9bce2b12a4114891bdeba2dea1e9ad84db59892fca", + "sha256:22ccba48abae827a0f952a78a7b1a7ff01866131e5bbe1f826ce9bda406bf051", + "sha256:26b5a70bdab09e6a2f40babc4f8f992e3771751e144bda1938084c70d3001c09", + "sha256:2d76780d9c65c7529cedd49fa4802d713e60798d8dc3b0d5b12a0a8f38cca51c", + "sha256:325fa1b15098e44fe4590a6c5c09a212ca10c6ebb5d96f7447d675f6c8340e4e", + "sha256:3a297d77b3d6979693f5948df02b89431ae3645ec95865e351fb45578031bdae", + "sha256:3b1dcbc49923ac3c973184a82832e1f018dec643b1e054867d04a3a22255ec6a", + "sha256:40240da438c0ebfe2aa76dd04b844effac6679423df61adbe3437d32f23468d9", + "sha256:46c6e5018ba31d5ee7582f323d8661498a154dea1117486a571db4c244531f24", + "sha256:46fb56fde006b7ef5f8eaa3698299b0ea47444238b869ff3ced1426aa9fedcb5", + "sha256:4dc350cb65fe4e3f737d50f0465fa6ea0dcae0e5722b7edf5d5b0a0e3cd2c3c7", + "sha256:51078855a16b7a4984ed2067b54e35803d18bca9861cb60c60f6234b50869a56", + "sha256:547111ef7cf13d73546c2de97ce434935626c897bdec96a578ca100b5fcd694b", + "sha256:5fb73cc8a34baba1dfa546ae83b9c248ef6150c238b06fc53d2773685b67ec67", + "sha256:654280c5f41831ddcc5a331c0e3ce2e480bbc3d7c93c18ecf6236313aae2d61a", + "sha256:6b3136eecf7e1b4a4d23e4b19d6c4e7a8e0b42d55f30444e3c529700cdacaa0d", + "sha256:7118ca592d25b2957ff7b662bc0fe4f4c2b5d5b27814b9b1bc9f2fb249a970e7", + "sha256:71af5b43e4fa286a35110fc5bb740fdeae2b36ca79fbcf0a54237485baeee8be", + "sha256:747190fcdadc377263223f8f72b038381b3b549a8a3df5baf4d067da4749b046", + "sha256:8395064d63b26947fa2c9faeea9c3eee35e52148c5339c37987e1d96fbf009b3", + "sha256:84f85adfebd7d3c3db649efdf73659e1677a2cf3fa6e2556a3f373578af14bf7", + "sha256:86bcf0009f2012847a688f2f4f9b16203ca4c835979a02549aa0595d9f457cc8", + "sha256:ab8a15c2750ae8d53e31f77a94f846d0a00772240f1c12817411fa2344351f86", + "sha256:af24b21c2283ca69c416a8a42cde9764dc36c63d3389645d28c69b0e93db3cd7", + "sha256:afe173ac2646c2636305ab820cc0380b22a00a7bca4290452e7166b4f4fa49d0", + "sha256:b9eb88593159a53a5ee0b0159daee531ff7dd9c87fa78f5d807ca059c7eb1b2b", + "sha256:c16635f8dddbeb1b827977d0b00d07b644b040aeb9ff8607a9fc0997afa3e567", + "sha256:ca3eefb02ef17257fae8b8555c85e7c1efdfd777f671384b0e4ef27409b02720", + "sha256:caa59e2cae0e23b1e225447d7a9ddb0f982f42a6a22d497a484dfe62a06f7c0e", + "sha256:cb55258931448d61e2d50187de4ee66fc9d9f34908b524949b8b2b93d0c57136", + "sha256:d248c46c0aa406695bda2abf99632db991f8b3a6d46018721a2892312a99f069", + "sha256:d2578e5149ff49878934debfacf5c743fab49eca5ecdb983d0b218e1e554c498", + "sha256:dd22085446f3eca990d12a0878eeb5199dc9553b2e71716bfe7bed9915a472ab", + "sha256:e7cf940af5fee00a92e281eb157abe8770227a5255207818ea9a34e54a29f5b2", + "sha256:f70f3d028794e31cf9d1a822914efc935aadb2438ec4e8d4871d95eb1ce032d6", + "sha256:fd2842a0faed9ab9aba0922c951906132d9384be89690570f0ed18cd4f20e658", + "sha256:fd628e63ffdba0112e3ddf1b1e9f3db29dd8262345138e08f4938acbc6d0805a", + "sha256:ffd7cf165ff71afb202b3f36daafbf298932bee325aac9f58e1c9cd55838bef0" ], "markers": "python_version >= '3.7'", - "version": "==1.4.2" + "version": "==1.4.3" }, "lru-dict": { "hashes": [ @@ -2572,7 +2599,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.2.0" }, "urllib3": { diff --git a/tools/zookeeper/__init__.py b/tools/zookeeper/__init__.py old mode 100755 new mode 100644 index cd64d3a1fb13ae..42438cb209d581 --- a/tools/zookeeper/__init__.py +++ b/tools/zookeeper/__init__.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 - -# Python library to control Zookeeper - -import ft4222 # pylint: disable=import-error -import ft4222.I2CMaster # pylint: disable=import-error +import ft4222 +import ft4222.I2CMaster DEBUG = False diff --git a/tools/zookeeper/power_monitor.py b/tools/zookeeper/power_monitor.py index f2796bad3172a0..d3bdd6679f3975 100755 --- a/tools/zookeeper/power_monitor.py +++ b/tools/zookeeper/power_monitor.py @@ -1,30 +1,30 @@ -#!/usr/bin/env python - +#!/usr/bin/env python3 import sys import time -from tools.zookeeper import Zookeeper -# Usage: check_consumption.py -# Exit code: 0 -> passed -# 1 -> failed +from common.realtime import Ratekeeper +from common.filter_simple import FirstOrderFilter +from tools.zookeeper import Zookeeper if __name__ == "__main__": z = Zookeeper() + z.set_device_power(True) + z.set_device_ignition(False) duration = None if len(sys.argv) > 1: duration = int(sys.argv[1]) + rate = 123 + rk = Ratekeeper(rate, print_delay_threshold=None) + fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False) + try: start_time = time.monotonic() - measurements = [] while duration is None or time.monotonic() - start_time < duration: - p = z.read_power() - print(round(p, 3), "W") - measurements.append(p) - time.sleep(0.25) + fltr.update(z.read_power()) + if rk.frame % rate == 0: + print(f"{fltr.x:.2f} W") + rk.keep_time() except KeyboardInterrupt: pass - finally: - average_power = sum(measurements)/len(measurements) - print(f"Average power: {round(average_power, 4)}W") diff --git a/tools/zookeeper/requirements.txt b/tools/zookeeper/requirements.txt deleted file mode 100644 index 6f70576caf0c36..00000000000000 --- a/tools/zookeeper/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -ft4222==1.2.1 \ No newline at end of file From 0d0f5926a006ead1709b78fe69c1186b5a607c66 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 14 Jun 2022 21:12:49 -0700 Subject: [PATCH 081/436] bump up modeld power draw threshold --- system/hardware/tici/test_power_draw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hardware/tici/test_power_draw.py b/system/hardware/tici/test_power_draw.py index 31b8471328229f..b6b5c735fa77b9 100755 --- a/system/hardware/tici/test_power_draw.py +++ b/system/hardware/tici/test_power_draw.py @@ -20,7 +20,7 @@ class Proc: PROCS = [ Proc('camerad', 2.15), - Proc('modeld', 1.0), + Proc('modeld', 1.0, atol=0.15), Proc('dmonitoringmodeld', 0.25), Proc('encoderd', 0.23), ] From a6652a539d8d4ba614b216900c472cc9e2e58a05 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Tue, 14 Jun 2022 22:29:08 -0700 Subject: [PATCH 082/436] Torque control: low speed boost (#24859) * Make very low speed more aggressive * Less extreme low speed boost * Update ref --- selfdrive/controls/lib/latcontrol_torque.py | 11 ++++++----- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 12714082a61eb9..f72ffc4b88ea7b 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -19,8 +19,7 @@ # move it at all, this is compensated for too. -LOW_SPEED_FACTOR = 200 -JERK_THRESHOLD = 0.2 +FRICTION_THRESHOLD = 0.2 def set_torque_tune(tune, MAX_LAT_ACCEL=2.5, FRICTION=0.01, steering_angle_deadzone_deg=0.0): @@ -66,14 +65,16 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - setpoint = desired_lateral_accel + LOW_SPEED_FACTOR * desired_curvature - measurement = actual_lateral_accel + LOW_SPEED_FACTOR * actual_curvature + + low_speed_factor = interp(CS.vEgo, [0, 15], [500, 0]) + setpoint = desired_lateral_accel + low_speed_factor * desired_curvature + measurement = actual_lateral_accel + low_speed_factor * actual_curvature error = apply_deadzone(setpoint - measurement, lateral_accel_deadzone) pid_log.error = error ff = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY # convert friction into lateral accel units for feedforward - friction_compensation = interp(error, [-JERK_THRESHOLD, JERK_THRESHOLD], [-self.friction, self.friction]) + friction_compensation = interp(error, [-FRICTION_THRESHOLD, FRICTION_THRESHOLD], [-self.friction, self.friction]) ff += friction_compensation / self.kf freeze_integrator = CS.steeringRateLimited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(error, diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 61bbd5cdd13400..b8976e5f389733 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -fe2da24194e3def1823681cc18e7879f24edfc6e +1d66eed104dbc124c4e5679f5dddf40197b86ce9 From b86ef0b70e81ef4e895d5102feb231dc3d43265f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 14 Jun 2022 23:30:35 -0700 Subject: [PATCH 083/436] regen & process replay: support no disengage on accelerator (#24850) * ACC on if enabled != 0 * small regen clean up and add HONDA3 * fixes * revert unneeded changes * not used * just alt exp Co-authored-by: redacid95 --- selfdrive/test/process_replay/process_replay.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 363035ab224ec6..b016eb1c9ff5d5 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -14,6 +14,7 @@ from cereal.services import service_list from common.params import Params from common.timeout import Timeout +from panda.python import ALTERNATIVE_EXPERIENCE from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.car.car_helpers import get_car, interfaces from selfdrive.test.process_replay.helpers import OpenpilotPrefix @@ -368,6 +369,9 @@ def setup_env(simulation=False, CP=None): # Regen or python process if CP is not None: + if CP.alternativeExperience == ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS: + params.put_bool("DisengageOnAccelerator", False) + if CP.fingerprintSource == "fw" and CP.carFingerprint in FW_VERSIONS: params.put("CarParamsCache", CP.as_builder().to_bytes()) else: From c3fa9151f39994984b60a19cdd7425dba73ec2fc Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 15 Jun 2022 11:32:07 +0200 Subject: [PATCH 084/436] Laikad: Cache orbit and nav data (#24831) * Cache orbit and nav data * Cleanup * Cleanup * Use ProcessPoolExecutor to fetch orbits * update laika repo * Minor * Create json de/serializers Save cache only 1 minute at max * Update laika repo * Speed up json by caching json in ephemeris class * Update laika * Fix test * Use constant --- common/params.cc | 1 + laika_repo | 2 +- selfdrive/locationd/laikad.py | 64 +++++++++++++++++++++--- selfdrive/locationd/test/test_laikad.py | 66 ++++++++++++++++++++++--- 4 files changed, 118 insertions(+), 15 deletions(-) diff --git a/common/params.cc b/common/params.cc index b740e5a71efc05..8cf1baa6c81512 100644 --- a/common/params.cc +++ b/common/params.cc @@ -127,6 +127,7 @@ std::unordered_map keys = { {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, {"IsUpdateAvailable", CLEAR_ON_MANAGER_START}, {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, + {"LaikadEphemeris", PERSISTENT}, {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, {"LastGPSPosition", PERSISTENT}, {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, diff --git a/laika_repo b/laika_repo index 36f2621fc53484..a3a80dc4f7977b 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 36f2621fc5348487bb2cd606c37c8c15de0e32cd +Subproject commit a3a80dc4f7977b2232946e56a16770e413190818 diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 33e41398a0b440..ddd57bdca9b40a 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import time from concurrent.futures import Future, ProcessPoolExecutor from typing import List, Optional @@ -9,9 +10,10 @@ from numpy.linalg import linalg from cereal import log, messaging +from common.params import Params, put_nonblocking from laika import AstroDog from laika.constants import SECS_IN_HR, SECS_IN_MIN -from laika.ephemeris import EphemerisType, convert_ublox_ephem +from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId from laika.raw_gnss import GNSSMeasurement, calc_pos_fix, correct_measurements, process_measurements, read_raw_ublox @@ -22,16 +24,40 @@ from system.swaglog import cloudlog MAX_TIME_GAP = 10 +EPHEMERIS_CACHE = 'LaikadEphemeris' +CACHE_VERSION = 0.1 class Laikad: - - def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV)): - self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) + def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), + save_ephemeris=False): + self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) self.gnss_kf = GNSSKalman(GENERATED_DIR) self.orbit_fetch_executor = ProcessPoolExecutor() self.orbit_fetch_future: Optional[Future] = None self.last_fetch_orbits_t = None + self.last_cached_t = None + self.save_ephemeris = save_ephemeris + self.load_cache() + + def load_cache(self): + cache = Params().get(EPHEMERIS_CACHE) + if not cache: + return + try: + cache = json.loads(cache, object_hook=deserialize_hook) + self.astro_dog.add_orbits(cache['orbits']) + self.astro_dog.add_navs(cache['nav']) + self.last_fetch_orbits_t = cache['last_fetch_orbits_t'] + except json.decoder.JSONDecodeError: + cloudlog.exception("Error parsing cache") + + def cache_ephemeris(self, t: GPSTime): + if self.save_ephemeris and (self.last_cached_t is None or t - self.last_cached_t > SECS_IN_MIN): + put_nonblocking(EPHEMERIS_CACHE, json.dumps( + {'version': CACHE_VERSION, 'last_fetch_orbits_t': self.last_fetch_orbits_t, 'orbits': self.astro_dog.orbits, 'nav': self.astro_dog.nav}, + cls=CacheSerializer)) + self.last_cached_t = t def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': @@ -83,7 +109,8 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): return dat elif ublox_msg.which == 'ephemeris': ephem = convert_ublox_ephem(ublox_msg.ephemeris) - self.astro_dog.add_navs([ephem]) + self.astro_dog.add_navs({ephem.prn: [ephem]}) + self.cache_ephemeris(t=ephem.epoch) # elif ublox_msg.which == 'ionoData': # todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them. @@ -101,7 +128,7 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement cloudlog.error("Gnss kalman std too far") if len(pos_fix) == 0: - cloudlog.warning("Position fix not available when resetting kalman filter") + cloudlog.info("Position fix not available when resetting kalman filter") return post_est = pos_fix[0][:3].tolist() self.init_gnss_localizer(post_est) @@ -134,10 +161,11 @@ def fetch_orbits(self, t: GPSTime, block): self.orbit_fetch_future.result() if self.orbit_fetch_future.done(): ret = self.orbit_fetch_future.result() + self.last_fetch_orbits_t = t if ret: self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret + self.cache_ephemeris(t=t) self.orbit_fetch_future = None - self.last_fetch_orbits_t = t def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): @@ -193,11 +221,31 @@ def get_bearing_from_gnss(ecef_pos, ecef_vel, vel_std): return float(np.rad2deg(bearing)), float(bearing_std) +class CacheSerializer(json.JSONEncoder): + + def default(self, o): + if isinstance(o, Ephemeris): + return o.to_json() + if isinstance(o, GPSTime): + return o.__dict__ + if isinstance(o, np.ndarray): + return o.tolist() + return json.JSONEncoder.default(self, o) + + +def deserialize_hook(dct): + if 'ephemeris' in dct: + return Ephemeris.from_json(dct) + if 'week' in dct: + return GPSTime(dct['week'], dct['tow']) + return dct + + def main(): sm = messaging.SubMaster(['ubloxGnss']) pm = messaging.PubMaster(['gnssMeasurements']) - laikad = Laikad() + laikad = Laikad(save_ephemeris=True) while True: sm.update() diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 7dc803d79919e1..01ea8fee27f23b 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 +import time import unittest from datetime import datetime from unittest import mock -from unittest.mock import Mock +from unittest.mock import Mock, patch +from common.params import Params from laika.ephemeris import EphemerisType from laika.gps_time import GPSTime -from laika.helpers import ConstellationId +from laika.helpers import ConstellationId, TimeRangeHolder from laika.raw_gnss import GNSSMeasurement, read_raw_ublox -from selfdrive.locationd.laikad import Laikad, create_measurement_msg +from selfdrive.locationd.laikad import EPHEMERIS_CACHE, Laikad, create_measurement_msg from selfdrive.test.openpilotci import get_url from tools.lib.logreader import LogReader @@ -20,12 +22,14 @@ def get_log(segs=range(0)): return [m for m in logs if m.which() == 'ubloxGnss'] -def verify_messages(lr, laikad): +def verify_messages(lr, laikad, return_one_success=False): good_msgs = [] for m in lr: msg = laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime, block=True) if msg is not None and len(msg.gnssMeasurements.correctedMeasurements) > 0: good_msgs.append(msg) + if return_one_success: + return msg return good_msgs @@ -35,6 +39,9 @@ class TestLaikad(unittest.TestCase): def setUpClass(cls): cls.logs = get_log(range(1)) + def setUp(self): + Params().delete(EPHEMERIS_CACHE) + def test_create_msg_without_errors(self): gpstime = GPSTime.from_datetime(datetime.now()) meas = GNSSMeasurement(ConstellationId.GPS, 1, gpstime.week, gpstime.tow, {'C1C': 0., 'D1C': 0.}, {'C1C': 0., 'D1C': 0.}) @@ -81,8 +88,7 @@ def test_laika_get_orbits(self): first_gps_time = self.get_first_gps_time() # Pretend process has loaded the orbits on startup by using the time of the first gps message. laikad.fetch_orbits(first_gps_time, block=True) - self.assertEqual(29, len(laikad.astro_dog.orbits.values())) - self.assertGreater(min([len(v) for v in laikad.astro_dog.orbits.values()]), 0) + self.dict_has_values(laikad.astro_dog.orbits) @unittest.skip("Use to debug live data") def test_laika_get_orbits_now(self): @@ -109,6 +115,54 @@ def test_get_orbits_in_process(self): self.assertGreater(len(laikad.astro_dog.orbit_fetched_times._ranges), 0) self.assertEqual(None, laikad.orbit_fetch_future) + def test_cache(self): + laikad = Laikad(auto_update=True, save_ephemeris=True) + first_gps_time = self.get_first_gps_time() + + def wait_for_cache(): + max_time = 2 + while Params().get(EPHEMERIS_CACHE) is None: + time.sleep(0.1) + max_time -= 0.1 + if max_time == 0: + self.fail("Cache has not been written after 2 seconds") + # Test cache with no ephemeris + laikad.cache_ephemeris(t=GPSTime(0, 0)) + wait_for_cache() + Params().delete(EPHEMERIS_CACHE) + + laikad.astro_dog.get_navs(first_gps_time) + laikad.fetch_orbits(first_gps_time, block=True) + + # Wait for cache to save + wait_for_cache() + + # Check both nav and orbits separate + laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.NAV) + # Verify orbits and nav are loaded from cache + self.dict_has_values(laikad.astro_dog.orbits) + self.dict_has_values(laikad.astro_dog.nav) + # Verify cache is working for only nav by running a segment + msg = verify_messages(self.logs, laikad, return_one_success=True) + self.assertIsNotNone(msg) + + with patch('selfdrive.locationd.laikad.get_orbit_data', return_value=None) as mock_method: + # Verify no orbit downloads even if orbit fetch times is reset since the cache has recently been saved and we don't want to download high frequently + laikad.astro_dog.orbit_fetched_times = TimeRangeHolder() + laikad.fetch_orbits(first_gps_time, block=False) + mock_method.assert_not_called() + + # Verify cache is working for only orbits by running a segment + laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT) + msg = verify_messages(self.logs, laikad, return_one_success=True) + self.assertIsNotNone(msg) + # Verify orbit data is not downloaded + mock_method.assert_not_called() + + def dict_has_values(self, dct): + self.assertGreater(len(dct), 0) + self.assertGreater(min([len(v) for v in dct.values()]), 0) + if __name__ == "__main__": unittest.main() From d528cd556848404891e28b3342d146498e3991cc Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 15 Jun 2022 13:06:46 +0200 Subject: [PATCH 085/436] UI: new set speed design, show speed limits (#24736) * basic US design * place based on center position * fix typo * eu sign without rounded box * same as steering wheel icon * proper rounded bottom for eu sign * add border * proper placement/sizes * needs to be semi bold * color changes * only when engaged * move helpers into util.h * Fix MAX placement * only change color when at least 5 over * implement override state * pixel perfect spacing around us sign --- selfdrive/ui/qt/onroad.cc | 162 ++++++++++++++++++++++++++++++++------ selfdrive/ui/qt/onroad.h | 15 +++- selfdrive/ui/qt/util.cc | 61 ++++++++++++++ selfdrive/ui/qt/util.h | 4 + 4 files changed, 216 insertions(+), 26 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index ce61c094bd6bd7..12a579c5736fff 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -174,17 +174,24 @@ void NvgWindow::updateState(const UIState &s) { const SubMaster &sm = *(s.sm); const auto cs = sm["controlsState"].getControlsState(); - float maxspeed = cs.getVCruise(); - bool cruise_set = maxspeed > 0 && (int)maxspeed != SET_SPEED_NA; + float set_speed = cs.getVCruise(); + bool cruise_set = set_speed > 0 && (int)set_speed != SET_SPEED_NA; if (cruise_set && !s.scene.is_metric) { - maxspeed *= KM_TO_MILE; + set_speed *= KM_TO_MILE; } - QString maxspeed_str = cruise_set ? QString::number(std::nearbyint(maxspeed)) : "N/A"; float cur_speed = std::max(0.0, sm["carState"].getCarState().getVEgo() * (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH)); + auto speed_limit_sign = sm["navInstruction"].getNavInstruction().getSpeedLimitSign(); + float speed_limit = sm["navInstruction"].getValid() ? sm["navInstruction"].getNavInstruction().getSpeedLimit() : 0.0; + speed_limit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); + + setProperty("speedLimit", speed_limit); + setProperty("has_us_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); + setProperty("has_eu_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); + setProperty("is_cruise_set", cruise_set); - setProperty("speed", QString::number(std::nearbyint(cur_speed))); - setProperty("maxSpeed", maxspeed_str); + setProperty("speed", cur_speed); + setProperty("setSpeed", set_speed); setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); setProperty("hideDM", cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE); setProperty("status", s.status); @@ -205,26 +212,139 @@ void NvgWindow::drawHud(QPainter &p) { bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); p.fillRect(0, 0, width(), header_h, bg); - // max speed - QRect rc(bdr_s * 2, bdr_s * 1.5, 184, 202); - p.setPen(QPen(QColor(0xff, 0xff, 0xff, 100), 10)); - p.setBrush(QColor(0, 0, 0, 100)); - p.drawRoundedRect(rc, 20, 20); - p.setPen(Qt::NoPen); + QString speedLimitStr = QString::number(std::nearbyint(speedLimit)); + QString speedStr = QString::number(std::nearbyint(speed)); + QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed)) : "–"; + + // Draw outer box + border to contain set speed and speed limit + int default_rect_width = 172; + int rect_width = default_rect_width; + if (has_us_speed_limit && speedLimitStr.size() >= 3) rect_width = 223; + else if (has_eu_speed_limit) rect_width = 200; + + int rect_height = 204; + if (has_us_speed_limit) rect_height = 402; + else if (has_eu_speed_limit) rect_height = 392; - configFont(p, "Open Sans", 48, "Regular"); - drawText(p, rc.center().x(), 118, "MAX", is_cruise_set ? 200 : 100); + int top_radius = 32; + int bottom_radius = has_eu_speed_limit ? 100 : 32; + + QRect set_speed_rect(60 + default_rect_width / 2 - rect_width / 2, 45, rect_width, rect_height); + p.setPen(QPen(QColor(255, 255, 255, 75), 6)); + p.setBrush(QColor(0, 0, 0, 166)); + drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius); + + // Draw set speed if (is_cruise_set) { - configFont(p, "Open Sans", 88, "Bold"); - drawText(p, rc.center().x(), 212, maxSpeed, 255); + if (speedLimit > 0 && status != STATUS_DISENGAGED && status != STATUS_OVERRIDE) { + p.setPen(interpColor( + setSpeed, + {speedLimit + 5, speedLimit + 15, speedLimit + 25}, + {QColor(0xff, 0xff, 0xff, 0xff), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} + )); + } else { + p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + } } else { - configFont(p, "Open Sans", 80, "SemiBold"); - drawText(p, rc.center().x(), 212, maxSpeed, 100); + p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); + } + configFont(p, "Open Sans", 90, "Bold"); + QRect speed_rect = getTextRect(p, Qt::AlignCenter, setSpeedStr); + speed_rect.moveCenter({set_speed_rect.center().x(), 0}); + speed_rect.moveTop(set_speed_rect.top() + 8); + p.drawText(speed_rect, Qt::AlignCenter, setSpeedStr); + + // Draw MAX + if (is_cruise_set) { + if (status == STATUS_DISENGAGED) { + p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + } else if (status == STATUS_OVERRIDE) { + p.setPen(QColor(0x91, 0x9b, 0x95, 0xff)); + } else if (speedLimit > 0) { + p.setPen(interpColor( + setSpeed, + {speedLimit + 5, speedLimit + 15, speedLimit + 25}, + {QColor(0x80, 0xd8, 0xa6, 0xff), QColor(0xff, 0xe4, 0xbf, 0xff), QColor(0xff, 0xbf, 0xbf, 0xff)} + )); + } else { + p.setPen(QColor(0x80, 0xd8, 0xa6, 0xff)); + } + } else { + p.setPen(QColor(0xa6, 0xa6, 0xa6, 0xff)); + } + configFont(p, "Open Sans", 40, "SemiBold"); + QRect max_rect = getTextRect(p, Qt::AlignCenter, "MAX"); + max_rect.moveCenter({set_speed_rect.center().x(), 0}); + max_rect.moveTop(set_speed_rect.top() + 123); + p.drawText(max_rect, Qt::AlignCenter, "MAX"); + + // US/Canada (MUTCD style) sign + if (has_us_speed_limit) { + const int border_width = 6; + const int sign_width = (speedLimitStr.size() >= 3) ? 199 : 148; + const int sign_height = 186; + + // White outer square + QRect sign_rect_outer(set_speed_rect.left() + 12, set_speed_rect.bottom() - 11 - sign_height, sign_width, sign_height); + p.setPen(Qt::NoPen); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawRoundedRect(sign_rect_outer, 24, 24); + + // Smaller white square with black border + QRect sign_rect(sign_rect_outer.left() + 1.5 * border_width, sign_rect_outer.top() + 1.5 * border_width, sign_width - 3 * border_width, sign_height - 3 * border_width); + p.setPen(QPen(QColor(0, 0, 0, 255), border_width)); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawRoundedRect(sign_rect, 16, 16); + + // "SPEED" + configFont(p, "Open Sans", 28, "SemiBold"); + QRect text_speed_rect = getTextRect(p, Qt::AlignCenter, "SPEED"); + text_speed_rect.moveCenter({sign_rect.center().x(), 0}); + text_speed_rect.moveTop(sign_rect_outer.top() + 20); + p.drawText(text_speed_rect, Qt::AlignCenter, "SPEED"); + + // "LIMIT" + QRect text_limit_rect = getTextRect(p, Qt::AlignCenter, "LIMIT"); + text_limit_rect.moveCenter({sign_rect.center().x(), 0}); + text_limit_rect.moveTop(sign_rect_outer.top() + 48); + p.drawText(text_limit_rect, Qt::AlignCenter, "LIMIT"); + + // Speed limit value + configFont(p, "Open Sans", 70, "Bold"); + QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); + speed_limit_rect.moveCenter({sign_rect.center().x(), 0}); + speed_limit_rect.moveTop(sign_rect.top() + 70); + p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); + } + + // EU (Vienna style) sign + if (has_eu_speed_limit) { + int outer_radius = 176 / 2; + int inner_radius_1 = outer_radius - 6; // White outer border + int inner_radius_2 = inner_radius_1 - 20; // Red circle + + // Draw white circle with red border + QPoint center(set_speed_rect.center().x() + 1, set_speed_rect.top() + 204 + outer_radius); + p.setPen(Qt::NoPen); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawEllipse(center, outer_radius, outer_radius); + p.setBrush(QColor(255, 0, 0, 255)); + p.drawEllipse(center, inner_radius_1, inner_radius_1); + p.setBrush(QColor(255, 255, 255, 255)); + p.drawEllipse(center, inner_radius_2, inner_radius_2); + + // Speed limit value + int font_size = (speedLimitStr.size() >= 3) ? 62 : 70; + configFont(p, "Open Sans", font_size, "Bold"); + QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); + speed_limit_rect.moveCenter(center); + p.setPen(QColor(0, 0, 0, 255)); + p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); } // current speed configFont(p, "Open Sans", 176, "Bold"); - drawText(p, rect().center().x(), 210, speed); + drawText(p, rect().center().x(), 210, speedStr); configFont(p, "Open Sans", 66, "Regular"); drawText(p, rect().center().x(), 290, speedUnit, 200); @@ -243,9 +363,7 @@ void NvgWindow::drawHud(QPainter &p) { } void NvgWindow::drawText(QPainter &p, int x, int y, const QString &text, int alpha) { - QFontMetrics fm(p.font()); - QRect init_rect = fm.boundingRect(text); - QRect real_rect = fm.boundingRect(init_rect, 0, text); + QRect real_rect = getTextRect(p, 0, text); real_rect.moveCenter({x, y - real_rect.height() / 2}); p.setPen(QColor(0xff, 0xff, 0xff, alpha)); diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 6ca2b3c738506c..3d90a63d7c100a 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -27,10 +27,14 @@ class OnroadAlerts : public QWidget { // container window for the NVG UI class NvgWindow : public CameraViewWidget { Q_OBJECT - Q_PROPERTY(QString speed MEMBER speed); + Q_PROPERTY(float speed MEMBER speed); Q_PROPERTY(QString speedUnit MEMBER speedUnit); - Q_PROPERTY(QString maxSpeed MEMBER maxSpeed); + Q_PROPERTY(float setSpeed MEMBER setSpeed); + Q_PROPERTY(float speedLimit MEMBER speedLimit); Q_PROPERTY(bool is_cruise_set MEMBER is_cruise_set); + Q_PROPERTY(bool has_eu_speed_limit MEMBER has_eu_speed_limit); + Q_PROPERTY(bool has_us_speed_limit MEMBER has_us_speed_limit); + Q_PROPERTY(bool engageable MEMBER engageable); Q_PROPERTY(bool dmActive MEMBER dmActive); Q_PROPERTY(bool hideDM MEMBER hideDM); @@ -48,13 +52,16 @@ class NvgWindow : public CameraViewWidget { QPixmap dm_img; const int radius = 192; const int img_size = (radius / 2) * 1.5; - QString speed; + float speed; QString speedUnit; - QString maxSpeed; + float setSpeed; + float speedLimit; bool is_cruise_set = false; bool engageable = false; bool dmActive = false; bool hideDM = false; + bool has_us_speed_limit = false; + bool has_eu_speed_limit = false; int status = STATUS_DISENGAGED; protected: diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 600885fb373c53..408652a0b9b414 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -135,3 +135,64 @@ QPixmap loadPixmap(const QString &fileName, const QSize &size, Qt::AspectRatioMo return QPixmap(fileName).scaled(size, aspectRatioMode, Qt::SmoothTransformation); } } + +QRect getTextRect(QPainter &p, int flags, QString text) { + QFontMetrics fm(p.font()); + QRect init_rect = fm.boundingRect(text); + return fm.boundingRect(init_rect, flags, text); +} + +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom){ + qreal w_2 = rect.width() / 2; + qreal h_2 = rect.height() / 2; + + xRadiusTop = 100 * qMin(xRadiusTop, w_2) / w_2; + yRadiusTop = 100 * qMin(yRadiusTop, h_2) / h_2; + + xRadiusBottom = 100 * qMin(xRadiusBottom, w_2) / w_2; + yRadiusBottom = 100 * qMin(yRadiusBottom, h_2) / h_2; + + qreal x = rect.x(); + qreal y = rect.y(); + qreal w = rect.width(); + qreal h = rect.height(); + + qreal rxx2Top = w*xRadiusTop/100; + qreal ryy2Top = h*yRadiusTop/100; + + qreal rxx2Bottom = w*xRadiusBottom/100; + qreal ryy2Bottom = h*yRadiusBottom/100; + + QPainterPath path; + path.arcMoveTo(x, y, rxx2Top, ryy2Top, 180); + path.arcTo(x, y, rxx2Top, ryy2Top, 180, -90); + path.arcTo(x+w-rxx2Top, y, rxx2Top, ryy2Top, 90, -90); + path.arcTo(x+w-rxx2Bottom, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 0, -90); + path.arcTo(x, y+h-ryy2Bottom, rxx2Bottom, ryy2Bottom, 270, -90); + path.closeSubpath(); + + painter.drawPath(path); +} + +QColor interpColor(float xv, std::vector xp, std::vector fp) { + assert(xp.size() == fp.size()); + + int N = xp.size(); + int hi = 0; + + while (hi < N and xv > xp[hi]) hi++; + int low = hi - 1; + + if (hi == N && xv > xp[low]) { + return fp[fp.size() - 1]; + } else if (hi == 0){ + return fp[0]; + } else { + return QColor( + (xv - xp[low]) * (fp[hi].red() - fp[low].red()) / (xp[hi] - xp[low]) + fp[low].red(), + (xv - xp[low]) * (fp[hi].green() - fp[low].green()) / (xp[hi] - xp[low]) + fp[low].green(), + (xv - xp[low]) * (fp[hi].blue() - fp[low].blue()) / (xp[hi] - xp[low]) + fp[low].blue(), + (xv - xp[low]) * (fp[hi].alpha() - fp[low].alpha()) / (xp[hi] - xp[low]) + fp[low].alpha() + ); + } +} diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 813777710a73bb..9491c6798e1819 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -22,3 +22,7 @@ void swagLogMessageHandler(QtMsgType type, const QMessageLogContext &context, co void initApp(int argc, char *argv[]); QWidget* topWidget (QWidget* widget); QPixmap loadPixmap(const QString &fileName, const QSize &size = {}, Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio); + +QRect getTextRect(QPainter &p, int flags, QString text); +void drawRoundedRect(QPainter &painter, const QRectF &rect, qreal xRadiusTop, qreal yRadiusTop, qreal xRadiusBottom, qreal yRadiusBottom); +QColor interpColor(float xv, std::vector xp, std::vector fp); From b046eb818e35f24d3b9b8a978fddb94dbfb85e59 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 15 Jun 2022 13:09:57 +0200 Subject: [PATCH 086/436] add speed limits to release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index acbafd5f5b5a20..7f365e5cef8284 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,7 @@ Version 0.8.15 (2022-XX-XX) * takes a larger input frame * outputs a driver state for both driver and passenger * automatically determines which side the driver is on (soon) +* Display speed limit while navigating * Reduced power usage: device runs cooler and fan spins less * AGNOS 5 * Hyundai Tucson 2021 support thanks to bluesforte! From 9e3d0e3c9cea4dcf2da10801aad041b1dd65cccb Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 15 Jun 2022 14:00:27 +0200 Subject: [PATCH 087/436] Laikad: Remove bearingDeg from message (#24864) * Remove bearingDeg from message. * Push cereal * Commit cereal --- cereal | 2 +- selfdrive/locationd/laikad.py | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/cereal b/cereal index 98f795fd733561..4b4d9aab03937f 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 98f795fd73356198f1fc8d5ea5a8f25e6f7c57e0 +Subproject commit 4b4d9aab03937f5a45677ff15efb275abf9c958b diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index ddd57bdca9b40a..ebe7404e17618e 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -20,7 +20,6 @@ from selfdrive.locationd.models.constants import GENERATED_DIR, ObservationKind from selfdrive.locationd.models.gnss_kf import GNSSKalman from selfdrive.locationd.models.gnss_kf import States as GStates -import common.transformations.coordinates as coord from system.swaglog import cloudlog MAX_TIME_GAP = 10 @@ -94,15 +93,12 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())).tolist() vel_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_VELOCITY].diagonal())).tolist() - bearing_deg, bearing_std = get_bearing_from_gnss(ecef_pos, ecef_vel, vel_std) - meas_msgs = [create_measurement_msg(m) for m in corrected_measurements] dat = messaging.new_message("gnssMeasurements") measurement_msg = log.LiveLocationKalman.Measurement.new_message dat.gnssMeasurements = { "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), - "bearingDeg": measurement_msg(value=[bearing_deg], std=[bearing_std], valid=kf_valid), "ubloxMonoTime": ublox_mono_time, "correctedMeasurements": meas_msgs } @@ -211,16 +207,6 @@ def kf_add_observations(gnss_kf: GNSSKalman, t: float, measurements: List[GNSSMe gnss_kf.predict_and_observe(t, kind, data) -def get_bearing_from_gnss(ecef_pos, ecef_vel, vel_std): - # init orientation with direction of velocity - converter = coord.LocalCoord.from_ecef(ecef_pos) - - ned_vel = np.einsum('ij,j ->i', converter.ned_from_ecef_matrix, ecef_vel) - bearing = np.arctan2(ned_vel[1], ned_vel[0]) - bearing_std = np.arctan2(np.linalg.norm(vel_std), np.linalg.norm(ned_vel)) - return float(np.rad2deg(bearing)), float(bearing_std) - - class CacheSerializer(json.JSONEncoder): def default(self, o): From 849ec17b20f8cae7f6fd6589456d7b00b07e794b Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Wed, 15 Jun 2022 23:10:56 +0900 Subject: [PATCH 088/436] ui: add black color define (#24866) --- selfdrive/ui/qt/onroad.cc | 24 ++++++++++++------------ selfdrive/ui/qt/onroad.h | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 12a579c5736fff..38541ef2d5088b 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -230,8 +230,8 @@ void NvgWindow::drawHud(QPainter &p) { int bottom_radius = has_eu_speed_limit ? 100 : 32; QRect set_speed_rect(60 + default_rect_width / 2 - rect_width / 2, 45, rect_width, rect_height); - p.setPen(QPen(QColor(255, 255, 255, 75), 6)); - p.setBrush(QColor(0, 0, 0, 166)); + p.setPen(QPen(whiteColor(75), 6)); + p.setBrush(blackColor(166)); drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius); // Draw set speed @@ -240,10 +240,10 @@ void NvgWindow::drawHud(QPainter &p) { p.setPen(interpColor( setSpeed, {speedLimit + 5, speedLimit + 15, speedLimit + 25}, - {QColor(0xff, 0xff, 0xff, 0xff), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} + {whiteColor(), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} )); } else { - p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + p.setPen(whiteColor()); } } else { p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); @@ -257,7 +257,7 @@ void NvgWindow::drawHud(QPainter &p) { // Draw MAX if (is_cruise_set) { if (status == STATUS_DISENGAGED) { - p.setPen(QColor(0xff, 0xff, 0xff, 0xff)); + p.setPen(whiteColor()); } else if (status == STATUS_OVERRIDE) { p.setPen(QColor(0x91, 0x9b, 0x95, 0xff)); } else if (speedLimit > 0) { @@ -287,13 +287,13 @@ void NvgWindow::drawHud(QPainter &p) { // White outer square QRect sign_rect_outer(set_speed_rect.left() + 12, set_speed_rect.bottom() - 11 - sign_height, sign_width, sign_height); p.setPen(Qt::NoPen); - p.setBrush(QColor(255, 255, 255, 255)); + p.setBrush(whiteColor()); p.drawRoundedRect(sign_rect_outer, 24, 24); // Smaller white square with black border QRect sign_rect(sign_rect_outer.left() + 1.5 * border_width, sign_rect_outer.top() + 1.5 * border_width, sign_width - 3 * border_width, sign_height - 3 * border_width); - p.setPen(QPen(QColor(0, 0, 0, 255), border_width)); - p.setBrush(QColor(255, 255, 255, 255)); + p.setPen(QPen(blackColor(), border_width)); + p.setBrush(whiteColor()); p.drawRoundedRect(sign_rect, 16, 16); // "SPEED" @@ -326,11 +326,11 @@ void NvgWindow::drawHud(QPainter &p) { // Draw white circle with red border QPoint center(set_speed_rect.center().x() + 1, set_speed_rect.top() + 204 + outer_radius); p.setPen(Qt::NoPen); - p.setBrush(QColor(255, 255, 255, 255)); + p.setBrush(whiteColor()); p.drawEllipse(center, outer_radius, outer_radius); p.setBrush(QColor(255, 0, 0, 255)); p.drawEllipse(center, inner_radius_1, inner_radius_1); - p.setBrush(QColor(255, 255, 255, 255)); + p.setBrush(whiteColor()); p.drawEllipse(center, inner_radius_2, inner_radius_2); // Speed limit value @@ -338,7 +338,7 @@ void NvgWindow::drawHud(QPainter &p) { configFont(p, "Open Sans", font_size, "Bold"); QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); speed_limit_rect.moveCenter(center); - p.setPen(QColor(0, 0, 0, 255)); + p.setPen(blackColor()); p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); } @@ -357,7 +357,7 @@ void NvgWindow::drawHud(QPainter &p) { // dm icon if (!hideDM) { drawIcon(p, radius / 2 + (bdr_s * 2), rect().bottom() - footer_h / 2, - dm_img, QColor(0, 0, 0, 70), dmActive ? 1.0 : 0.2); + dm_img, blackColor(70), dmActive ? 1.0 : 0.2); } p.restore(); } diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 3d90a63d7c100a..e58d7a79729130 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -74,6 +74,7 @@ class NvgWindow : public CameraViewWidget { void drawHud(QPainter &p); inline QColor redColor(int alpha = 255) { return QColor(201, 34, 49, alpha); } inline QColor whiteColor(int alpha = 255) { return QColor(255, 255, 255, alpha); } + inline QColor blackColor(int alpha = 255) { return QColor(0, 0, 0, alpha); } double prev_draw_t = 0; FirstOrderFilter fps_filter; From d2400150e51833678f8c216c0c605111cfa13018 Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Wed, 15 Jun 2022 23:16:18 +0900 Subject: [PATCH 089/436] ui: advanced network settings fix button colors and sizes (#24846) * ui: button pressed color add * match colors from other buttons Co-authored-by: Willem Melching --- selfdrive/ui/qt/offroad/networking.cc | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 9c2088b1d79b83..80c7ad045bd61c 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -28,9 +28,9 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { vlayout->setContentsMargins(20, 20, 20, 20); if (show_advanced) { QPushButton* advancedSettings = new QPushButton("Advanced"); - advancedSettings->setObjectName("advancedBtn"); + advancedSettings->setObjectName("advanced_btn"); advancedSettings->setStyleSheet("margin-right: 30px;"); - advancedSettings->setFixedSize(350, 100); + advancedSettings->setFixedSize(400, 100); connect(advancedSettings, &QPushButton::clicked, [=]() { main_layout->setCurrentWidget(an); }); vlayout->addSpacing(10); vlayout->addWidget(advancedSettings, 0, Qt::AlignRight); @@ -57,14 +57,17 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { // TODO: revisit pressed colors setStyleSheet(R"( - #wifiWidget > QPushButton, #back_btn, #advancedBtn { + #wifiWidget > QPushButton, #back_btn, #advanced_btn { font-size: 50px; margin: 0px; padding: 15px; border-width: 0; border-radius: 30px; color: #dddddd; - background-color: #444444; + background-color: #393939; + } + #back_btn:pressed, #advanced_btn:pressed { + background-color: #4a4a4a; } )"); main_layout->setCurrentWidget(wifiScreen); @@ -118,7 +121,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid // Back button QPushButton* back = new QPushButton("Back"); back->setObjectName("back_btn"); - back->setFixedSize(500, 100); + back->setFixedSize(400, 100); connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); main_layout->addWidget(back, 0, Qt::AlignLeft); From bb02c1c5158b3403e7d2395611d2db1ab44aed5e Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 15 Jun 2022 16:19:04 +0200 Subject: [PATCH 090/436] networking.cc: remove resolved TODO --- selfdrive/ui/qt/offroad/networking.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 80c7ad045bd61c..418ccd31793270 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -55,7 +55,6 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { setAutoFillBackground(true); setPalette(pal); - // TODO: revisit pressed colors setStyleSheet(R"( #wifiWidget > QPushButton, #back_btn, #advanced_btn { font-size: 50px; From 41721917239e8eac314a2dc6c64cfb4695ff460e Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Thu, 16 Jun 2022 00:35:46 +0900 Subject: [PATCH 091/436] selfdrive/ui/qt/util.cc: missing include (#24867) --- selfdrive/ui/qt/util.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 408652a0b9b414..366aa76f494d53 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -3,6 +3,7 @@ #include #include #include +#include #include "common/params.h" #include "common/swaglog.h" From fa4f017bbe6c117913ec2327e9129c70b3e47223 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 15 Jun 2022 18:03:15 +0200 Subject: [PATCH 092/436] laikad: calc_pos_fix numpy implementation (#24865) * Replace posfix with gauss newton method * Cleanup * Check if glonass is in the list * Fix * also return residual * Add residuals * Update selfdrive/locationd/laikad.py Co-authored-by: Willem Melching * Cleanup Co-authored-by: Gijs Koning --- selfdrive/locationd/laikad.py | 103 +++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 8 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index ebe7404e17618e..8eb81345ff1924 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -7,16 +7,17 @@ import numpy as np from collections import defaultdict +import sympy from numpy.linalg import linalg from cereal import log, messaging from common.params import Params, put_nonblocking from laika import AstroDog -from laika.constants import SECS_IN_HR, SECS_IN_MIN +from laika.constants import EARTH_ROTATION_RATE, SECS_IN_HR, SECS_IN_MIN, SPEED_OF_LIGHT from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId -from laika.raw_gnss import GNSSMeasurement, calc_pos_fix, correct_measurements, process_measurements, read_raw_ublox +from laika.raw_gnss import GNSSMeasurement, correct_measurements, process_measurements, read_raw_ublox from selfdrive.locationd.models.constants import GENERATED_DIR, ObservationKind from selfdrive.locationd.models.gnss_kf import GNSSKalman from selfdrive.locationd.models.gnss_kf import States as GStates @@ -38,6 +39,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.last_cached_t = None self.save_ephemeris = save_ephemeris self.load_cache() + self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} def load_cache(self): cache = Params().get(EPHEMERIS_CACHE) @@ -66,7 +68,9 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) new_meas = read_raw_ublox(report) processed_measurements = process_measurements(new_meas, self.astro_dog) - pos_fix = calc_pos_fix(processed_measurements, min_measurements=4) + + min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 + pos_fix = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) t = ublox_mono_time * 1e-9 kf_pos_std = None @@ -84,7 +88,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if est_pos is not None: corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) - self.update_localizer(pos_fix, t, corrected_measurements) + self.update_localizer(est_pos, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() @@ -110,7 +114,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): # elif ublox_msg.which == 'ionoData': # todo add this. Needed to better correct messages offline. First fix ublox_msg.cc to sent them. - def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement]): + def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement]): # Check time and outputs are valid valid = self.kf_valid(t) if not all(valid): @@ -123,11 +127,10 @@ def update_localizer(self, pos_fix, t: float, measurements: List[GNSSMeasurement else: cloudlog.error("Gnss kalman std too far") - if len(pos_fix) == 0: + if est_pos is None: cloudlog.info("Position fix not available when resetting kalman filter") return - post_est = pos_fix[0][:3].tolist() - self.init_gnss_localizer(post_est) + self.init_gnss_localizer(est_pos.tolist()) if len(measurements) > 0: kf_add_observations(self.gnss_kf, t, measurements) else: @@ -227,6 +230,90 @@ def deserialize_hook(dct): return dct +def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C1C', min_measurements=6): + ''' + Calculates gps fix using gauss newton method + To solve the problem a minimal of 4 measurements are required. + If Glonass is included 5 are required to solve for the additional free variable. + returns: + 0 -> list with positions + ''' + if x0 is None: + x0 = [0, 0, 0, 0, 0] + n = len(measurements) + if n < min_measurements: + return [] + + Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) + x = gauss_newton(Fx_pos, x0) + residual, _ = Fx_pos(x, weight=1.0) + return x, residual + + +def pr_residual(measurements, posfix_functions, signal='C1C'): + def Fx_pos(inp, weight=None): + vals, gradients = [], [] + + for meas in measurements: + pr = meas.observables[signal] + pr += meas.sat_clock_err * SPEED_OF_LIGHT + + w = (1 / meas.observables_std[signal]) if weight is None else weight + + val, *gradient = posfix_functions[meas.constellation_id](*inp, pr, *meas.sat_pos, w) + vals.append(val) + gradients.append(gradient) + return np.asarray(vals), np.asarray(gradients) + + return Fx_pos + + +def gauss_newton(fun, b, xtol=1e-8, max_n=25): + for _ in range(max_n): + # Compute function and jacobian on current estimate + r, J = fun(b) + + # Update estimate + delta = np.linalg.pinv(J) @ r + b -= delta + + # Check step size for stopping condition + if np.linalg.norm(delta) < xtol: + break + return b + + +def get_posfix_sympy_fun(constellation): + # Unknowns + x, y, z = sympy.Symbol('x'), sympy.Symbol('y'), sympy.Symbol('z') + bc = sympy.Symbol('bc') + bg = sympy.Symbol('bg') + var = [x, y, z, bc, bg] + + # Knowns + pr = sympy.Symbol('pr') + sat_x, sat_y, sat_z = sympy.Symbol('sat_x'), sympy.Symbol('sat_y'), sympy.Symbol('sat_z') + weight = sympy.Symbol('weight') + + theta = EARTH_ROTATION_RATE * (pr - bc) / SPEED_OF_LIGHT + val = sympy.sqrt( + (sat_x * sympy.cos(theta) + sat_y * sympy.sin(theta) - x) ** 2 + + (sat_y * sympy.cos(theta) - sat_x * sympy.sin(theta) - y) ** 2 + + (sat_z - z) ** 2 + ) + + if constellation == ConstellationId.GLONASS: + res = weight * (val - (pr - bc - bg)) + elif constellation == ConstellationId.GPS: + res = weight * (val - (pr - bc)) + else: + raise NotImplementedError(f"Constellation {constellation} not supported") + + res = [res] + [sympy.diff(res, v) for v in var] + + return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) + + def main(): sm = messaging.SubMaster(['ubloxGnss']) pm = messaging.PubMaster(['gnssMeasurements']) From efc8aa05b4573f9ac96d518fbf04b6d799e487d0 Mon Sep 17 00:00:00 2001 From: Harald Schafer Date: Wed, 15 Jun 2022 11:32:26 -0700 Subject: [PATCH 093/436] Update model ref --- selfdrive/test/process_replay/model_replay_ref_commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index a7a909d2a4db24..ec5ef113114e4b 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -512a9d4596c8faba304d6f7ded2ce77837357b65 +6ec6db2f2a070ba1603bbe1d934509781cc4556a From c851cf737931cba16909b2fe20ffec4858acd374 Mon Sep 17 00:00:00 2001 From: Harald Schafer Date: Wed, 15 Jun 2022 11:33:59 -0700 Subject: [PATCH 094/436] Revert "Update model ref" This reverts commit efc8aa05b4573f9ac96d518fbf04b6d799e487d0. --- selfdrive/test/process_replay/model_replay_ref_commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index ec5ef113114e4b..a7a909d2a4db24 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -6ec6db2f2a070ba1603bbe1d934509781cc4556a +512a9d4596c8faba304d6f7ded2ce77837357b65 From 9283040d847b120fdf7759d5bd12000863e12f73 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Wed, 15 Jun 2022 15:29:42 -0700 Subject: [PATCH 095/436] Rocket league model (#24869) * dd9a502d-c8e2-4831-b365-804b0ae0739d/600 80041070-d276-4fed-bdb9-0075e5442908/420 * no elementwise op * 9dabf0fe-2e60-44bf-8d3a-d20a74aca072/600 ae746590-0bb5-4a16-80db-15f02d314f03/300 c4663a12-b499-4c9b-90dd-b169e3948cb1/60 * explicit slice * some copies are useful * 1456d261-d232-4654-8885-4d9fde883894/440 c06eba55-1931-4e00-9d63-acad00161be0/700 af2eb6ba-1935-4318-aaf8-868db81a4932/425 * 154f663e-d3e9-4020-ad49-0e640588ebbe/399 badb5e69-504f-4544-a99e-ba75ed204b74/800 08330327-7663-4874-af7a-dcbd2c994ba7/800 * set steer rate cost to 1.0 * smaller temporal size * Update model reg * update model ref again * This did upload somehow * Update steer rate cost Co-authored-by: Yassine Yousfi --- selfdrive/car/body/interface.py | 1 - selfdrive/car/chrysler/interface.py | 1 - selfdrive/car/ford/interface.py | 1 - selfdrive/car/gm/interface.py | 1 - selfdrive/car/honda/interface.py | 1 - selfdrive/car/hyundai/interface.py | 1 - selfdrive/car/mazda/interface.py | 1 - selfdrive/car/nissan/interface.py | 1 - selfdrive/car/subaru/interface.py | 1 - selfdrive/car/tesla/interface.py | 1 - selfdrive/car/tests/test_car_interfaces.py | 1 - selfdrive/car/tests/test_models.py | 1 - selfdrive/car/toyota/interface.py | 1 - selfdrive/car/volkswagen/interface.py | 1 - selfdrive/controls/lib/lateral_planner.py | 9 ++++----- selfdrive/controls/plannerd.py | 2 +- selfdrive/modeld/models/driving.h | 2 +- selfdrive/modeld/models/supercombo.dlc | 4 ++-- selfdrive/modeld/models/supercombo.onnx | 4 ++-- selfdrive/modeld/thneed/compile.cc | 2 +- selfdrive/modeld/thneed/optimizer.cc | 8 ++++---- selfdrive/test/process_replay/model_replay_ref_commit | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 23 files changed, 17 insertions(+), 32 deletions(-) diff --git a/selfdrive/car/body/interface.py b/selfdrive/car/body/interface.py index 3d6bd5e22abff8..e85735b1a60013 100644 --- a/selfdrive/car/body/interface.py +++ b/selfdrive/car/body/interface.py @@ -19,7 +19,6 @@ def get_params(candidate, fingerprint=None, car_fw=None, disable_radar=False): ret.minSteerSpeed = -math.inf ret.maxLateralAccel = math.inf # TODO: set to a reasonable value ret.steerRatio = 0.5 - ret.steerRateCost = 0.5 ret.steerLimitTimer = 1.0 ret.steerActuatorDelay = 0. diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 55672dd4ab60c0..817a5b387ef4c9 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -20,7 +20,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]] ret.lateralTuning.pid.kf = 0.00006 # full torque for 10 deg at 80mph means 0.00007818594 ret.steerActuatorDelay = 0.1 - ret.steerRateCost = 0.7 ret.steerLimitTimer = 0.4 if candidate in (CAR.JEEP_CHEROKEE, CAR.JEEP_CHEROKEE_2019): diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index ec7c723669be49..c608cc08d97f82 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -59,7 +59,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa # LCA can steer down to zero ret.minSteerSpeed = 0. - ret.steerRateCost = 1.0 ret.centerToFront = ret.wheelbase * 0.44 ret.rotationalInertia = scale_rot_inertia(ret.mass, ret.wheelbase) diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 498f4b334732f6..9c1744dfb4c60c 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -63,7 +63,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 - ret.steerRateCost = 1.0 ret.steerActuatorDelay = 0.1 # Default delay, not measured yet ret.longitudinalTuning.kpBP = [5., 35.] diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index ea7deab5d70b24..994152608e3bdd 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -319,7 +319,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl tire_stiffness_factor=tire_stiffness_factor) ret.steerActuatorDelay = 0.1 - ret.steerRateCost = 0.5 ret.steerLimitTimer = 0.8 return ret diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 6fd75ebfabc084..497d6b3f307a5e 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -43,7 +43,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.dashcamOnly = not os.path.exists('/data/enable-ev6') ret.steerActuatorDelay = 0.1 # Default delay - ret.steerRateCost = 0.5 ret.steerLimitTimer = 0.4 tire_stiffness_factor = 1. diff --git a/selfdrive/car/mazda/interface.py b/selfdrive/car/mazda/interface.py index 0ef573c6b644b8..cbeb910de24121 100755 --- a/selfdrive/car/mazda/interface.py +++ b/selfdrive/car/mazda/interface.py @@ -25,7 +25,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.dashcamOnly = candidate not in (CAR.CX5_2022, CAR.CX9_2021) ret.steerActuatorDelay = 0.1 - ret.steerRateCost = 1.0 ret.steerLimitTimer = 0.8 tire_stiffness_factor = 0.70 # not optimized yet diff --git a/selfdrive/car/nissan/interface.py b/selfdrive/car/nissan/interface.py index 3bf34e82660749..436cef68bf151c 100644 --- a/selfdrive/car/nissan/interface.py +++ b/selfdrive/car/nissan/interface.py @@ -14,7 +14,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.nissan)] ret.steerLimitTimer = 1.0 - ret.steerRateCost = 0.5 ret.steerActuatorDelay = 0.1 diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index d0d8e91ce18f70..8c5cab86e8731a 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -22,7 +22,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.dashcamOnly = candidate in PREGLOBAL_CARS - ret.steerRateCost = 0.7 ret.steerLimitTimer = 0.4 if candidate == CAR.ASCENT: diff --git a/selfdrive/car/tesla/interface.py b/selfdrive/car/tesla/interface.py index 71594cecb6831f..d4eda38e6441a6 100755 --- a/selfdrive/car/tesla/interface.py +++ b/selfdrive/car/tesla/interface.py @@ -42,7 +42,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerLimitTimer = 1.0 ret.steerActuatorDelay = 0.25 - ret.steerRateCost = 0.5 if candidate in (CAR.AP2_MODELS, CAR.AP1_MODELS): ret.mass = 2100. + STD_CARGO_KG diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 52c89440c74226..15df1aafef918c 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -33,7 +33,6 @@ def test_car_interfaces(self, car_name): assert car_interface self.assertGreater(car_params.mass, 1) - self.assertGreater(car_params.steerRateCost, 1e-3) if car_params.steerControlType != car.CarParams.SteerControlType.angle: tuning = car_params.lateralTuning.which() diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 9ab881279b2b1b..27dc4ba776096a 100755 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -121,7 +121,6 @@ def test_car_params(self): # make sure car params are within a valid range self.assertGreater(self.CP.mass, 1) - self.assertGreater(self.CP.steerRateCost, 1e-3) if self.CP.steerControlType != car.CarParams.SteerControlType.angle: tuning = self.CP.lateralTuning.which() diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 7969775a89bfb8..d0f47897758467 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -213,7 +213,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 4305. * CV.LB_TO_KG + STD_CARGO_KG set_lat_tune(ret.lateralTuning, LatTunes.PID_J) - ret.steerRateCost = 1. ret.centerToFront = ret.wheelbase * 0.44 # TODO: get actual value, for now starting with reasonable value for diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 3a0d7c8ce852fc..6782105c163af2 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -45,7 +45,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa # Global lateral tuning defaults, can be overridden per-vehicle ret.steerActuatorDelay = 0.1 - ret.steerRateCost = 1.0 ret.steerLimitTimer = 0.4 ret.steerRatio = 15.6 # Let the params learner figure this out tire_stiffness_factor = 1.0 # Let the params learner figure this out diff --git a/selfdrive/controls/lib/lateral_planner.py b/selfdrive/controls/lib/lateral_planner.py index ec21c16e91366c..019a19fb1dfe66 100644 --- a/selfdrive/controls/lib/lateral_planner.py +++ b/selfdrive/controls/lib/lateral_planner.py @@ -11,13 +11,12 @@ class LateralPlanner: - def __init__(self, CP, use_lanelines=True, wide_camera=False): + def __init__(self, use_lanelines=True, wide_camera=False): self.use_lanelines = use_lanelines self.LP = LanePlanner(wide_camera) self.DH = DesireHelper() self.last_cloudlog_t = 0 - self.steer_rate_cost = CP.steerRateCost self.solution_invalid_cnt = 0 self.path_xyz = np.zeros((TRAJECTORY_SIZE, 3)) @@ -59,12 +58,12 @@ def update(self, sm): # Calculate final driving path and set MPC costs if self.use_lanelines: d_path_xyz = self.LP.get_d_path(v_ego, self.t_idxs, self.path_xyz) - self.lat_mpc.set_weights(MPC_COST_LAT.PATH, MPC_COST_LAT.HEADING, self.steer_rate_cost) + self.lat_mpc.set_weights(MPC_COST_LAT.PATH, MPC_COST_LAT.HEADING, MPC_COST_LAT.STEER_RATE) else: d_path_xyz = self.path_xyz # Heading cost is useful at low speed, otherwise end of plan can be off-heading heading_cost = interp(v_ego, [5.0, 10.0], [MPC_COST_LAT.HEADING, 0.15]) - self.lat_mpc.set_weights(MPC_COST_LAT.PATH, heading_cost, self.steer_rate_cost) + self.lat_mpc.set_weights(MPC_COST_LAT.PATH, heading_cost, MPC_COST_LAT.STEER_RATE) y_pts = np.interp(v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(d_path_xyz, axis=1), d_path_xyz[:, 1]) heading_pts = np.interp(v_ego * self.t_idxs[:LAT_MPC_N + 1], np.linalg.norm(self.path_xyz, axis=1), self.plan_yaw) @@ -79,7 +78,7 @@ def update(self, sm): y_pts, heading_pts) # init state for next - # mpc.u_sol is the desired curvature rate given x0 curv state. + # mpc.u_sol is the desired curvature rate given x0 curv state. # with x0[3] = measured_curvature, this would be the actual desired rate. # instead, interpolate x_sol so that x0[3] is the desired curvature for lat_control. self.x0[3] = interp(DT_MDL, self.t_idxs[:LAT_MPC_N + 1], self.lat_mpc.x_sol[:, 3]) diff --git a/selfdrive/controls/plannerd.py b/selfdrive/controls/plannerd.py index 06807b2a5cb84c..9356a55d8433f8 100755 --- a/selfdrive/controls/plannerd.py +++ b/selfdrive/controls/plannerd.py @@ -22,7 +22,7 @@ def plannerd_thread(sm=None, pm=None): cloudlog.event("e2e mode", on=use_lanelines) longitudinal_planner = Planner(CP) - lateral_planner = LateralPlanner(CP, use_lanelines=use_lanelines, wide_camera=wide_camera) + lateral_planner = LateralPlanner(use_lanelines=use_lanelines, wide_camera=wide_camera) if sm is None: sm = messaging.SubMaster(['carState', 'controlsState', 'radarState', 'modelV2'], diff --git a/selfdrive/modeld/models/driving.h b/selfdrive/modeld/models/driving.h index 97e65fbc0e4b4e..a6910516364d38 100644 --- a/selfdrive/modeld/models/driving.h +++ b/selfdrive/modeld/models/driving.h @@ -245,7 +245,7 @@ struct ModelOutput { constexpr int OUTPUT_SIZE = sizeof(ModelOutput) / sizeof(float); #ifdef TEMPORAL - constexpr int TEMPORAL_SIZE = 512; + constexpr int TEMPORAL_SIZE = 512+256; #else constexpr int TEMPORAL_SIZE = 0; #endif diff --git a/selfdrive/modeld/models/supercombo.dlc b/selfdrive/modeld/models/supercombo.dlc index 23f6d904fb238b..90f7a2e65b94da 100644 --- a/selfdrive/modeld/models/supercombo.dlc +++ b/selfdrive/modeld/models/supercombo.dlc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:027cbb1fabae369878271cb0e3505071a8bdaa07473fad9a0b2e8d695c5dc1ff -size 76725611 +oid sha256:4c2cb3a3054f3292bbe538d6b793908dc2e234c200802d41b6766d3cb51b0b44 +size 101662751 diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 9023c18dd7f970..0493398560cd27 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:484976ea5bd4ddcabc82e95faf30d7311a27802c1e337472558699fa2395a499 -size 77472267 +oid sha256:96b60d0bfd1386c93b4f79195aa1c5e77b23e0250578a308ee2c58857ed5eb49 +size 102570834 diff --git a/selfdrive/modeld/thneed/compile.cc b/selfdrive/modeld/thneed/compile.cc index f76c63b2b917cd..a2f55ffd972b0b 100644 --- a/selfdrive/modeld/thneed/compile.cc +++ b/selfdrive/modeld/thneed/compile.cc @@ -5,7 +5,7 @@ #include "selfdrive/modeld/thneed/thneed.h" #include "system/hardware/hw.h" -#define TEMPORAL_SIZE 512 +#define TEMPORAL_SIZE 512+256 #define DESIRE_LEN 8 #define TRAFFIC_CONVENTION_LEN 2 diff --git a/selfdrive/modeld/thneed/optimizer.cc b/selfdrive/modeld/thneed/optimizer.cc index 03d20ff3861332..39737d3d7623ba 100644 --- a/selfdrive/modeld/thneed/optimizer.cc +++ b/selfdrive/modeld/thneed/optimizer.cc @@ -9,7 +9,7 @@ extern map g_program_source; -static int is_same_size_image(cl_mem a, cl_mem b) { +/*static int is_same_size_image(cl_mem a, cl_mem b) { size_t a_width, a_height, a_depth, a_array_size, a_row_pitch, a_slice_pitch; clGetImageInfo(a, CL_IMAGE_WIDTH, sizeof(a_width), &a_width, NULL); clGetImageInfo(a, CL_IMAGE_HEIGHT, sizeof(a_height), &a_height, NULL); @@ -29,7 +29,7 @@ static int is_same_size_image(cl_mem a, cl_mem b) { return (a_width == b_width) && (a_height == b_height) && (a_depth == b_depth) && (a_array_size == b_array_size) && (a_row_pitch == b_row_pitch) && (a_slice_pitch == b_slice_pitch); -} +}*/ static cl_mem make_image_like(cl_context context, cl_mem val) { cl_image_format format; @@ -138,7 +138,7 @@ int Thneed::optimize() { // delete useless copy layers // saves ~0.7 ms - if (kq[i]->name == "concatenation" || kq[i]->name == "flatten") { + /*if (kq[i]->name == "concatenation" || kq[i]->name == "flatten") { string in = kq[i]->args[kq[i]->get_arg_num("input")]; string out = kq[i]->args[kq[i]->get_arg_num("output")]; if (is_same_size_image(*(cl_mem*)in.data(), *(cl_mem*)out.data())) { @@ -148,7 +148,7 @@ int Thneed::optimize() { kq.erase(kq.begin()+i); --i; } - } + }*/ // NOTE: if activations/accumulation are done in the wrong order, this will be wrong diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index a7a909d2a4db24..54c2ed54ad877f 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -512a9d4596c8faba304d6f7ded2ce77837357b65 +629eaa7b26d1721a71547f9de880f99732cb27f3 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b8976e5f389733..8ed68d5bddf082 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -1d66eed104dbc124c4e5679f5dddf40197b86ce9 +ed1dfb8b155ebcd8fdad4e06462b3bb7869fc67b From 725ccc01798c7f4b3278cc34525f99a6344e52aa Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 15 Jun 2022 18:01:02 -0700 Subject: [PATCH 096/436] HKG: simplify Kia K5 compatibility (#24810) LKAS/LFA is standard on the K5 --- docs/CARS.md | 2 +- selfdrive/car/hyundai/values.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 9b3322ac8eb0db..c0bd62eb3f97f7 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -95,7 +95,7 @@ How We Rate The Cars |Kia|Ceed 2019|SCC + LKAS|||||| |Kia|Forte 2018|SCC + LKAS|||||| |Kia|Forte 2019-21|SCC + LKAS|||||| -|Kia|K5 2021-22|SCC + LFA|||||| +|Kia|K5 2021-22|SCC|||||| |Kia|Niro Hybrid 2021|SCC + LKAS|||||| |Kia|Niro Hybrid 2022|SCC + LKAS|||||| |Kia|Optima 2019|SCC + LKAS|||||| diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 4ca81fdb403db8..df7ce1e6f4d209 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -80,6 +80,8 @@ class CAR: @dataclass class HyundaiCarInfo(CarInfo): + # TODO: we can probably remove LKAS. LKAS is standard on many + # HKG and for others, it's likely packaged together with SCC package: str = "SCC + LKAS" good_torque: bool = True @@ -121,7 +123,7 @@ class HyundaiCarInfo(CarInfo): HyundaiCarInfo("Kia Forte 2018", harness=Harness.hyundai_b), HyundaiCarInfo("Kia Forte 2019-21", harness=Harness.hyundai_g), ], - CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", "SCC + LFA", harness=Harness.hyundai_a), + CAR.KIA_K5_2021: HyundaiCarInfo("Kia K5 2021-22", "SCC", harness=Harness.hyundai_a), CAR.KIA_NIRO_EV: [ HyundaiCarInfo("Kia Niro Electric 2019-20", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_f), HyundaiCarInfo("Kia Niro Electric 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), From e910ce87a44884a3f216fbced306e8d3124a04de Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 15 Jun 2022 21:42:54 -0700 Subject: [PATCH 097/436] thermald: fix panda dropout when we miss a pandaStates (#24870) immediate fix for "panda dropout" --- selfdrive/thermald/thermald.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index b909d1198cdf49..6cf5c428a88d1d 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -220,7 +220,7 @@ def thermald_thread(end_event, hw_queue): if TICI: fan_controller = TiciFanController() - elif (sec_since_boot() - sm.rcv_time['pandaStates']/1e9) > DISCONNECT_TIMEOUT: + elif (sec_since_boot() - sm.rcv_time['pandaStates']) > DISCONNECT_TIMEOUT: if onroad_conditions["ignition"]: onroad_conditions["ignition"] = False cloudlog.error("panda timed out onroad") From 76593b1da02cae7165aaa699fd7ea807558751b0 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Wed, 15 Jun 2022 22:50:39 -0700 Subject: [PATCH 098/436] VW MQB: Populate stock ACC standstill flag (#24877) * VW MQB: Populate stock ACC standstill flag * it really do be like that sometimes --- selfdrive/car/volkswagen/carcontroller.py | 2 +- selfdrive/car/volkswagen/carstate.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index aeb56c5d67b334..842eb6d13911b0 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -93,7 +93,7 @@ def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visib # Cancel ACC if it's engaged with OP disengaged. self.graButtonStatesToSend = BUTTON_STATES.copy() self.graButtonStatesToSend["cancel"] = True - elif c.enabled and CS.esp_hold_confirmation: + elif c.enabled and CS.out.cruiseState.standstill: # Blip the Resume button if we're engaged at standstill. # FIXME: This is a naive implementation, improve with visiond or radar input. self.graButtonStatesToSend = BUTTON_STATES.copy() diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index 4193030d87bb92..9c4a111e4e8cf7 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -50,7 +50,6 @@ def update(self, pt_cp, cam_cp, ext_cp, trans_type): ret.brake = pt_cp.vl["ESP_05"]["ESP_Bremsdruck"] / 250.0 # FIXME: this is pressure in Bar, not sure what OP expects ret.brakePressed = bool(pt_cp.vl["ESP_05"]["ESP_Fahrer_bremst"]) ret.parkingBrake = bool(pt_cp.vl["Kombi_01"]["KBI_Handbremse"]) # FIXME: need to include an EPB check as well - self.esp_hold_confirmation = pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"] # Update gear and/or clutch position data. if trans_type == TransmissionType.automatic: @@ -105,6 +104,7 @@ def update(self, pt_cp, cam_cp, ext_cp, trans_type): # ACC okay but disabled (1), or a radar visibility or other fault/disruption (6 or 7) ret.cruiseState.available = False ret.cruiseState.enabled = False + ret.cruiseState.standstill = bool(pt_cp.vl["ESP_21"]["ESP_Haltebestaetigung"]) ret.accFaulted = pt_cp.vl["TSK_06"]["TSK_Status"] in (6, 7) # Update ACC setpoint. When the setpoint is zero or there's an error, the From 5aaf5be54c544a822483ad3e7c354fdb89cae45c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 16 Jun 2022 01:47:29 -0700 Subject: [PATCH 099/436] Mazda: fix resume spam at standstill (#24876) * Fix Mazda resume spam at standstill * one line * Revert "one line" This reverts commit 30c6504ed36adb92991acd6ebf42ffd6fe6c3b8c. --- selfdrive/car/mazda/carcontroller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py index 68eebeaf71bfa0..d003079d63eea0 100644 --- a/selfdrive/car/mazda/carcontroller.py +++ b/selfdrive/car/mazda/carcontroller.py @@ -27,6 +27,7 @@ def update(self, c, CS, frame): CS.out.steeringTorque, CarControllerParams) self.steer_rate_limited = new_steer != apply_steer + if c.enabled: if CS.out.standstill and frame % 5 == 0: # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds # Send Resume button at 20hz if we're engaged at standstill to support full stop and go! From 5958e78037e468795e0db8d42d5209e1e41aec18 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 16 Jun 2022 15:21:33 +0200 Subject: [PATCH 100/436] Laikad: More logging and use last_pos_fix for correcting (#24868) Use last available pos_fix for correcting measurements. Improve logging measurements --- cereal | 2 +- laika_repo | 2 +- selfdrive/locationd/laikad.py | 77 ++++++++++++++----------- selfdrive/locationd/test/test_laikad.py | 76 +++++++++++++++++------- 4 files changed, 100 insertions(+), 57 deletions(-) diff --git a/cereal b/cereal index 4b4d9aab03937f..3166178611989c 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 4b4d9aab03937f5a45677ff15efb275abf9c958b +Subproject commit 3166178611989ca555d7b254130375d1d8a55cc8 diff --git a/laika_repo b/laika_repo index a3a80dc4f7977b..951ab080b998ee 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit a3a80dc4f7977b2232946e56a16770e413190818 +Subproject commit 951ab080b998ee3edde6229654d1a4cb63cda6a9 diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 8eb81345ff1924..0e2838da6d757c 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -2,13 +2,13 @@ import json import time from concurrent.futures import Future, ProcessPoolExecutor +from enum import IntEnum from typing import List, Optional import numpy as np from collections import defaultdict import sympy -from numpy.linalg import linalg from cereal import log, messaging from common.params import Params, put_nonblocking @@ -30,7 +30,7 @@ class Laikad: def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), - save_ephemeris=False): + save_ephemeris=False, last_known_position=None): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) self.gnss_kf = GNSSKalman(GENERATED_DIR) self.orbit_fetch_executor = ProcessPoolExecutor() @@ -40,6 +40,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.save_ephemeris = save_ephemeris self.load_cache() self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} + self.last_pos_fix = last_known_position def load_cache(self): cache = Params().get(EPHEMERIS_CACHE) @@ -70,27 +71,16 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): processed_measurements = process_measurements(new_meas, self.astro_dog) min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 - pos_fix = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) + pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) + if len(pos_fix) > 0: + self.last_pos_fix = pos_fix[:3] + est_pos = self.last_pos_fix - t = ublox_mono_time * 1e-9 - kf_pos_std = None - if all(self.kf_valid(t)): - self.gnss_kf.predict(t) - kf_pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())) - # If localizer is valid use its position to correct measurements - if kf_pos_std is not None and linalg.norm(kf_pos_std) < 100: - est_pos = self.gnss_kf.x[GStates.ECEF_POS] - elif len(pos_fix) > 0 and abs(np.array(pos_fix[1])).mean() < 1000: - est_pos = pos_fix[0][:3] - else: - est_pos = None - corrected_measurements = [] - if est_pos is not None: - corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) + corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) if est_pos is not None else [] + t = ublox_mono_time * 1e-9 self.update_localizer(est_pos, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) - ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() @@ -103,6 +93,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): dat.gnssMeasurements = { "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), + "positionFixECEF": measurement_msg(value=pos_fix, std=pos_fix_residual, valid=len(pos_fix) > 0), "ubloxMonoTime": ublox_mono_time, "correctedMeasurements": meas_msgs } @@ -124,13 +115,7 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") - else: - cloudlog.error("Gnss kalman std too far") - - if est_pos is None: - cloudlog.info("Position fix not available when resetting kalman filter") - return - self.init_gnss_localizer(est_pos.tolist()) + self.init_gnss_localizer(est_pos) if len(measurements) > 0: kf_add_observations(self.gnss_kf, t, measurements) else: @@ -141,14 +126,13 @@ def kf_valid(self, t: float): filter_time = self.gnss_kf.filter.filter_time return [filter_time is not None, filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, - all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS])), - linalg.norm(self.gnss_kf.P[GStates.ECEF_POS]) < 1e5] + all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS]))] def init_gnss_localizer(self, est_pos): x_initial, p_initial_diag = np.copy(GNSSKalman.x_initial), np.copy(np.diagonal(GNSSKalman.P_initial)) - x_initial[GStates.ECEF_POS] = est_pos + if est_pos is not None: + x_initial[GStates.ECEF_POS] = est_pos p_initial_diag[GStates.ECEF_POS] = 1000 ** 2 - self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) def fetch_orbits(self, t: GPSTime, block): @@ -192,6 +176,27 @@ def create_measurement_msg(meas: GNSSMeasurement): c.pseudorangeRateStd = float(meas.observables_std['D1C']) c.satPos = meas.sat_pos_final.tolist() c.satVel = meas.sat_vel.tolist() + c.satVel = meas.sat_vel.tolist() + ephem = meas.sat_ephemeris + assert ephem is not None + if ephem.eph_type == EphemerisType.NAV: + source_type = EphemerisSourceType.nav + week, time_of_week = -1, -1 + else: + assert ephem.file_epoch is not None + week = ephem.file_epoch.week + time_of_week = ephem.file_epoch.tow + file_src = ephem.file_source + if file_src == 'igu': # example nasa: '2214/igu22144_00.sp3.Z' + source_type = EphemerisSourceType.nasaUltraRapid + elif file_src == 'Sta': # example nasa: '22166/ultra/Stark_1D_22061518.sp3' + source_type = EphemerisSourceType.glonassIacUltraRapid + else: + raise Exception(f"Didn't expect file source {file_src}") + + c.ephemerisSource.type = source_type.value + c.ephemerisSource.gpsWeek = week + c.ephemerisSource.gpsTimeOfWeek = time_of_week return c @@ -242,12 +247,12 @@ def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C x0 = [0, 0, 0, 0, 0] n = len(measurements) if n < min_measurements: - return [] + return [], [] Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) x = gauss_newton(Fx_pos, x0) residual, _ = Fx_pos(x, weight=1.0) - return x, residual + return x.tolist(), residual.tolist() def pr_residual(measurements, posfix_functions, signal='C1C'): @@ -314,10 +319,16 @@ def get_posfix_sympy_fun(constellation): return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) +class EphemerisSourceType(IntEnum): + nav = 0 + nasaUltraRapid = 1 + glonassIacUltraRapid = 2 + + def main(): sm = messaging.SubMaster(['ubloxGnss']) pm = messaging.PubMaster(['gnssMeasurements']) - + # todo get last_known_position laikad = Laikad(save_ephemeris=True) while True: sm.update() diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 01ea8fee27f23b..3a72303b0c0533 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 import time import unittest +from collections import defaultdict from datetime import datetime from unittest import mock from unittest.mock import Mock, patch from common.params import Params -from laika.ephemeris import EphemerisType +from laika.ephemeris import EphemerisType, GPSEphemeris from laika.gps_time import GPSTime from laika.helpers import ConstellationId, TimeRangeHolder from laika.raw_gnss import GNSSMeasurement, read_raw_ublox -from selfdrive.locationd.laikad import EPHEMERIS_CACHE, Laikad, create_measurement_msg +from selfdrive.locationd.laikad import EPHEMERIS_CACHE, EphemerisSourceType, Laikad, create_measurement_msg from selfdrive.test.openpilotci import get_url from tools.lib.logreader import LogReader @@ -33,23 +34,62 @@ def verify_messages(lr, laikad, return_one_success=False): return good_msgs +def get_first_gps_time(logs): + for m in logs: + if m.ubloxGnss.which == 'measurementReport': + new_meas = read_raw_ublox(m.ubloxGnss.measurementReport) + if len(new_meas) > 0: + return new_meas[0].recv_time + + +def get_measurement_mock(gpstime, sat_ephemeris): + meas = GNSSMeasurement(ConstellationId.GPS, 1, gpstime.week, gpstime.tow, {'C1C': 0., 'D1C': 0.}, {'C1C': 0., 'D1C': 0.}) + # Fake measurement being processed + meas.observables_final = meas.observables + meas.sat_ephemeris = sat_ephemeris + return meas + + class TestLaikad(unittest.TestCase): @classmethod def setUpClass(cls): - cls.logs = get_log(range(1)) + logs = get_log(range(1)) + cls.logs = logs + first_gps_time = get_first_gps_time(logs) + cls.first_gps_time = first_gps_time def setUp(self): Params().delete(EPHEMERIS_CACHE) - def test_create_msg_without_errors(self): - gpstime = GPSTime.from_datetime(datetime.now()) - meas = GNSSMeasurement(ConstellationId.GPS, 1, gpstime.week, gpstime.tow, {'C1C': 0., 'D1C': 0.}, {'C1C': 0., 'D1C': 0.}) - # Fake observables_final to be correct - meas.observables_final = meas.observables + def test_ephemeris_source_in_msg(self): + data_mock = defaultdict(str) + data_mock['sv_id'] = 1 + + gpstime = GPSTime.from_datetime(datetime(2022, month=3, day=1)) + laikad = Laikad() + laikad.fetch_orbits(gpstime, block=True) + meas = get_measurement_mock(gpstime, laikad.astro_dog.orbits['R01'][0]) + msg = create_measurement_msg(meas) + self.assertEqual(msg.ephemerisSource.type.raw, EphemerisSourceType.glonassIacUltraRapid) + # Verify gps satellite returns same source + meas = get_measurement_mock(gpstime, laikad.astro_dog.orbits['R01'][0]) + msg = create_measurement_msg(meas) + self.assertEqual(msg.ephemerisSource.type.raw, EphemerisSourceType.glonassIacUltraRapid) + + # Test nasa source by using older date + gpstime = GPSTime.from_datetime(datetime(2021, month=3, day=1)) + laikad = Laikad() + laikad.fetch_orbits(gpstime, block=True) + meas = get_measurement_mock(gpstime, laikad.astro_dog.orbits['G01'][0]) msg = create_measurement_msg(meas) + self.assertEqual(msg.ephemerisSource.type.raw, EphemerisSourceType.nasaUltraRapid) - self.assertEqual(msg.constellationId, 'gps') + # Test nav source type + ephem = GPSEphemeris(data_mock, gpstime) + meas = get_measurement_mock(gpstime, ephem) + msg = create_measurement_msg(meas) + self.assertEqual(msg.ephemerisSource.type.raw, EphemerisSourceType.nav) def test_laika_online(self): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT) @@ -76,18 +116,10 @@ def test_laika_offline(self, downloader_mock): self.assertEqual(256, len(correct_msgs)) self.assertEqual(256, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) - def get_first_gps_time(self): - for m in self.logs: - if m.ubloxGnss.which == 'measurementReport': - new_meas = read_raw_ublox(m.ubloxGnss.measurementReport) - if len(new_meas) != 0: - return new_meas[0].recv_time - def test_laika_get_orbits(self): laikad = Laikad(auto_update=False) - first_gps_time = self.get_first_gps_time() # Pretend process has loaded the orbits on startup by using the time of the first gps message. - laikad.fetch_orbits(first_gps_time, block=True) + laikad.fetch_orbits(self.first_gps_time, block=True) self.dict_has_values(laikad.astro_dog.orbits) @unittest.skip("Use to debug live data") @@ -117,7 +149,6 @@ def test_get_orbits_in_process(self): def test_cache(self): laikad = Laikad(auto_update=True, save_ephemeris=True) - first_gps_time = self.get_first_gps_time() def wait_for_cache(): max_time = 2 @@ -126,13 +157,14 @@ def wait_for_cache(): max_time -= 0.1 if max_time == 0: self.fail("Cache has not been written after 2 seconds") + # Test cache with no ephemeris laikad.cache_ephemeris(t=GPSTime(0, 0)) wait_for_cache() Params().delete(EPHEMERIS_CACHE) - laikad.astro_dog.get_navs(first_gps_time) - laikad.fetch_orbits(first_gps_time, block=True) + laikad.astro_dog.get_navs(self.first_gps_time) + laikad.fetch_orbits(self.first_gps_time, block=True) # Wait for cache to save wait_for_cache() @@ -149,7 +181,7 @@ def wait_for_cache(): with patch('selfdrive.locationd.laikad.get_orbit_data', return_value=None) as mock_method: # Verify no orbit downloads even if orbit fetch times is reset since the cache has recently been saved and we don't want to download high frequently laikad.astro_dog.orbit_fetched_times = TimeRangeHolder() - laikad.fetch_orbits(first_gps_time, block=False) + laikad.fetch_orbits(self.first_gps_time, block=False) mock_method.assert_not_called() # Verify cache is working for only orbits by running a segment From 2699bae778fa7d9a4d40cf169b1eb56c74cb22c8 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 16 Jun 2022 16:06:41 +0200 Subject: [PATCH 101/436] ui: disable controls unresponsive alert on PC --- selfdrive/ui/ui.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 192a353ad11f91..cbf3921e4ebefa 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -54,7 +54,7 @@ struct Alert { return {"openpilot Unavailable", "Waiting for controls to start", "controlsWaiting", cereal::ControlsState::AlertSize::MID, AudibleAlert::NONE}; - } else if (controls_missing > CONTROLS_TIMEOUT) { + } else if (controls_missing > CONTROLS_TIMEOUT && !Hardware::PC()) { // car is started, but controls is lagging or died if (cs.getEnabled() && (controls_missing - CONTROLS_TIMEOUT) < 10) { return {"TAKE CONTROL IMMEDIATELY", "Controls Unresponsive", From dc98511b7a7092e969237655b6e08e4fa857106d Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 16 Jun 2022 17:45:08 +0200 Subject: [PATCH 102/436] laikad: fixes to run on device (#24879) * Always run laikad on device! * Update laika * Update laika * Fix gps week and time of week in msg * Reset kalman filter if pos_fix or last_known_position * put behind file * move pr parsing into common file Co-authored-by: Willem Melching --- laika_repo | 2 +- release/files_common | 9 +- selfdrive/locationd/laikad.py | 103 +++------------------ selfdrive/locationd/laikad_helpers.py | 89 ++++++++++++++++++ selfdrive/locationd/models/gnss_helpers.py | 21 +++++ selfdrive/locationd/models/gnss_kf.py | 2 +- selfdrive/locationd/models/loc_kf.py | 25 +---- selfdrive/manager/process_config.py | 1 + 8 files changed, 135 insertions(+), 117 deletions(-) create mode 100644 selfdrive/locationd/laikad_helpers.py create mode 100644 selfdrive/locationd/models/gnss_helpers.py diff --git a/laika_repo b/laika_repo index 951ab080b998ee..44f048bc1f58ae 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 951ab080b998ee3edde6229654d1a4cb63cda6a9 +Subproject commit 44f048bc1f58ae9e28dfdeb98e40aea3e0f2b699 diff --git a/release/files_common b/release/files_common index 6731ae4a120451..b6cd4fa797ff4a 100644 --- a/release/files_common +++ b/release/files_common @@ -209,15 +209,20 @@ selfdrive/locationd/generated/ubx.h selfdrive/locationd/generated/gps.cpp selfdrive/locationd/generated/gps.h +selfdrive/locationd/laikad.py +selfdrive/locationd/laikad_helpers.py +selfdrive/locationd/locationd.cc selfdrive/locationd/locationd.h selfdrive/locationd/locationd.cc selfdrive/locationd/paramsd.py selfdrive/locationd/models/.gitignore -selfdrive/locationd/models/live_kf.py selfdrive/locationd/models/car_kf.py -selfdrive/locationd/models/constants.py +selfdrive/locationd/models/gnss_kf.py +selfdrive/locationd/models/live_kf.py selfdrive/locationd/models/live_kf.h selfdrive/locationd/models/live_kf.cc +selfdrive/locationd/models/constants.py +selfdrive/locationd/models/gnss_helpers.py selfdrive/locationd/calibrationd.py diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 0e2838da6d757c..8ec570e2bec989 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 import json import time +from collections import defaultdict from concurrent.futures import Future, ProcessPoolExecutor from enum import IntEnum from typing import List, Optional import numpy as np -from collections import defaultdict - -import sympy from cereal import log, messaging from common.params import Params, put_nonblocking from laika import AstroDog -from laika.constants import EARTH_ROTATION_RATE, SECS_IN_HR, SECS_IN_MIN, SPEED_OF_LIGHT +from laika.constants import SECS_IN_HR, SECS_IN_MIN from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId from laika.raw_gnss import GNSSMeasurement, correct_measurements, process_measurements, read_raw_ublox +from selfdrive.locationd.laikad_helpers import calc_pos_fix_gauss_newton, get_posfix_sympy_fun from selfdrive.locationd.models.constants import GENERATED_DIR, ObservationKind from selfdrive.locationd.models.gnss_kf import GNSSKalman from selfdrive.locationd.models.gnss_kf import States as GStates @@ -91,6 +90,8 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): dat = messaging.new_message("gnssMeasurements") measurement_msg = log.LiveLocationKalman.Measurement.new_message dat.gnssMeasurements = { + "gpsWeek": report.gpsWeek, + "gpsTimeOfWeek": report.rcvTow, "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), "positionFixECEF": measurement_msg(value=pos_fix, std=pos_fix_residual, valid=len(pos_fix) > 0), @@ -115,7 +116,12 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") - self.init_gnss_localizer(est_pos) + if est_pos is not None: + cloudlog.info(f"Reset kalman filter with {est_pos}") + self.init_gnss_localizer(est_pos) + else: + cloudlog.info("Could not reset kalman filter") + return if len(measurements) > 0: kf_add_observations(self.gnss_kf, t, measurements) else: @@ -130,8 +136,7 @@ def kf_valid(self, t: float): def init_gnss_localizer(self, est_pos): x_initial, p_initial_diag = np.copy(GNSSKalman.x_initial), np.copy(np.diagonal(GNSSKalman.P_initial)) - if est_pos is not None: - x_initial[GStates.ECEF_POS] = est_pos + x_initial[GStates.ECEF_POS] = est_pos p_initial_diag[GStates.ECEF_POS] = 1000 ** 2 self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) @@ -235,90 +240,6 @@ def deserialize_hook(dct): return dct -def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C1C', min_measurements=6): - ''' - Calculates gps fix using gauss newton method - To solve the problem a minimal of 4 measurements are required. - If Glonass is included 5 are required to solve for the additional free variable. - returns: - 0 -> list with positions - ''' - if x0 is None: - x0 = [0, 0, 0, 0, 0] - n = len(measurements) - if n < min_measurements: - return [], [] - - Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) - x = gauss_newton(Fx_pos, x0) - residual, _ = Fx_pos(x, weight=1.0) - return x.tolist(), residual.tolist() - - -def pr_residual(measurements, posfix_functions, signal='C1C'): - def Fx_pos(inp, weight=None): - vals, gradients = [], [] - - for meas in measurements: - pr = meas.observables[signal] - pr += meas.sat_clock_err * SPEED_OF_LIGHT - - w = (1 / meas.observables_std[signal]) if weight is None else weight - - val, *gradient = posfix_functions[meas.constellation_id](*inp, pr, *meas.sat_pos, w) - vals.append(val) - gradients.append(gradient) - return np.asarray(vals), np.asarray(gradients) - - return Fx_pos - - -def gauss_newton(fun, b, xtol=1e-8, max_n=25): - for _ in range(max_n): - # Compute function and jacobian on current estimate - r, J = fun(b) - - # Update estimate - delta = np.linalg.pinv(J) @ r - b -= delta - - # Check step size for stopping condition - if np.linalg.norm(delta) < xtol: - break - return b - - -def get_posfix_sympy_fun(constellation): - # Unknowns - x, y, z = sympy.Symbol('x'), sympy.Symbol('y'), sympy.Symbol('z') - bc = sympy.Symbol('bc') - bg = sympy.Symbol('bg') - var = [x, y, z, bc, bg] - - # Knowns - pr = sympy.Symbol('pr') - sat_x, sat_y, sat_z = sympy.Symbol('sat_x'), sympy.Symbol('sat_y'), sympy.Symbol('sat_z') - weight = sympy.Symbol('weight') - - theta = EARTH_ROTATION_RATE * (pr - bc) / SPEED_OF_LIGHT - val = sympy.sqrt( - (sat_x * sympy.cos(theta) + sat_y * sympy.sin(theta) - x) ** 2 + - (sat_y * sympy.cos(theta) - sat_x * sympy.sin(theta) - y) ** 2 + - (sat_z - z) ** 2 - ) - - if constellation == ConstellationId.GLONASS: - res = weight * (val - (pr - bc - bg)) - elif constellation == ConstellationId.GPS: - res = weight * (val - (pr - bc)) - else: - raise NotImplementedError(f"Constellation {constellation} not supported") - - res = [res] + [sympy.diff(res, v) for v in var] - - return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) - - class EphemerisSourceType(IntEnum): nav = 0 nasaUltraRapid = 1 diff --git a/selfdrive/locationd/laikad_helpers.py b/selfdrive/locationd/laikad_helpers.py new file mode 100644 index 00000000000000..81f5ac3dd676b1 --- /dev/null +++ b/selfdrive/locationd/laikad_helpers.py @@ -0,0 +1,89 @@ +import numpy as np +import sympy + +from laika.constants import EARTH_ROTATION_RATE, SPEED_OF_LIGHT +from laika.helpers import ConstellationId + + +def calc_pos_fix_gauss_newton(measurements, posfix_functions, x0=None, signal='C1C', min_measurements=6): + ''' + Calculates gps fix using gauss newton method + To solve the problem a minimal of 4 measurements are required. + If Glonass is included 5 are required to solve for the additional free variable. + returns: + 0 -> list with positions + ''' + if x0 is None: + x0 = [0, 0, 0, 0, 0] + n = len(measurements) + if n < min_measurements: + return [], [] + + Fx_pos = pr_residual(measurements, posfix_functions, signal=signal) + x = gauss_newton(Fx_pos, x0) + residual, _ = Fx_pos(x, weight=1.0) + return x.tolist(), residual.tolist() + + +def pr_residual(measurements, posfix_functions, signal='C1C'): + def Fx_pos(inp, weight=None): + vals, gradients = [], [] + + for meas in measurements: + pr = meas.observables[signal] + pr += meas.sat_clock_err * SPEED_OF_LIGHT + + w = (1 / meas.observables_std[signal]) if weight is None else weight + + val, *gradient = posfix_functions[meas.constellation_id](*inp, pr, *meas.sat_pos, w) + vals.append(val) + gradients.append(gradient) + return np.asarray(vals), np.asarray(gradients) + + return Fx_pos + + +def gauss_newton(fun, b, xtol=1e-8, max_n=25): + for _ in range(max_n): + # Compute function and jacobian on current estimate + r, J = fun(b) + + # Update estimate + delta = np.linalg.pinv(J) @ r + b -= delta + + # Check step size for stopping condition + if np.linalg.norm(delta) < xtol: + break + return b + + +def get_posfix_sympy_fun(constellation): + # Unknowns + x, y, z = sympy.Symbol('x'), sympy.Symbol('y'), sympy.Symbol('z') + bc = sympy.Symbol('bc') + bg = sympy.Symbol('bg') + var = [x, y, z, bc, bg] + + # Knowns + pr = sympy.Symbol('pr') + sat_x, sat_y, sat_z = sympy.Symbol('sat_x'), sympy.Symbol('sat_y'), sympy.Symbol('sat_z') + weight = sympy.Symbol('weight') + + theta = EARTH_ROTATION_RATE * (pr - bc) / SPEED_OF_LIGHT + val = sympy.sqrt( + (sat_x * sympy.cos(theta) + sat_y * sympy.sin(theta) - x) ** 2 + + (sat_y * sympy.cos(theta) - sat_x * sympy.sin(theta) - y) ** 2 + + (sat_z - z) ** 2 + ) + + if constellation == ConstellationId.GLONASS: + res = weight * (val - (pr - bc - bg)) + elif constellation == ConstellationId.GPS: + res = weight * (val - (pr - bc)) + else: + raise NotImplementedError(f"Constellation {constellation} not supported") + + res = [res] + [sympy.diff(res, v) for v in var] + + return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) diff --git a/selfdrive/locationd/models/gnss_helpers.py b/selfdrive/locationd/models/gnss_helpers.py new file mode 100644 index 00000000000000..a3bcbc00028f83 --- /dev/null +++ b/selfdrive/locationd/models/gnss_helpers.py @@ -0,0 +1,21 @@ +import numpy as np + +def parse_prr(m): + from laika.raw_gnss import GNSSMeasurement + sat_pos_vel_i = np.concatenate((m[GNSSMeasurement.SAT_POS], + m[GNSSMeasurement.SAT_VEL])) + R_i = np.atleast_2d(m[GNSSMeasurement.PRR_STD]**2) + z_i = m[GNSSMeasurement.PRR] + return z_i, R_i, sat_pos_vel_i + + +def parse_pr(m): + from laika.raw_gnss import GNSSMeasurement + pseudorange = m[GNSSMeasurement.PR] + pseudorange_stdev = m[GNSSMeasurement.PR_STD] + sat_pos_freq_i = np.concatenate((m[GNSSMeasurement.SAT_POS], + np.array([m[GNSSMeasurement.GLONASS_FREQ]]))) + z_i = np.atleast_1d(pseudorange) + R_i = np.atleast_2d(pseudorange_stdev**2) + return z_i, R_i, sat_pos_freq_i + diff --git a/selfdrive/locationd/models/gnss_kf.py b/selfdrive/locationd/models/gnss_kf.py index 3f4baab7b575e4..ce0ff84dc2705a 100755 --- a/selfdrive/locationd/models/gnss_kf.py +++ b/selfdrive/locationd/models/gnss_kf.py @@ -7,7 +7,7 @@ from rednose.helpers.ekf_sym import EKF_sym, gen_code from selfdrive.locationd.models.constants import ObservationKind -from selfdrive.locationd.models.loc_kf import parse_pr, parse_prr +from selfdrive.locationd.models.gnss_helpers import parse_pr, parse_prr class States(): diff --git a/selfdrive/locationd/models/loc_kf.py b/selfdrive/locationd/models/loc_kf.py index 8391426dd1cd10..6b08828695a4eb 100755 --- a/selfdrive/locationd/models/loc_kf.py +++ b/selfdrive/locationd/models/loc_kf.py @@ -5,33 +5,14 @@ import numpy as np import sympy as sp -from selfdrive.locationd.models.constants import ObservationKind from rednose.helpers.ekf_sym import EKF_sym, gen_code from rednose.helpers.lst_sq_computer import LstSqComputer from rednose.helpers.sympy_helpers import euler_rotate, quat_matrix_r, quat_rotate -EARTH_GM = 3.986005e14 # m^3/s^2 (gravitational constant * mass of earth) - - -def parse_prr(m): - from laika.raw_gnss import GNSSMeasurement - sat_pos_vel_i = np.concatenate((m[GNSSMeasurement.SAT_POS], - m[GNSSMeasurement.SAT_VEL])) - R_i = np.atleast_2d(m[GNSSMeasurement.PRR_STD]**2) - z_i = m[GNSSMeasurement.PRR] - return z_i, R_i, sat_pos_vel_i - - -def parse_pr(m): - from laika.raw_gnss import GNSSMeasurement - pseudorange = m[GNSSMeasurement.PR] - pseudorange_stdev = m[GNSSMeasurement.PR_STD] - sat_pos_freq_i = np.concatenate((m[GNSSMeasurement.SAT_POS], - np.array([m[GNSSMeasurement.GLONASS_FREQ]]))) - z_i = np.atleast_1d(pseudorange) - R_i = np.atleast_2d(pseudorange_stdev**2) - return z_i, R_i, sat_pos_freq_i +from selfdrive.locationd.models.constants import ObservationKind +from selfdrive.locationd.models.gnss_helpers import parse_pr, parse_prr +EARTH_GM = 3.986005e14 # m^3/s^2 (gravitational constant * mass of earth) class States(): ECEF_POS = slice(0, 3) # x, y and z in ECEF in meters diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index b3efe7e2deebc6..a9a5c78a7f80b6 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -57,6 +57,7 @@ def logging(started, params, CP: car.CarParams) -> bool: # Experimental PythonProcess("rawgpsd", "selfdrive.sensord.rawgps.rawgpsd", enabled=os.path.isfile("/persist/comma/use-quectel-rawgps")), + PythonProcess("laikad", "selfdrive.locationd.laikad", enabled=os.path.isfile("/persist/comma/use-laikad")), ] managed_processes = {p.name: p for p in procs} From 750daa6374e0f739b51df3e0fe4747407f023a73 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 16 Jun 2022 17:45:38 +0200 Subject: [PATCH 103/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 3166178611989c..6faf34064b70ab 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 3166178611989ca555d7b254130375d1d8a55cc8 +Subproject commit 6faf34064b70ab98c241d4a1cd9006b09ecaadfc From a875afd5639d6e1ac24baa4de047cb8a2988a953 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 16 Jun 2022 18:18:03 +0200 Subject: [PATCH 104/436] navd: handle maxspeed being none (#24871) * navd: handle maxspeed being none * none is encoded like this --- selfdrive/navd/navd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/selfdrive/navd/navd.py b/selfdrive/navd/navd.py index 509653a46daaf6..f02de43c7bf9fb 100755 --- a/selfdrive/navd/navd.py +++ b/selfdrive/navd/navd.py @@ -143,8 +143,10 @@ def calculate_route(self, destination): coord = Coordinate.from_mapbox_tuple(c) # Last step does not have maxspeed - if (maxspeed_idx < len(maxspeeds)) and ('unknown' not in maxspeeds[maxspeed_idx]): - coord.annotations['maxspeed'] = maxspeed_to_ms(maxspeeds[maxspeed_idx]) + if (maxspeed_idx < len(maxspeeds)): + maxspeed = maxspeeds[maxspeed_idx] + if ('unknown' not in maxspeed) and ('none' not in maxspeed): + coord.annotations['maxspeed'] = maxspeed_to_ms(maxspeed) coords.append(coord) maxspeed_idx += 1 From d4886b2c2995db88d9bc9f7d7cf3f7cb9def5b12 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 16 Jun 2022 19:41:51 +0200 Subject: [PATCH 105/436] Cast gpstimeofweek to int --- selfdrive/locationd/laikad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 8ec570e2bec989..6fd70875acdd4e 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -201,7 +201,7 @@ def create_measurement_msg(meas: GNSSMeasurement): c.ephemerisSource.type = source_type.value c.ephemerisSource.gpsWeek = week - c.ephemerisSource.gpsTimeOfWeek = time_of_week + c.ephemerisSource.gpsTimeOfWeek = int(time_of_week) return c From b941b39c56db2d9d7f756cb0949ef6a7cf61774a Mon Sep 17 00:00:00 2001 From: grekiki <96022003+GregorKikelj@users.noreply.github.com> Date: Thu, 16 Jun 2022 19:47:53 +0200 Subject: [PATCH 106/436] More accurate jerk limits (#24755) * More accurate jerk limits * Min is not - max For example max_curvature_rate can be negative. * reduce diff --- selfdrive/controls/lib/drive_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 1fecdd7c638a1a..d79f94bbfdad82 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -122,7 +122,7 @@ def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): # This is the "desired rate of the setpoint" not an actual desired rate desired_curvature_rate = curvature_rates[0] - max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) + max_curvature_rate = MAX_LATERAL_JERK / (v_ego**2) # inexact calculation, check https://github.com/commaai/openpilot/pull/24755 safe_desired_curvature_rate = clip(desired_curvature_rate, -max_curvature_rate, max_curvature_rate) From 88a80983a45e043e2d0fca2bc70c53beca256afe Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 16 Jun 2022 17:50:51 -0700 Subject: [PATCH 107/436] Chrysler: interface cleanup (#24884) * Chrysler: interface cleanup * little more --- selfdrive/car/chrysler/interface.py | 50 ++++++++++++++++++----------- selfdrive/car/chrysler/values.py | 3 ++ 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 817a5b387ef4c9..8ebcb6b1266005 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -12,28 +12,38 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.carName = "chrysler" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.chrysler)] - # Speed conversion: 20, 45 mph - ret.wheelbase = 3.089 # in meters for Pacifica Hybrid 2017 - ret.steerRatio = 16.2 # Pacifica Hybrid 2017 - ret.mass = 2242. + STD_CARGO_KG # kg curb weight Pacifica Hybrid 2017 - ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]] - ret.lateralTuning.pid.kf = 0.00006 # full torque for 10 deg at 80mph means 0.00007818594 ret.steerActuatorDelay = 0.1 ret.steerLimitTimer = 0.4 - if candidate in (CAR.JEEP_CHEROKEE, CAR.JEEP_CHEROKEE_2019): - ret.wheelbase = 2.91 # in meters - ret.steerRatio = 12.7 - ret.steerActuatorDelay = 0.2 # in seconds - - ret.centerToFront = ret.wheelbase * 0.44 - ret.minSteerSpeed = 3.8 # m/s if candidate in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_CHEROKEE_2019): - # TODO allow 2019 cars to steer down to 13 m/s if already engaged. + # TODO: allow 2019 cars to steer down to 13 m/s if already engaged. ret.minSteerSpeed = 17.5 # m/s 17 on the way up, 13 on the way down once engaged. + # Chrysler + if candidate in (CAR.PACIFICA_2017_HYBRID, CAR.PACIFICA_2018, CAR.PACIFICA_2018_HYBRID, CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020): + ret.mass = 2242. + STD_CARGO_KG + ret.wheelbase = 3.089 + ret.steerRatio = 16.2 # Pacifica Hybrid 2017 + ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]] + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]] + ret.lateralTuning.pid.kf = 0.00006 + + # Jeep + elif candidate in (CAR.JEEP_CHEROKEE, CAR.JEEP_CHEROKEE_2019): + ret.mass = 1778 + STD_CARGO_KG + ret.wheelbase = 2.71 + ret.steerRatio = 16.7 + ret.steerActuatorDelay = 0.2 + ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[9., 20.], [9., 20.]] + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]] + ret.lateralTuning.pid.kf = 0.00006 + + else: + raise ValueError(f"Unsupported car: {candidate}") + + ret.centerToFront = ret.wheelbase * 0.44 + # starting with reasonable value for civic and scaling by mass and wheelbase ret.rotationalInertia = scale_rot_inertia(ret.mass, ret.wheelbase) @@ -45,7 +55,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa return ret - # returns a car.CarState def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) @@ -54,14 +63,17 @@ def _update(self, c): # events events = self.create_common_events(ret, extra_gears=[car.CarState.GearShifter.low]) - if ret.vEgo < self.CP.minSteerSpeed: + # Low speed steer alert hysteresis logic + if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 1.): + self.low_speed_alert = True + elif ret.vEgo > (self.CP.minSteerSpeed + 2.): + self.low_speed_alert = False + if self.low_speed_alert: events.add(car.CarEvent.EventName.belowSteerSpeed) ret.events = events.to_msg() return ret - # pass in a car.CarControl - # to be called @ 100hz def apply(self, c): return self.CC.update(c, self.CS) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index b2757aafc28b6a..a0d4225d9a8595 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -16,11 +16,14 @@ class CarControllerParams: class CAR: + # Chrysler PACIFICA_2017_HYBRID = "CHRYSLER PACIFICA HYBRID 2017" PACIFICA_2018_HYBRID = "CHRYSLER PACIFICA HYBRID 2018" PACIFICA_2019_HYBRID = "CHRYSLER PACIFICA HYBRID 2019" PACIFICA_2018 = "CHRYSLER PACIFICA 2018" # includes 2017 Pacifica PACIFICA_2020 = "CHRYSLER PACIFICA 2020" + + # Jeep JEEP_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk JEEP_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk From b85547ccf068d40f57dda467757301b069db0c80 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 16 Jun 2022 18:14:25 -0700 Subject: [PATCH 108/436] bump panda --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 42772b49e31368..6c1738814b451d 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 42772b49e313688f8f96114806a72784f978e990 +Subproject commit 6c1738814b451d23797ab7d8fddd43c3b5296b45 From 6c02e554e7174745c7605f4e8f0e3c1aea637091 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 16 Jun 2022 20:23:25 -0700 Subject: [PATCH 109/436] VW FPv2: reduce number of ECU queries (#24706) * only send valid/needed queries * just do volkswagen * clean up * add parameter name clean up * add test for whitelist * rename * Update selfdrive/car/fw_versions.py Co-authored-by: Jason Young <46612682+jyoung8607@users.noreply.github.com> * fix test * log response addresses * bump cereal * handle response pending with IsoTpParallelQuery * remove response pending stuff * temporarily disregard cache for easier testing * revert this Co-authored-by: Jason Young <46612682+jyoung8607@users.noreply.github.com> --- selfdrive/car/fw_versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index c7253d794dcb4b..81883cecaaaff3 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -146,12 +146,14 @@ class Request: "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], + whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], rx_offset=VOLKSWAGEN_RX_OFFSET, ), Request( "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], + whitelist_ecus=[Ecu.engine, Ecu.transmission], ), # Mazda Request( From 1b0167ce24afb037b36464c40f9c5e0d657e77d9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 16 Jun 2022 22:34:07 -0700 Subject: [PATCH 110/436] Chrysler: use universal gas and brake signals (#24886) * Chrysler_Update * Update signal names * bump panda * bump submodules * bump panda to master Co-authored-by: Jonathan --- opendbc | 2 +- panda | 2 +- selfdrive/car/chrysler/carstate.py | 15 +++++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/opendbc b/opendbc index a7b391cce88908..5e2a82026842a7 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit a7b391cce88908824f1417f0c7abd35e3ae16f96 +Subproject commit 5e2a82026842a7082e5e81e5823dab6b6616dbf4 diff --git a/panda b/panda index 6c1738814b451d..515df2bb727fe6 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 6c1738814b451d23797ab7d8fddd43c3b5296b45 +Subproject commit 515df2bb727fe616869e5d460817e8898792e44f diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 8ddf1c86847cb4..e3ee4753cc3e3d 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -24,9 +24,12 @@ def update(self, cp, cp_cam): cp.vl["DOORS"]["DOOR_OPEN_RR"]]) ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_UNLATCHED"] == 1 - ret.brakePressed = cp.vl["BRAKE_2"]["BRAKE_PRESSED_2"] == 5 # human-only + # brake pedal ret.brake = 0 - ret.gas = cp.vl["ACCEL_GAS_134"]["ACCEL_134"] + ret.brakePressed = cp.vl["ESP_1"]['Brake_Pedal_State'] == 1 # Physical brake pedal switch + + # gas pedal + ret.gas = cp.vl["ECM_5"]["Accelerator_Position"] ret.gasPressed = ret.gas > 1e-5 ret.espDisabled = (cp.vl["TRACTION_BUTTON"]["TRACTION_OFF"] == 1) @@ -83,8 +86,8 @@ def get_can_parser(CP): ("DOOR_OPEN_FR", "DOORS"), ("DOOR_OPEN_RL", "DOORS"), ("DOOR_OPEN_RR", "DOORS"), - ("BRAKE_PRESSED_2", "BRAKE_2"), - ("ACCEL_134", "ACCEL_GAS_134"), + ("Brake_Pedal_State", "ESP_1"), + ("Accelerator_Position", "ECM_5"), ("SPEED_LEFT", "SPEED_1"), ("SPEED_RIGHT", "SPEED_1"), ("WHEEL_SPEED_FL", "WHEEL_SPEEDS"), @@ -110,14 +113,14 @@ def get_can_parser(CP): checks = [ # sig_address, frequency - ("BRAKE_2", 50), + ("ESP_1", 50), ("EPS_STATUS", 100), ("SPEED_1", 100), ("WHEEL_SPEEDS", 50), ("STEERING", 100), ("ACC_2", 50), ("GEAR", 50), - ("ACCEL_GAS_134", 50), + ("ECM_5", 50), ("WHEEL_BUTTONS", 50), ("DASHBOARD", 15), ("STEERING_LEVERS", 10), From 5dce006a82879f133ab69355b6c9b8c74d2b4d33 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 16 Jun 2022 22:41:13 -0700 Subject: [PATCH 111/436] regen: send wideRoadCameraState (#24863) * fix camera malfunction * revert * send in one process to fix frames out of sync * not used * revert * fix and add --no-upload for CI testing * fingerprint if source is fw * no FW versions --- .../test/process_replay/process_replay.py | 3 +-- selfdrive/test/process_replay/regen.py | 22 ++++++++++--------- selfdrive/test/process_replay/regen_all.py | 9 ++++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index b016eb1c9ff5d5..c368e6b575917b 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -15,7 +15,6 @@ from common.params import Params from common.timeout import Timeout from panda.python import ALTERNATIVE_EXPERIENCE -from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.car.car_helpers import get_car, interfaces from selfdrive.test.process_replay.helpers import OpenpilotPrefix from selfdrive.manager.process import PythonProcess @@ -372,7 +371,7 @@ def setup_env(simulation=False, CP=None): if CP.alternativeExperience == ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS: params.put_bool("DisengageOnAccelerator", False) - if CP.fingerprintSource == "fw" and CP.carFingerprint in FW_VERSIONS: + if CP.fingerprintSource == "fw": params.put("CarParamsCache", CP.as_builder().to_bytes()) else: os.environ['SKIP_FW_QUERY'] = "1" diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index dfc62a4558e0b9..d2f239c249b0b1 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -127,7 +127,10 @@ def replay_cameras(lr, frs, disable_tqdm=False): ] def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): - pm = messaging.PubMaster([s, ]) + services = [(s, stream)] + if use_extra_client: + services.append(("wideRoadCameraState", VisionStreamType.VISION_STREAM_WIDE_ROAD)) + pm = messaging.PubMaster([s for s, _ in services]) rk = Ratekeeper(1 / dt, print_delay_threshold=None) img = b"\x00" * int(size[0] * size[1] * 3 / 2) @@ -137,16 +140,15 @@ def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): rk.keep_time() - m = messaging.new_message(s) - msg = getattr(m, s) - msg.frameId = rk.frame - msg.timestampSof = m.logMonoTime - msg.timestampEof = m.logMonoTime - pm.send(s, m) + for s, stream in services: + m = messaging.new_message(s) + msg = getattr(m, s) + msg.frameId = rk.frame + msg.timestampSof = m.logMonoTime + msg.timestampEof = m.logMonoTime + pm.send(s, m) - vipc_server.send(stream, img, msg.frameId, msg.timestampSof, msg.timestampEof) - if use_extra_client: - vipc_server.send(VisionStreamType.VISION_STREAM_WIDE_ROAD, img, msg.frameId, msg.timestampSof, msg.timestampEof) + vipc_server.send(stream, img, msg.frameId, msg.timestampSof, msg.timestampEof) init_data = [m for m in lr if m.which() == 'initData'][0] cameras = tici_cameras if (init_data.initData.deviceType == 'tici') else eon_cameras diff --git a/selfdrive/test/process_replay/regen_all.py b/selfdrive/test/process_replay/regen_all.py index c3ea1d41e1a8b9..f10d7ea03a6ced 100755 --- a/selfdrive/test/process_replay/regen_all.py +++ b/selfdrive/test/process_replay/regen_all.py @@ -11,12 +11,12 @@ from tools.lib.route import SegmentName -def regen_job(segment, disable_tqdm): +def regen_job(segment, upload, disable_tqdm): with OpenpilotPrefix(): sn = SegmentName(segment[1]) fake_dongle_id = 'regen' + ''.join(random.choice('0123456789ABCDEF') for _ in range(11)) try: - relr = regen_and_save(sn.route_name.canonical_name, sn.segment_num, upload=True, use_route_meta=False, outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm) + relr = regen_and_save(sn.route_name.canonical_name, sn.segment_num, upload=upload, use_route_meta=False, outdir=os.path.join(FAKEDATA, fake_dongle_id), disable_tqdm=disable_tqdm) relr = '|'.join(relr.split('/')[-2:]) return f' ("{segment[0]}", "{relr}"), ' except Exception as e: @@ -26,12 +26,13 @@ def regen_job(segment, disable_tqdm): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Generate new segments from old ones") parser.add_argument("-j", "--jobs", type=int, default=1) + parser.add_argument("--no-upload", action="store_true") args = parser.parse_args() with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: - p = list(pool.map(regen_job, segments, [args.jobs > 1] * args.jobs)) + p = pool.map(regen_job, segments, [not args.no_upload] * len(segments), [args.jobs > 1] * len(segments)) msg = "Copy these new segments into test_processes.py:" - for seg in tqdm(p, desc="Generating segments"): + for seg in tqdm(p, desc="Generating segments", total=len(segments)): msg += "\n" + str(seg) print() print() From cd87772e036acde3b7b74857f8c03e87f3af63c1 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 17 Jun 2022 11:43:40 +0200 Subject: [PATCH 112/436] live_cpu_and_temp: handle duplicate proc names --- selfdrive/debug/live_cpu_and_temp.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/selfdrive/debug/live_cpu_and_temp.py b/selfdrive/debug/live_cpu_and_temp.py index baeebb1c4ca71c..c35afc474bea3a 100755 --- a/selfdrive/debug/live_cpu_and_temp.py +++ b/selfdrive/debug/live_cpu_and_temp.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import argparse import capnp +from collections import defaultdict from cereal.messaging import SubMaster from common.numpy_fast import mean -from typing import Optional +from typing import Optional, Dict def cputime_total(ct): return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq @@ -75,7 +76,7 @@ def proc_name(proc): print(f"CPU {100.0 * mean(cores):.2f}% - RAM: {last_mem:.2f}% - Temp {last_temp:.2f}C") if args.cpu and prev_proclog is not None and prev_proclog_t is not None: - procs = {} + procs: Dict[str, float] = defaultdict(float) dt = (sm.logMonoTime['procLog'] - prev_proclog_t) / 1e9 for proc in m.procs: try: @@ -83,12 +84,12 @@ def proc_name(proc): prev_proc = [p for p in prev_proclog.procs if proc.pid == p.pid][0] cpu_time = proc_cputime_total(proc) - proc_cputime_total(prev_proc) cpu_usage = cpu_time / dt * 100. - procs[name] = cpu_usage + procs[name] += cpu_usage except IndexError: pass print("Top CPU usage:") - for k, v in sorted(procs.items(), key=lambda item: item[1], reverse=True)[:10]: # type: ignore + for k, v in sorted(procs.items(), key=lambda item: item[1], reverse=True)[:10]: print(f"{k.rjust(70)} {v:.2f} %") print() From 7c826b4fa1ff5d4a22da4a9643ecef803c0b7536 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 17 Jun 2022 02:56:04 -0700 Subject: [PATCH 113/436] add param to override carParams.dashcamOnly (#24857) * add param to override carParams.dashcamOnly * little cleaner --- common/params.cc | 1 + selfdrive/car/hyundai/interface.py | 5 +---- selfdrive/controls/controlsd.py | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/params.cc b/common/params.cc index 8cf1baa6c81512..b8526bc231ac22 100644 --- a/common/params.cc +++ b/common/params.cc @@ -94,6 +94,7 @@ std::unordered_map keys = { {"CompletedTrainingVersion", PERSISTENT}, {"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, + {"DashcamOverride", PERSISTENT}, {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON}, {"DisablePowerDown", PERSISTENT}, {"DisableRadar_Allow", PERSISTENT}, diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 497d6b3f307a5e..2739ebd8cc47f7 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import os from cereal import car from panda import Panda from common.conversions import Conversions as CV @@ -38,9 +37,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl # These cars have been put into dashcam only due to both a lack of users and test coverage. # These cars likely still work fine. Once a user confirms each car works and a test route is # added to selfdrive/car/tests/routes.py, we can remove it from this list. - ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} - if candidate in HDA2_CAR: - ret.dashcamOnly = not os.path.exists('/data/enable-ev6') + ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} or candidate in HDA2_CAR ret.steerActuatorDelay = 0.1 # Default delay ret.steerLimitTimer = 0.4 diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 033072aa731e2e..67eaf1588bcdec 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -104,6 +104,9 @@ def __init__(self, sm=None, pm=None, can_sock=None, CI=None): if not self.disengage_on_accelerator: self.CP.alternativeExperience |= ALTERNATIVE_EXPERIENCE.DISABLE_DISENGAGE_ON_GAS + if self.CP.dashcamOnly and params.get_bool("DashcamOverride"): + self.CP.dashcamOnly = False + # read params self.is_metric = params.get_bool("IsMetric") self.is_ldw_enabled = params.get_bool("IsLdwEnabled") From b701ea33ce0f36b78c703429afeb5d3d94d38105 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 17 Jun 2022 16:27:05 +0200 Subject: [PATCH 114/436] replay: no HW decoder, fix UV stride --- selfdrive/ui/replay/framereader.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/replay/framereader.cc b/selfdrive/ui/replay/framereader.cc index 8453407ce77f8c..38d98bddc56209 100644 --- a/selfdrive/ui/replay/framereader.cc +++ b/selfdrive/ui/replay/framereader.cc @@ -242,7 +242,7 @@ bool FrameReader::copyBuffers(AVFrame *f, uint8_t *yuv) { f->data[1], f->linesize[1], f->data[2], f->linesize[2], y, width, - uv, width / 2, + uv, width, width, height); } return true; From 2c877ce4907657d59111d5e850afb587b3be8d6e Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Fri, 17 Jun 2022 10:35:24 -0700 Subject: [PATCH 115/436] Long e2e planner: better xva weights (#24893) better long xva weights --- selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py index bc0fc9fea6c04b..94efd7a87545d7 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @@ -251,7 +251,7 @@ def set_weights_for_lead_policy(self, prev_accel_constraint=True): self.solver.cost_set(i, 'Zl', Zl) def set_weights_for_xva_policy(self): - W = np.asfortranarray(np.diag([0., 10., 1., 10., 0.0, 1.])) + W = np.asfortranarray(np.diag([0., 0.2, 0.25, 1., 0.0, .1])) for i in range(N): self.solver.cost_set(i, 'W', W) # Setting the slice without the copy make the array not contiguous, From 33eaebfab2387ed92ef3fc69e3c610f27ee3db67 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 17 Jun 2022 19:47:37 +0200 Subject: [PATCH 116/436] ui: show empty box when speed limit is unavailable during nav (#24891) * ui: show empty box when speed limit is unavailable during nav * variant 2, long - --- selfdrive/ui/qt/onroad.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 38541ef2d5088b..84b538411bd3fb 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -186,8 +186,8 @@ void NvgWindow::updateState(const UIState &s) { speed_limit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); setProperty("speedLimit", speed_limit); - setProperty("has_us_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); - setProperty("has_eu_speed_limit", speed_limit > 1 && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); + setProperty("has_us_speed_limit", sm["navInstruction"].getValid() && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); + setProperty("has_eu_speed_limit", sm["navInstruction"].getValid() && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); setProperty("is_cruise_set", cruise_set); setProperty("speed", cur_speed); @@ -212,7 +212,7 @@ void NvgWindow::drawHud(QPainter &p) { bg.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0)); p.fillRect(0, 0, width(), header_h, bg); - QString speedLimitStr = QString::number(std::nearbyint(speedLimit)); + QString speedLimitStr = (speedLimit > 1) ? QString::number(std::nearbyint(speedLimit)) : "–"; QString speedStr = QString::number(std::nearbyint(speed)); QString setSpeedStr = is_cruise_set ? QString::number(std::nearbyint(setSpeed)) : "–"; @@ -334,6 +334,8 @@ void NvgWindow::drawHud(QPainter &p) { p.drawEllipse(center, inner_radius_2, inner_radius_2); // Speed limit value + if (speedLimit < 1 ) center -= {0, 2}; // Make sure dash is centered if no speed limit available + int font_size = (speedLimitStr.size() >= 3) ? 62 : 70; configFont(p, "Open Sans", font_size, "Bold"); QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); From 208d4a4fc73cf3f41111de67a168b4ef83156e6d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 17 Jun 2022 13:39:15 -0700 Subject: [PATCH 117/436] Add Hyundai P harness (#24872) * Update docs_definitions.py * Update values.py --- selfdrive/car/docs_definitions.py | 1 + selfdrive/car/hyundai/values.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index f09dad78601093..e1344174abb565 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -144,6 +144,7 @@ class Harness(Enum): hyundai_m = "Hyundai M" hyundai_n = "Hyundai N" hyundai_o = "Hyundai O" + hyundai_p = "Hyundai P" custom = "Developer" obd_ii = "OBD-II" nissan_a = "Nissan A" diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index df7ce1e6f4d209..cf47501588ee67 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -146,7 +146,7 @@ class HyundaiCarInfo(CarInfo): ], CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), - CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.none), + CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.hyundai_p), # Genesis CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018", "All", harness=Harness.hyundai_f), From 0f0b4cac893b41dd81288947158d3aa9cbbfc2fd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 18 Jun 2022 17:15:45 -0700 Subject: [PATCH 118/436] Chrysler: use unified signal definitions (#24895) * Update some signals to unified names and definitions Co-authored-by: Jonathan * steering looks good * Fix cp signals * Do steering signal changes separately * bump opendbc to master Co-authored-by: Jonathan --- opendbc | 2 +- selfdrive/car/chrysler/carstate.py | 31 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/opendbc b/opendbc index 5e2a82026842a7..57c8340a180dd8 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 5e2a82026842a7082e5e81e5823dab6b6616dbf4 +Subproject commit 57c8340a180dd8c75139b18050eb17c72c9cb6e4 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index e3ee4753cc3e3d..7571286c56a5e1 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -18,10 +18,10 @@ def update(self, cp, cp_cam): self.frame = int(cp.vl["EPS_STATUS"]["COUNTER"]) - ret.doorOpen = any([cp.vl["DOORS"]["DOOR_OPEN_FL"], - cp.vl["DOORS"]["DOOR_OPEN_FR"], - cp.vl["DOORS"]["DOOR_OPEN_RL"], - cp.vl["DOORS"]["DOOR_OPEN_RR"]]) + ret.doorOpen = any([cp.vl["BCM_1"]["Driver_Door_Ajar"], + cp.vl["BCM_1"]["Passenger_Door_Ajar"], + cp.vl["BCM_1"]["Left_Rear_Door_Ajar"], + cp.vl["BCM_1"]["Right_Rear_Door_Ajar"]]) ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_UNLATCHED"] == 1 # brake pedal @@ -51,12 +51,12 @@ def update(self, cp, cp_cam): ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) - ret.cruiseState.enabled = cp.vl["ACC_2"]["ACC_STATUS_2"] == 7 # ACC is green. - ret.cruiseState.available = ret.cruiseState.enabled # FIXME: for now same as enabled + ret.cruiseState.available = cp.vl["DAS_3"]["ACC_Engaged"] == 1 # ACC is white + ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_Enabled"] == 1 # ACC is green ret.cruiseState.speed = cp.vl["DASHBOARD"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too ret.cruiseState.nonAdaptive = cp.vl["DASHBOARD"]["CRUISE_STATE"] in (1, 2) - ret.accFaulted = cp.vl["ACC_2"]["ACC_FAULTED"] != 0 + ret.accFaulted = cp.vl["DAS_3"]["ACC_Faulted"] != 0 ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] @@ -82,10 +82,10 @@ def get_can_parser(CP): signals = [ # sig_name, sig_address ("PRNDL", "GEAR"), - ("DOOR_OPEN_FL", "DOORS"), - ("DOOR_OPEN_FR", "DOORS"), - ("DOOR_OPEN_RL", "DOORS"), - ("DOOR_OPEN_RR", "DOORS"), + ("Driver_Door_Ajar", "BCM_1"), + ("Passenger_Door_Ajar", "BCM_1"), + ("Left_Rear_Door_Ajar", "BCM_1"), + ("Right_Rear_Door_Ajar", "BCM_1"), ("Brake_Pedal_State", "ESP_1"), ("Accelerator_Position", "ECM_5"), ("SPEED_LEFT", "SPEED_1"), @@ -97,8 +97,9 @@ def get_can_parser(CP): ("STEER_ANGLE", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), - ("ACC_STATUS_2", "ACC_2"), - ("ACC_FAULTED", "ACC_2"), + ("ACC_Enabled", "DAS_3"), + ("ACC_Engaged", "DAS_3"), + ("ACC_Faulted", "DAS_3"), ("HIGH_BEAM_FLASH", "STEERING_LEVERS"), ("ACC_SPEED_CONFIG_KPH", "DASHBOARD"), ("CRUISE_STATE", "DASHBOARD"), @@ -118,14 +119,14 @@ def get_can_parser(CP): ("SPEED_1", 100), ("WHEEL_SPEEDS", 50), ("STEERING", 100), - ("ACC_2", 50), + ("DAS_3", 50), ("GEAR", 50), ("ECM_5", 50), ("WHEEL_BUTTONS", 50), ("DASHBOARD", 15), ("STEERING_LEVERS", 10), ("SEATBELT_STATUS", 2), - ("DOORS", 1), + ("BCM_1", 1), ("TRACTION_BUTTON", 1), ] From add335d9e67f24a3090e09d39d4267b8f5620ff1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jun 2022 19:01:42 -0700 Subject: [PATCH 119/436] jenkins: move simulator build into lock --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index ebc26a5920c3cf..279716d7cf8407 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -88,8 +88,8 @@ pipeline { steps { sh "git config --global --add safe.directory ${WORKSPACE}" sh "git lfs pull" - sh "${WORKSPACE}/tools/sim/build_container.sh" lock(resource: "", label: "simulator", inversePrecedence: true, quantity: 1) { + sh "${WORKSPACE}/tools/sim/build_container.sh" sh "DETACH=1 ${WORKSPACE}/tools/sim/start_carla.sh" sh "${WORKSPACE}/tools/sim/start_openpilot_docker.sh" } From a27d242e3d35cb76761b0cd14eb0d58e668cfbb9 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 18 Jun 2022 19:02:09 -0700 Subject: [PATCH 120/436] zookeeper: add avg power to power_monitor.py --- tools/zookeeper/power_monitor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/zookeeper/power_monitor.py b/tools/zookeeper/power_monitor.py index d3bdd6679f3975..f88741813eab65 100755 --- a/tools/zookeeper/power_monitor.py +++ b/tools/zookeeper/power_monitor.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys import time +import datetime from common.realtime import Ratekeeper from common.filter_simple import FirstOrderFilter @@ -19,12 +20,19 @@ rk = Ratekeeper(rate, print_delay_threshold=None) fltr = FirstOrderFilter(0, 5, 1. / rate, initialized=False) + measurements = [] + start_time = time.monotonic() + try: - start_time = time.monotonic() while duration is None or time.monotonic() - start_time < duration: fltr.update(z.read_power()) if rk.frame % rate == 0: print(f"{fltr.x:.2f} W") + measurements.append(fltr.x) rk.keep_time() except KeyboardInterrupt: pass + + t = datetime.timedelta(seconds=time.monotonic() - start_time) + avg = sum(measurements) / len(measurements) + print(f"\nAverage power: {avg:.2f}W over {t}") From abcc7338d41359e7e4f4d2d37d33449c292224ed Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sun, 19 Jun 2022 00:06:23 -0700 Subject: [PATCH 121/436] Car Port: 2022 Honda Civic (#24535) * master 2022 Civic * bump panda * bump * bump cereal * fix * needed * try for now * maybe * revert for now * move to Cam parser * fix * move to cam * need AEB_STATUS too * bump for debug prints * bump opendbc and add cruise_params * bump opendbc and update cruise_params * bump * test route * update ref * Revert "update ref" This reverts commit 28345dab63d1919865ccb510265222a4cd4252f4. * cleanup * just to test * bump * revert * need to send val 12 too? * change bus * not needed * update bus * syntax * move this to other bus too * Revert "move this to other bus too" This reverts commit 770bf4745ee244c8426ac108f44b67777198d0a7. * test new lane line signal * needed too * maybe need both? * Test new LKAS hud message * bump * missing comma * missing * maybe * add frame and idx * add in hud_lanes * switch this too * bump panda * add this * I guess need this too * to match * also * wasnt correct * bump opendbc * bump panda * move to cam parser * missing * add here too * bump * remove from cam parser * bump * back to cam parser * its 5hz * bump for new checksum function * bump for correct frequency * update frame and idx * bump * bump and update * send set me bit * bump * pass these values through * silly atom * ret * fix this * use copy instead * add these too * to check keyerror * switch * bump submodules * send too * proper * Replace HUD with BOH * add dashed lanes * small fix * clean up * not needed anymore * remove and change * this too * dont always set * remove additional LKAS message * bump * add * to test * add frame * bump * rebase * remove default values * rename * clean up some carstate logic * regenerate docs * spacing * simplify more logic * bump opendbc * bump opendbc * only if radarless * panda at least builds now * add comment * bump * fixes * bump opendbc * bump opendbc fix for new DBC * bump opendbc * bump opendbc * carstate: fix bus, parser signals * Set safety param * pt bus is 0, not 1 * Fix SCM_BUTTONS and bump panda and opendbc * fixes for ACC_CONTROL * bump opendbc * bump opendbc * convert from MPH on HONDA_BOSCH_RADARLESS move is_metric * make sure we don't disable if radarless * don't show incorrect harness on website don't show incorrect harness on website * bump panda * remove/update comments * bump panda * Fix harnesses * one line check * bump opendbc * remove this * Some carstate cleanup We removed STANDSTILL->WHEELS_MOVING we don't use CRUISE_PARAMS add back add back * more cleanup * update docs * marketing says it has TJA and ACC with low speed follow * send buttons on bus 0 bump panda * comment * camera needs to see buttons on bus 2 comment * bump panda * add to releases * remove comments * comment * we don't use stock hud yet Co-authored-by: vanillagorillaa Co-authored-by: vanillagorillaa <31773928+vanillagorillaa@users.noreply.github.com> Co-authored-by: kevinharbin <76784413+kevinharbin@users.noreply.github.com> --- RELEASES.md | 1 + docs/CARS.md | 3 +- panda | 2 +- release/files_common | 1 + selfdrive/car/docs_definitions.py | 3 +- selfdrive/car/honda/carcontroller.py | 6 +-- selfdrive/car/honda/carstate.py | 56 ++++++++++++++------------ selfdrive/car/honda/hondacan.py | 18 +++++++-- selfdrive/car/honda/interface.py | 16 +++++--- selfdrive/car/honda/values.py | 60 +++++++++++++++++++++++----- selfdrive/car/tests/routes.py | 1 + 11 files changed, 115 insertions(+), 52 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 7f365e5cef8284..5d81b4308579eb 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ Version 0.8.15 (2022-XX-XX) * Display speed limit while navigating * Reduced power usage: device runs cooler and fan spins less * AGNOS 5 +* Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! diff --git a/docs/CARS.md b/docs/CARS.md index c0bd62eb3f97f7..50db34dbb560ea 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -149,7 +149,7 @@ How We Rate The Cars |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 69 cars +# Bronze - 70 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -168,6 +168,7 @@ How We Rate The Cars |Honda|Accord Hybrid 2018-21|All|||||| |Honda|Civic 2016-18|Honda Sensing|||||| |Honda|Civic 2019-20|All|||[2](#footnotes)||| +|Honda|Civic 2022|All|||||| |Honda|Civic Hatchback 2017-21|Honda Sensing|||||| |Honda|CR-V 2015-16|Touring|||||| |Honda|CR-V 2017-21|Honda Sensing|||||| diff --git a/panda b/panda index 515df2bb727fe6..e1b2f1253cb7f0 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 515df2bb727fe616869e5d460817e8898792e44f +Subproject commit e1b2f1253cb7f05f39d4afa21500565bb8b955d2 diff --git a/release/files_common b/release/files_common index b6cd4fa797ff4a..e84c6687323a3f 100644 --- a/release/files_common +++ b/release/files_common @@ -495,6 +495,7 @@ opendbc/honda_odyssey_exl_2018_generated.dbc opendbc/honda_odyssey_extreme_edition_2018_china_can_generated.dbc opendbc/honda_insight_ex_2019_can_generated.dbc opendbc/acura_ilx_2016_nidec.dbc +opendbc/honda_civic_ex_2022_can_generated.dbc opendbc/kia_ev6.dbc opendbc/hyundai_kia_generic.dbc diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index e1344174abb565..618c986d185730 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -123,7 +123,8 @@ def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: class Harness(Enum): nidec = "Honda Nidec" - bosch = "Honda Bosch A" + bosch_a = "Honda Bosch A" + bosch_b = "Honda Bosch B" toyota = "Toyota" subaru = "Subaru" fca = "FCA" diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index a6aa84adf6a6d4..4d04927b2fc6aa 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -7,7 +7,7 @@ from opendbc.can.packer import CANPacker from selfdrive.car import create_gas_interceptor_command from selfdrive.car.honda import hondacan -from selfdrive.car.honda.values import CruiseButtons, VISUAL_HUD, HONDA_BOSCH, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams +from selfdrive.car.honda.values import CruiseButtons, VISUAL_HUD, HONDA_BOSCH, HONDA_BOSCH_RADARLESS, HONDA_NIDEC_ALT_PCM_ACCEL, CarControllerParams from selfdrive.controls.lib.drive_helpers import rate_limit VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -189,7 +189,7 @@ def update(self, CC, CS): pcm_accel = int(clip((accel / 1.44) / max_accel, 0.0, 1.0) * 0xc6) if not self.CP.openpilotLongitudinalControl: - if self.frame % 2 == 0: + if self.frame % 2 == 0 and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS: # radarless cars don't have supplemental message idx = self.frame // 2 can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CP.carFingerprint, idx)) # If using stock ACC, spam cancel command to kill gas when OP disengages. @@ -241,7 +241,7 @@ def update(self, CC, CS): idx = (self.frame // 10) % 4 hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_control.leadVisible, hud_control.lanesVisible, fcw_display, acc_alert, steer_required) - can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud)) + can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud, self.frame)) if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH: self.speed = pcm_speed diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index f5cdc838c4cc8c..5358ce249a1fb1 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -5,8 +5,9 @@ from common.numpy_fast import interp from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser +from selfdrive.car.honda.hondacan import get_pt_bus +from selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL, HONDA_BOSCH_RADARLESS from selfdrive.car.interfaces import CarStateBase -from selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL TransmissionType = car.CarParams.TransmissionType @@ -80,7 +81,8 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): checks.append(("EPB_STATUS", 50)) if CP.carFingerprint in HONDA_BOSCH: - if not CP.openpilotLongitudinalControl: + # these messages are on camera bus on radarless cars + if not CP.openpilotLongitudinalControl and CP.carFingerprint not in HONDA_BOSCH_RADARLESS: signals += [ ("CRUISE_CONTROL_LABEL", "ACC_HUD"), ("CRUISE_SPEED", "ACC_HUD"), @@ -100,23 +102,16 @@ def get_can_signals(CP, gearbox_msg, main_on_sig_msg): else: checks.append(("CRUISE_PARAMS", 50)) - if CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E): + if CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022): signals.append(("DRIVERS_DOOR_OPEN", "SCM_FEEDBACK")) - elif CP.carFingerprint == CAR.ODYSSEY_CHN: + elif CP.carFingerprint in (CAR.ODYSSEY_CHN, CAR.FREED, CAR.HRV): signals.append(("DRIVERS_DOOR_OPEN", "SCM_BUTTONS")) - elif CP.carFingerprint in (CAR.FREED, CAR.HRV): - signals += [("DRIVERS_DOOR_OPEN", "SCM_BUTTONS"), - ("WHEELS_MOVING", "STANDSTILL")] else: signals += [("DOOR_OPEN_FL", "DOORS_STATUS"), ("DOOR_OPEN_FR", "DOORS_STATUS"), ("DOOR_OPEN_RL", "DOORS_STATUS"), - ("DOOR_OPEN_RR", "DOORS_STATUS"), - ("WHEELS_MOVING", "STANDSTILL")] - checks += [ - ("DOORS_STATUS", 3), - ("STANDSTILL", 50), - ] + ("DOOR_OPEN_RR", "DOORS_STATUS")] + checks.append(("DOORS_STATUS", 3)) # add gas interceptor reading if we are using it if CP.enableGasInterceptor: @@ -175,7 +170,8 @@ def update(self, cp, cp_cam, cp_body): # STANDSTILL->WHEELS_MOVING bit can be noisy around zero, so use XMISSION_SPEED # panda checks if the signal is non-zero ret.standstill = cp.vl["ENGINE_DATA"]["XMISSION_SPEED"] < 1e-5 - if self.CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E): + # TODO: find a common signal across all cars + if self.CP.carFingerprint in (CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022): ret.doorOpen = bool(cp.vl["SCM_FEEDBACK"]["DRIVERS_DOOR_OPEN"]) elif self.CP.carFingerprint in (CAR.ODYSSEY_CHN, CAR.FREED, CAR.HRV): ret.doorOpen = bool(cp.vl["SCM_BUTTONS"]["DRIVERS_DOOR_OPEN"]) @@ -235,11 +231,15 @@ def update(self, cp, cp_cam, cp_body): if self.CP.carFingerprint in HONDA_BOSCH: if not self.CP.openpilotLongitudinalControl: - ret.cruiseState.nonAdaptive = cp.vl["ACC_HUD"]["CRUISE_CONTROL_LABEL"] != 0 - ret.cruiseState.standstill = cp.vl["ACC_HUD"]["CRUISE_SPEED"] == 252. + # ACC_HUD is on camera bus on radarless cars + acc_hud = cp_cam.vl["ACC_HUD"] if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS else cp.vl["ACC_HUD"] + ret.cruiseState.nonAdaptive = acc_hud["CRUISE_CONTROL_LABEL"] != 0 + ret.cruiseState.standstill = acc_hud["CRUISE_SPEED"] == 252. + # on certain cars, CRUISE_SPEED changes to imperial with car's unit setting + conversion_factor = CV.MPH_TO_MS if self.CP.carFingerprint in HONDA_BOSCH_RADARLESS and not self.is_metric else CV.KPH_TO_MS # On set, cruise set speed pulses between 254~255 and the set speed prev is set to avoid this. - ret.cruiseState.speed = self.v_cruise_pcm_prev if cp.vl["ACC_HUD"]["CRUISE_SPEED"] > 160.0 else cp.vl["ACC_HUD"]["CRUISE_SPEED"] * CV.KPH_TO_MS + ret.cruiseState.speed = self.v_cruise_pcm_prev if acc_hud["CRUISE_SPEED"] > 160.0 else acc_hud["CRUISE_SPEED"] * conversion_factor self.v_cruise_pcm_prev = ret.cruiseState.speed else: ret.cruiseState.speed = cp.vl["CRUISE"]["CRUISE_SPEED_PCM"] * CV.KPH_TO_MS @@ -269,14 +269,14 @@ def update(self, cp, cp_cam, cp_body): ret.brakePressed = True if self.CP.carFingerprint in HONDA_BOSCH: - ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5) + # TODO: find the radarless AEB_STATUS bit and make sure ACCEL_COMMAND is correct to enable AEB alerts + if self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS: + ret.stockAeb = (not self.CP.openpilotLongitudinalControl) and bool(cp.vl["ACC_CONTROL"]["AEB_STATUS"] and cp.vl["ACC_CONTROL"]["ACCEL_COMMAND"] < -1e-5) else: ret.stockAeb = bool(cp_cam.vl["BRAKE_COMMAND"]["AEB_REQ_1"] and cp_cam.vl["BRAKE_COMMAND"]["COMPUTER_BRAKE"] > 1e-5) - if self.CP.carFingerprint in HONDA_BOSCH: - self.stock_hud = False - ret.stockFcw = False - else: + self.stock_hud = False + if self.CP.carFingerprint not in HONDA_BOSCH: ret.stockFcw = cp_cam.vl["BRAKE_COMMAND"]["FCW"] != 0 self.stock_hud = cp_cam.vl["ACC_HUD"] self.stock_brake = cp_cam.vl["BRAKE_COMMAND"] @@ -291,8 +291,7 @@ def update(self, cp, cp_cam, cp_body): def get_can_parser(self, CP): signals, checks = get_can_signals(CP, self.gearbox_msg, self.main_on_sig_msg) - bus_pt = 1 if CP.carFingerprint in HONDA_BOSCH else 0 - return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, bus_pt) + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, get_pt_bus(CP.carFingerprint)) @staticmethod def get_cam_can_parser(CP): @@ -301,7 +300,14 @@ def get_cam_can_parser(CP): ("STEERING_CONTROL", 100), ] - if CP.carFingerprint not in HONDA_BOSCH: + if CP.carFingerprint in HONDA_BOSCH_RADARLESS and not CP.openpilotLongitudinalControl: + signals += [ + ("CRUISE_SPEED", "ACC_HUD"), + ("CRUISE_CONTROL_LABEL", "ACC_HUD"), + ] + checks.append(("ACC_HUD", 10)) + + elif CP.carFingerprint not in HONDA_BOSCH: signals += [("COMPUTER_BRAKE", "BRAKE_COMMAND"), ("AEB_REQ_1", "BRAKE_COMMAND"), ("FCW", "BRAKE_COMMAND"), diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 3d8c79c80911e9..6a1fb2e4597011 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -1,5 +1,5 @@ from common.conversions import Conversions as CV -from selfdrive.car.honda.values import HondaFlags, HONDA_BOSCH, CAR, CarControllerParams +from selfdrive.car.honda.values import HondaFlags, HONDA_BOSCH, HONDA_BOSCH_RADARLESS, CAR, CarControllerParams # CAN bus layout with relay # 0 = ACC-CAN - radar side @@ -7,8 +7,9 @@ # 2 = ACC-CAN - camera side # 3 = F-CAN A - OBDII port + def get_pt_bus(car_fingerprint): - return 1 if car_fingerprint in HONDA_BOSCH else 0 + return 1 if car_fingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) else 0 def get_lkas_cmd_bus(car_fingerprint, radar_disabled=False): @@ -18,6 +19,7 @@ def get_lkas_cmd_bus(car_fingerprint, radar_disabled=False): # normally steering commands are sent to radar, which forwards them to powertrain bus return 0 + def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, idx, car_fingerprint, stock_brake): # TODO: do we loose pressure if we keep pump off for long? brakelights = apply_brake > 0 @@ -78,6 +80,7 @@ def create_acc_commands(packer, enabled, active, accel, gas, idx, stopping, car_ return commands + def create_steering_control(packer, apply_steer, lkas_active, car_fingerprint, idx, radar_disabled): values = { "STEER_TORQUE": apply_steer if lkas_active else 0, @@ -98,7 +101,7 @@ def create_bosch_supplemental_1(packer, car_fingerprint, idx): return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values, idx) -def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud): +def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud, frame): commands = [] bus_pt = get_pt_bus(CP.carFingerprint) radar_disabled = CP.carFingerprint in HONDA_BOSCH and CP.openpilotLongitudinalControl @@ -135,6 +138,12 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc 'BEEP': 0, } + if CP.carFingerprint in HONDA_BOSCH_RADARLESS: + lkas_hud_values['LANE_LINES'] = 3 + lkas_hud_values['DASHED_LANES'] = hud.lanes_visible + # TODO: understand this better, does car need to see it fall after start up? + lkas_hud_values['LKAS_PROBLEM'] = 0 if frame > 200 else 1 + if not (CP.flags & HondaFlags.BOSCH_EXT_HUD): lkas_hud_values['SET_ME_X48'] = 0x48 @@ -162,5 +171,6 @@ def spam_buttons_command(packer, button_val, idx, car_fingerprint): 'CRUISE_BUTTONS': button_val, 'CRUISE_SETTING': 0, } - bus = get_pt_bus(car_fingerprint) + # send buttons to camera on radarless cars + bus = 2 if car_fingerprint in HONDA_BOSCH_RADARLESS else get_pt_bus(car_fingerprint) return packer.make_can_msg("SCM_BUTTONS", bus, values, idx) diff --git a/selfdrive/car/honda/interface.py b/selfdrive/car/honda/interface.py index 994152608e3bdd..a78189b697448c 100755 --- a/selfdrive/car/honda/interface.py +++ b/selfdrive/car/honda/interface.py @@ -3,7 +3,7 @@ from panda import Panda from common.conversions import Conversions as CV from common.numpy_fast import interp -from selfdrive.car.honda.values import CarControllerParams, CruiseButtons, HondaFlags, CAR, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL +from selfdrive.car.honda.values import CarControllerParams, CruiseButtons, HondaFlags, CAR, HONDA_BOSCH, HONDA_NIDEC_ALT_SCM_MESSAGES, HONDA_BOSCH_ALT_BRAKE_SIGNAL, HONDA_BOSCH_RADARLESS from selfdrive.car import STD_CARGO_KG, CivicParams, create_button_enable_events, create_button_event, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase from selfdrive.car.disable_ecu import disable_ecu @@ -37,9 +37,10 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.hondaBosch)] ret.radarOffCan = True - # Disable the radar and let openpilot control longitudinal - # WARNING: THIS DISABLES AEB! - ret.openpilotLongitudinalControl = disable_radar + if candidate not in HONDA_BOSCH_RADARLESS: + # Disable the radar and let openpilot control longitudinal + # WARNING: THIS DISABLES AEB! + ret.openpilotLongitudinalControl = disable_radar ret.pcmCruise = not ret.openpilotLongitudinalControl else: @@ -104,7 +105,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[1.1], [0.33]] tire_stiffness_factor = 1. - elif candidate in (CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL): + elif candidate in (CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CIVIC_2022): stop_and_go = True ret.mass = CivicParams.MASS ret.wheelbase = CivicParams.WHEELBASE @@ -304,6 +305,9 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl if ret.openpilotLongitudinalControl and candidate in HONDA_BOSCH: ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_BOSCH_LONG + if candidate in HONDA_BOSCH_RADARLESS: + ret.safetyConfigs[0].safetyParam |= Panda.FLAG_HONDA_RADARLESS + # min speed to enable ACC. if car can do stop and go, then set enabling speed # to a negative value, so it won't matter. Otherwise, add 0.5 mph margin to not # conflict with PCM acc @@ -325,7 +329,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl @staticmethod def init(CP, logcan, sendcan): - if CP.carFingerprint in HONDA_BOSCH and CP.openpilotLongitudinalControl: + if CP.carFingerprint in (HONDA_BOSCH - HONDA_BOSCH_RADARLESS) and CP.openpilotLongitudinalControl: disable_ecu(logcan, sendcan, bus=1, addr=0x18DAB0F1, com_cont_req=b'\x28\x83\x03') # returns a car.CarState diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 152f71e98a7286..0bdb8ccd912e0e 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -75,6 +75,7 @@ class CAR: CIVIC = "HONDA CIVIC 2016" CIVIC_BOSCH = "HONDA CIVIC (BOSCH) 2019" CIVIC_BOSCH_DIESEL = "HONDA CIVIC SEDAN 1.6 DIESEL 2019" + CIVIC_2022 = "HONDA CIVIC 2022" ACURA_ILX = "ACURA ILX 2016" CRV = "HONDA CR-V 2016" CRV_5G = "HONDA CR-V 2017" @@ -108,33 +109,34 @@ class HondaCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.ACCORD: [ - HondaCarInfo("Honda Accord 2018-21", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), - HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), + HondaCarInfo("Honda Accord 2018-21", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), ], - CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), + CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", harness=Harness.nidec), CAR.CIVIC_BOSCH: [ - HondaCarInfo("Honda Civic 2019-20", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch), - HondaCarInfo("Honda Civic Hatchback 2017-21", harness=Harness.bosch), + HondaCarInfo("Honda Civic 2019-20", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Civic Hatchback 2017-21", harness=Harness.bosch_a), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform + CAR.CIVIC_2022: HondaCarInfo("Honda Civic 2022", "All", min_steer_speed=0., harness=Harness.bosch_b), CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS, harness=Harness.nidec), CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring", harness=Harness.nidec), - CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-21", harness=Harness.bosch), + CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-21", harness=Harness.bosch_a), CAR.CRV_EU: None, # HondaCarInfo("Honda CR-V EU", "Touring"), # Euro version of CRV Touring - CAR.CRV_HYBRID: HondaCarInfo("Honda CR-V Hybrid 2017-19", harness=Harness.bosch), + CAR.CRV_HYBRID: HondaCarInfo("Honda CR-V Hybrid 2017-19", harness=Harness.bosch_a), CAR.FIT: HondaCarInfo("Honda Fit 2018-19", harness=Harness.nidec), CAR.FREED: HondaCarInfo("Honda Freed 2020", harness=Harness.nidec), CAR.HRV: HondaCarInfo("Honda HR-V 2019-20", harness=Harness.nidec), CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-20", min_steer_speed=0., harness=Harness.nidec), CAR.ODYSSEY_CHN: None, # Chinese version of Odyssey CAR.ACURA_RDX: HondaCarInfo("Acura RDX 2016-18", "AcuraWatch Plus", harness=Harness.nidec), - CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), + CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), CAR.PILOT: HondaCarInfo("Honda Pilot 2016-21", harness=Harness.nidec), CAR.PASSPORT: HondaCarInfo("Honda Passport 2019-21", "All", harness=Harness.nidec), CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-22", harness=Harness.nidec), - CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), - CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch), + CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), } @@ -1392,6 +1394,40 @@ class HondaCarInfo(CarInfo): b'57114-TYF-E030\x00\x00' ], }, + CAR.CIVIC_2022: { + (Ecu.eps, 0x18DA30F1, None): [ + b'39990-T39-A130\x00\x00', + b'39990-T43-J020\x00\x00', + ], + (Ecu.gateway, 0x18DAEFF1, None): [ + b'38897-T20-A020\x00\x00', + b'38897-T20-A510\x00\x00', + b'38897-T21-A010\x00\x00', + ], + (Ecu.srs, 0x18DA53F1, None): [ + b'77959-T20-A970\x00\x00', + b'77959-T47-A940\x00\x00', + ], + (Ecu.combinationMeter, 0x18DA60F1, None): [ + b'78108-T21-A220\x00\x00', + b'78108-T21-A620\x00\x00', + b'78108-T23-A110\x00\x00', + ], + (Ecu.vsa, 0x18DA28F1, None): [ + b'57114-T20-AB40\x00\x00', + b'57114-T43-JB30\x00\x00', + ], + (Ecu.transmission, 0x18da1ef1, None): [ + b'28101-65D-A020\x00\x00', + b'28101-65D-A120\x00\x00', + b'28101-65H-A020\x00\x00', + ], + (Ecu.programmedFuelInjection, 0x18da10f1, None): [ + b'37805-64L-A540\x00\x00', + b'37805-64S-A540\x00\x00', + b'37805-64S-A720\x00\x00', + ], + }, } DBC = { @@ -1417,6 +1453,7 @@ class HondaCarInfo(CarInfo): CAR.RIDGELINE: dbc_dict('acura_ilx_2016_can_generated', 'acura_ilx_2016_nidec'), CAR.INSIGHT: dbc_dict('honda_insight_ex_2019_can_generated', None), CAR.HONDA_E: dbc_dict('acura_rdx_2020_can_generated', None), + CAR.CIVIC_2022: dbc_dict('honda_civic_ex_2022_can_generated', None), } STEER_THRESHOLD = { @@ -1429,5 +1466,6 @@ class HondaCarInfo(CarInfo): HONDA_NIDEC_ALT_SCM_MESSAGES = {CAR.ACURA_ILX, CAR.ACURA_RDX, CAR.CRV, CAR.CRV_EU, CAR.FIT, CAR.FREED, CAR.HRV, CAR.ODYSSEY_CHN, CAR.PILOT, CAR.PASSPORT, CAR.RIDGELINE} HONDA_BOSCH = {CAR.ACCORD, CAR.ACCORDH, CAR.CIVIC_BOSCH, CAR.CIVIC_BOSCH_DIESEL, CAR.CRV_5G, - CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E} + CAR.CRV_HYBRID, CAR.INSIGHT, CAR.ACURA_RDX_3G, CAR.HONDA_E, CAR.CIVIC_2022} HONDA_BOSCH_ALT_BRAKE_SIGNAL = {CAR.ACCORD, CAR.CRV_5G, CAR.ACURA_RDX_3G} +HONDA_BOSCH_RADARLESS = {CAR.CIVIC_2022} diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index a9d36f9eabd636..19b1a05c239723 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -71,6 +71,7 @@ TestRoute("f34a60d68d83b1e5|2020-10-06--14-35-55", HONDA.ACURA_RDX), TestRoute("54fd8451b3974762|2021-04-01--14-50-10", HONDA.RIDGELINE), TestRoute("2d5808fae0b38ac6|2021-09-01--17-14-11", HONDA.HONDA_E), + TestRoute("f44aa96ace22f34a|2021-12-22--06-22-31", HONDA.CIVIC_2022), TestRoute("6fe86b4e410e4c37|2020-07-22--16-27-13", HYUNDAI.HYUNDAI_GENESIS), TestRoute("70c5bec28ec8e345|2020-08-08--12-22-23", HYUNDAI.GENESIS_G70), From 28431f7dddb72da31a79005ef37fb6642f822a4c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jun 2022 14:07:19 -0700 Subject: [PATCH 122/436] Revert "Chrysler: use unified signal definitions (#24895)" This reverts commit 0f0b4cac893b41dd81288947158d3aa9cbbfc2fd. --- opendbc | 2 +- selfdrive/car/chrysler/carstate.py | 31 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/opendbc b/opendbc index 57c8340a180dd8..5e2a82026842a7 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 57c8340a180dd8c75139b18050eb17c72c9cb6e4 +Subproject commit 5e2a82026842a7082e5e81e5823dab6b6616dbf4 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 7571286c56a5e1..e3ee4753cc3e3d 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -18,10 +18,10 @@ def update(self, cp, cp_cam): self.frame = int(cp.vl["EPS_STATUS"]["COUNTER"]) - ret.doorOpen = any([cp.vl["BCM_1"]["Driver_Door_Ajar"], - cp.vl["BCM_1"]["Passenger_Door_Ajar"], - cp.vl["BCM_1"]["Left_Rear_Door_Ajar"], - cp.vl["BCM_1"]["Right_Rear_Door_Ajar"]]) + ret.doorOpen = any([cp.vl["DOORS"]["DOOR_OPEN_FL"], + cp.vl["DOORS"]["DOOR_OPEN_FR"], + cp.vl["DOORS"]["DOOR_OPEN_RL"], + cp.vl["DOORS"]["DOOR_OPEN_RR"]]) ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_UNLATCHED"] == 1 # brake pedal @@ -51,12 +51,12 @@ def update(self, cp, cp_cam): ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) - ret.cruiseState.available = cp.vl["DAS_3"]["ACC_Engaged"] == 1 # ACC is white - ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_Enabled"] == 1 # ACC is green + ret.cruiseState.enabled = cp.vl["ACC_2"]["ACC_STATUS_2"] == 7 # ACC is green. + ret.cruiseState.available = ret.cruiseState.enabled # FIXME: for now same as enabled ret.cruiseState.speed = cp.vl["DASHBOARD"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too ret.cruiseState.nonAdaptive = cp.vl["DASHBOARD"]["CRUISE_STATE"] in (1, 2) - ret.accFaulted = cp.vl["DAS_3"]["ACC_Faulted"] != 0 + ret.accFaulted = cp.vl["ACC_2"]["ACC_FAULTED"] != 0 ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] @@ -82,10 +82,10 @@ def get_can_parser(CP): signals = [ # sig_name, sig_address ("PRNDL", "GEAR"), - ("Driver_Door_Ajar", "BCM_1"), - ("Passenger_Door_Ajar", "BCM_1"), - ("Left_Rear_Door_Ajar", "BCM_1"), - ("Right_Rear_Door_Ajar", "BCM_1"), + ("DOOR_OPEN_FL", "DOORS"), + ("DOOR_OPEN_FR", "DOORS"), + ("DOOR_OPEN_RL", "DOORS"), + ("DOOR_OPEN_RR", "DOORS"), ("Brake_Pedal_State", "ESP_1"), ("Accelerator_Position", "ECM_5"), ("SPEED_LEFT", "SPEED_1"), @@ -97,9 +97,8 @@ def get_can_parser(CP): ("STEER_ANGLE", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), - ("ACC_Enabled", "DAS_3"), - ("ACC_Engaged", "DAS_3"), - ("ACC_Faulted", "DAS_3"), + ("ACC_STATUS_2", "ACC_2"), + ("ACC_FAULTED", "ACC_2"), ("HIGH_BEAM_FLASH", "STEERING_LEVERS"), ("ACC_SPEED_CONFIG_KPH", "DASHBOARD"), ("CRUISE_STATE", "DASHBOARD"), @@ -119,14 +118,14 @@ def get_can_parser(CP): ("SPEED_1", 100), ("WHEEL_SPEEDS", 50), ("STEERING", 100), - ("DAS_3", 50), + ("ACC_2", 50), ("GEAR", 50), ("ECM_5", 50), ("WHEEL_BUTTONS", 50), ("DASHBOARD", 15), ("STEERING_LEVERS", 10), ("SEATBELT_STATUS", 2), - ("BCM_1", 1), + ("DOORS", 1), ("TRACTION_BUTTON", 1), ] From d3185a0af0d5a0382cf4688fa35ab79a48a0914c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jun 2022 14:08:14 -0700 Subject: [PATCH 123/436] Chrysler: 2022 pacific hybrid is supported --- docs/CARS.md | 2 +- selfdrive/car/chrysler/values.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 50db34dbb560ea..77b1c5e787ef6a 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -161,7 +161,7 @@ How We Rate The Cars |Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica 2020|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica Hybrid 2019-21|Adaptive Cruise|||||| +|Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| |Genesis|G90 2018|All|||||| |GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| |Honda|Accord 2018-21|All|||||| diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index a0d4225d9a8595..bdd80757f47552 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -37,7 +37,7 @@ class ChryslerCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017-18"), CAR.PACIFICA_2018_HYBRID: None, # same platforms - CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-21"), + CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-22"), CAR.PACIFICA_2018: ChryslerCarInfo("Chrysler Pacifica 2017-18"), CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2020"), CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), From 6123ab3d1c901ed3763e1a7cb8e1aac3f6b8fda3 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jun 2022 14:43:49 -0700 Subject: [PATCH 124/436] Move camerad to system/ (#24836) * mv camerad * add hardware symlink * fix unit tests --- .github/workflows/selfdrive_tests.yaml | 2 +- Jenkinsfile | 4 +-- SConstruct | 4 +-- docs/c_docs.rst | 6 ++-- release/files_common | 31 ++++++++++--------- release/files_tici | 6 ++-- selfdrive/athena/athenad.py | 2 +- selfdrive/loggerd/encoder/encoder.h | 2 +- selfdrive/loggerd/loggerd.h | 2 +- selfdrive/manager/process_config.py | 4 +-- selfdrive/test/process_replay/test_debayer.py | 4 +-- selfdrive/ui/qt/widgets/cameraview.h | 2 +- selfdrive/ui/replay/logreader.h | 2 +- {selfdrive => system}/camerad/SConscript | 0 {selfdrive => system}/camerad/__init__.py | 0 .../camerad/cameras/camera_common.cc | 8 ++--- .../camerad/cameras/camera_common.h | 2 +- .../camerad/cameras/camera_qcom2.cc | 4 +-- .../camerad/cameras/camera_qcom2.h | 2 +- .../camerad/cameras/real_debayer.cl | 0 .../camerad/cameras/sensor2_i2c.h | 0 {selfdrive => system}/camerad/imgproc/conv.cl | 0 {selfdrive => system}/camerad/imgproc/pool.cl | 0 .../camerad/imgproc/utils.cc | 2 +- {selfdrive => system}/camerad/imgproc/utils.h | 0 .../camerad/include/media/cam_cpas.h | 0 .../camerad/include/media/cam_defs.h | 0 .../camerad/include/media/cam_fd.h | 0 .../camerad/include/media/cam_icp.h | 0 .../camerad/include/media/cam_isp.h | 0 .../camerad/include/media/cam_isp_ife.h | 0 .../camerad/include/media/cam_isp_vfe.h | 0 .../camerad/include/media/cam_jpeg.h | 0 .../camerad/include/media/cam_lrme.h | 0 .../camerad/include/media/cam_req_mgr.h | 0 .../camerad/include/media/cam_sensor.h | 0 .../include/media/cam_sensor_cmn_header.h | 0 .../camerad/include/media/cam_sync.h | 0 .../camerad/include/msm_cam_sensor.h | 0 .../camerad/include/msm_camsensor_sdk.h | 0 .../camerad/include/msmb_camera.h | 0 .../camerad/include/msmb_isp.h | 0 .../camerad/include/msmb_ispif.h | 0 {selfdrive => system}/camerad/main.cc | 2 +- .../camerad/snapshot/__init__.py | 0 .../camerad/snapshot/snapshot.py | 2 +- {selfdrive => system}/camerad/test/.gitignore | 0 .../camerad/test/ae_gray_test.cc | 2 +- .../camerad/test/ae_gray_test.h | 0 .../camerad/test/camera_test.h | 0 .../camerad/test/check_skips.py | 0 .../camerad/test/frame_test.py | 0 .../test/get_thumbnails_for_segment.py | 0 .../camerad/test/stress_restart.sh | 0 .../camerad/test/test_camerad.py | 0 .../camerad/test/test_exposure.py | 2 +- .../camerad/transforms/rgb_to_yuv.cc | 2 +- .../camerad/transforms/rgb_to_yuv.cl | 0 .../camerad/transforms/rgb_to_yuv.h | 0 .../camerad/transforms/rgb_to_yuv_test.cc | 2 +- tools/webcam/README.md | 4 +-- 61 files changed, 53 insertions(+), 52 deletions(-) rename {selfdrive => system}/camerad/SConscript (100%) rename {selfdrive => system}/camerad/__init__.py (100%) rename {selfdrive => system}/camerad/cameras/camera_common.cc (98%) rename {selfdrive => system}/camerad/cameras/camera_common.h (98%) rename {selfdrive => system}/camerad/cameras/camera_qcom2.cc (99%) rename {selfdrive => system}/camerad/cameras/camera_qcom2.h (98%) rename {selfdrive => system}/camerad/cameras/real_debayer.cl (100%) rename {selfdrive => system}/camerad/cameras/sensor2_i2c.h (100%) rename {selfdrive => system}/camerad/imgproc/conv.cl (100%) rename {selfdrive => system}/camerad/imgproc/pool.cl (100%) rename {selfdrive => system}/camerad/imgproc/utils.cc (98%) rename {selfdrive => system}/camerad/imgproc/utils.h (100%) rename {selfdrive => system}/camerad/include/media/cam_cpas.h (100%) rename {selfdrive => system}/camerad/include/media/cam_defs.h (100%) rename {selfdrive => system}/camerad/include/media/cam_fd.h (100%) rename {selfdrive => system}/camerad/include/media/cam_icp.h (100%) rename {selfdrive => system}/camerad/include/media/cam_isp.h (100%) rename {selfdrive => system}/camerad/include/media/cam_isp_ife.h (100%) rename {selfdrive => system}/camerad/include/media/cam_isp_vfe.h (100%) rename {selfdrive => system}/camerad/include/media/cam_jpeg.h (100%) rename {selfdrive => system}/camerad/include/media/cam_lrme.h (100%) rename {selfdrive => system}/camerad/include/media/cam_req_mgr.h (100%) rename {selfdrive => system}/camerad/include/media/cam_sensor.h (100%) rename {selfdrive => system}/camerad/include/media/cam_sensor_cmn_header.h (100%) rename {selfdrive => system}/camerad/include/media/cam_sync.h (100%) rename {selfdrive => system}/camerad/include/msm_cam_sensor.h (100%) rename {selfdrive => system}/camerad/include/msm_camsensor_sdk.h (100%) rename {selfdrive => system}/camerad/include/msmb_camera.h (100%) rename {selfdrive => system}/camerad/include/msmb_isp.h (100%) rename {selfdrive => system}/camerad/include/msmb_ispif.h (100%) rename {selfdrive => system}/camerad/main.cc (89%) rename {selfdrive => system}/camerad/snapshot/__init__.py (100%) rename {selfdrive => system}/camerad/snapshot/snapshot.py (98%) rename {selfdrive => system}/camerad/test/.gitignore (100%) rename {selfdrive => system}/camerad/test/ae_gray_test.cc (97%) rename {selfdrive => system}/camerad/test/ae_gray_test.h (100%) rename {selfdrive => system}/camerad/test/camera_test.h (100%) rename {selfdrive => system}/camerad/test/check_skips.py (100%) rename {selfdrive => system}/camerad/test/frame_test.py (100%) rename {selfdrive => system}/camerad/test/get_thumbnails_for_segment.py (100%) rename {selfdrive => system}/camerad/test/stress_restart.sh (100%) rename {selfdrive => system}/camerad/test/test_camerad.py (100%) rename {selfdrive => system}/camerad/test/test_exposure.py (96%) rename {selfdrive => system}/camerad/transforms/rgb_to_yuv.cc (96%) rename {selfdrive => system}/camerad/transforms/rgb_to_yuv.cl (100%) rename {selfdrive => system}/camerad/transforms/rgb_to_yuv.h (100%) rename {selfdrive => system}/camerad/transforms/rgb_to_yuv_test.cc (99%) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 5bd9acc200ce91..fedeaa734bb65d 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -310,7 +310,7 @@ jobs: ./selfdrive/loggerd/tests/test_logger &&\ ./system/proclogd/tests/test_proclog && \ ./selfdrive/ui/replay/tests/test_replay && \ - ./selfdrive/camerad/test/ae_gray_test && \ + ./system/camerad/test/ae_gray_test && \ coverage xml" - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v2 diff --git a/Jenkinsfile b/Jenkinsfile index 279716d7cf8407..dba1f257d6e136 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -139,8 +139,8 @@ pipeline { steps { phone_steps("tici-party", [ ["build", "cd selfdrive/manager && ./build.py"], - ["test camerad", "python selfdrive/camerad/test/test_camerad.py"], - ["test exposure", "python selfdrive/camerad/test/test_exposure.py"], + ["test camerad", "python system/camerad/test/test_camerad.py"], + ["test exposure", "python system/camerad/test/test_exposure.py"], ]) } } diff --git a/SConstruct b/SConstruct index b5c4edc99bff0d..b243a4dc45d609 100644 --- a/SConstruct +++ b/SConstruct @@ -94,7 +94,7 @@ if arch == "larch64": "/usr/lib/aarch64-linux-gnu" ] cpppath += [ - "#selfdrive/camerad/include", + "#system/camerad/include", ] cflags = ["-DQCOM2", "-mcpu=cortex-a57"] cxxflags = ["-DQCOM2", "-mcpu=cortex-a57"] @@ -379,6 +379,7 @@ SConscript(['rednose/SConscript']) # Build system services SConscript([ + 'system/camerad/SConscript', 'system/clocksd/SConscript', 'system/proclogd/SConscript', ]) @@ -396,7 +397,6 @@ SConscript(['third_party/SConscript']) SConscript(['common/kalman/SConscript']) SConscript(['common/transformations/SConscript']) -SConscript(['selfdrive/camerad/SConscript']) SConscript(['selfdrive/modeld/SConscript']) SConscript(['selfdrive/controls/lib/cluster/SConscript']) diff --git a/docs/c_docs.rst b/docs/c_docs.rst index 218cd5a7d96e88..4e1e8a247a100d 100644 --- a/docs/c_docs.rst +++ b/docs/c_docs.rst @@ -28,11 +28,11 @@ selfdrive camerad ^^^^^^^ .. autodoxygenindex:: - :project: selfdrive_camerad_cameras + :project: system_camerad_cameras .. autodoxygenindex:: - :project: selfdrive_camerad_transforms + :project: system_camerad_transforms .. autodoxygenindex:: - :project: selfdrive_camerad_imgproc + :project: system_camerad_imgproc locationd ^^^^^^^^^ diff --git a/release/files_common b/release/files_common index e84c6687323a3f..ecb71b4e692a84 100644 --- a/release/files_common +++ b/release/files_common @@ -182,6 +182,7 @@ selfdrive/controls/lib/longitudinal_mpc_lib/.gitignore selfdrive/controls/lib/lateral_mpc_lib/* selfdrive/controls/lib/longitudinal_mpc_lib/* +selfdrive/hardware system/hardware/__init__.py system/hardware/base.h system/hardware/base.py @@ -298,24 +299,24 @@ selfdrive/ui/replay/*.h selfdrive/ui/qt/maps/*.cc selfdrive/ui/qt/maps/*.h -selfdrive/camerad/SConscript -selfdrive/camerad/main.cc +system/camerad/SConscript +system/camerad/main.cc -selfdrive/camerad/snapshot/* -selfdrive/camerad/include/* -selfdrive/camerad/cameras/camera_common.h -selfdrive/camerad/cameras/camera_common.cc -selfdrive/camerad/cameras/sensor2_i2c.h +system/camerad/snapshot/* +system/camerad/include/* +system/camerad/cameras/camera_common.h +system/camerad/cameras/camera_common.cc +system/camerad/cameras/sensor2_i2c.h -selfdrive/camerad/transforms/rgb_to_yuv.cc -selfdrive/camerad/transforms/rgb_to_yuv.h -selfdrive/camerad/transforms/rgb_to_yuv.cl -selfdrive/camerad/transforms/rgb_to_yuv_test.cc +system/camerad/transforms/rgb_to_yuv.cc +system/camerad/transforms/rgb_to_yuv.h +system/camerad/transforms/rgb_to_yuv.cl +system/camerad/transforms/rgb_to_yuv_test.cc -selfdrive/camerad/imgproc/conv.cl -selfdrive/camerad/imgproc/pool.cl -selfdrive/camerad/imgproc/utils.cc -selfdrive/camerad/imgproc/utils.h +system/camerad/imgproc/conv.cl +system/camerad/imgproc/pool.cl +system/camerad/imgproc/utils.cc +system/camerad/imgproc/utils.h selfdrive/manager/__init__.py selfdrive/manager/build.py diff --git a/release/files_tici b/release/files_tici index 485a898799ed4b..f8c0a279593806 100644 --- a/release/files_tici +++ b/release/files_tici @@ -7,9 +7,9 @@ system/timezoned.py selfdrive/assets/navigation/* selfdrive/assets/training_wide/* -selfdrive/camerad/cameras/camera_qcom2.cc -selfdrive/camerad/cameras/camera_qcom2.h -selfdrive/camerad/cameras/real_debayer.cl +system/camerad/cameras/camera_qcom2.cc +system/camerad/cameras/camera_qcom2.h +system/camerad/cameras/real_debayer.cl selfdrive/ui/qt/spinner_larch64 selfdrive/ui/qt/text_larch64 diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index 26220dfa99d430..b0e138c49527c7 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -493,7 +493,7 @@ def getNetworks(): @dispatcher.add_method def takeSnapshot(): - from selfdrive.camerad.snapshot.snapshot import jpeg_write, snapshot + from system.camerad.snapshot.snapshot import jpeg_write, snapshot ret = snapshot() if ret is not None: def b64jpeg(x): diff --git a/selfdrive/loggerd/encoder/encoder.h b/selfdrive/loggerd/encoder/encoder.h index 312b68ba19945d..21ef65cf12bbef 100644 --- a/selfdrive/loggerd/encoder/encoder.h +++ b/selfdrive/loggerd/encoder/encoder.h @@ -8,7 +8,7 @@ #include "cereal/visionipc/visionipc.h" #include "common/queue.h" #include "selfdrive/loggerd/video_writer.h" -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #define V4L2_BUF_FLAG_KEYFRAME 8 diff --git a/selfdrive/loggerd/loggerd.h b/selfdrive/loggerd/loggerd.h index 7e13e90e63117e..2c4990086a2109 100644 --- a/selfdrive/loggerd/loggerd.h +++ b/selfdrive/loggerd/loggerd.h @@ -15,7 +15,7 @@ #include "cereal/services.h" #include "cereal/visionipc/visionipc.h" #include "cereal/visionipc/visionipc_client.h" -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include "common/params.h" #include "common/swaglog.h" #include "common/timing.h" diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index a9a5c78a7f80b6..7e4029664d9280 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -18,6 +18,8 @@ def logging(started, params, CP: car.CarParams) -> bool: return started and run procs = [ + # due to qualcomm kernel bugs SIGKILLing camerad sometimes causes page table corruption + NativeProcess("camerad", "system/camerad", ["./camerad"], unkillable=True, callback=driverview), NativeProcess("clocksd", "system/clocksd", ["./clocksd"]), NativeProcess("logcatd", "system/logcatd", ["./logcatd"]), NativeProcess("proclogd", "system/proclogd", ["./proclogd"]), @@ -25,8 +27,6 @@ def logging(started, params, CP: car.CarParams) -> bool: PythonProcess("timezoned", "system.timezoned", enabled=not PC, offroad=True), DaemonProcess("manage_athenad", "selfdrive.athena.manage_athenad", "AthenadPid"), - # due to qualcomm kernel bugs SIGKILLing camerad sometimes causes page table corruption - NativeProcess("camerad", "selfdrive/camerad", ["./camerad"], unkillable=True, callback=driverview), NativeProcess("dmonitoringmodeld", "selfdrive/modeld", ["./dmonitoringmodeld"], enabled=(not PC or WEBCAM), callback=driverview), NativeProcess("encoderd", "selfdrive/loggerd", ["./encoderd"]), NativeProcess("loggerd", "selfdrive/loggerd", ["./loggerd"], onroad=False, callback=logging), diff --git a/selfdrive/test/process_replay/test_debayer.py b/selfdrive/test/process_replay/test_debayer.py index eff77fc479d591..1b3e0f112e5ad1 100755 --- a/selfdrive/test/process_replay/test_debayer.py +++ b/selfdrive/test/process_replay/test_debayer.py @@ -10,7 +10,7 @@ from common.basedir import BASEDIR from selfdrive.test.openpilotci import BASE_URL, get_url from system.version import get_commit -from selfdrive.camerad.snapshot.snapshot import yuv_to_rgb +from system.camerad.snapshot.snapshot import yuv_to_rgb from tools.lib.logreader import LogReader from tools.lib.filereader import FileReader @@ -62,7 +62,7 @@ def unbzip_frames(url): def init_kernels(frame_offset=0): ctx = cl.create_some_context(interactive=False) - with open(os.path.join(BASEDIR, 'selfdrive/camerad/cameras/real_debayer.cl')) as f: + with open(os.path.join(BASEDIR, 'system/camerad/cameras/real_debayer.cl')) as f: build_args = ' -cl-fast-relaxed-math -cl-denorms-are-zero -cl-single-precision-constant' + \ f' -DFRAME_STRIDE={FRAME_STRIDE} -DRGB_WIDTH={FRAME_WIDTH} -DRGB_HEIGHT={FRAME_HEIGHT} -DFRAME_OFFSET={frame_offset} -DCAM_NUM=0' if PC: diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index ddc3fc253b3a41..42e9043602000f 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -17,7 +17,7 @@ #endif #include "cereal/visionipc/visionipc_client.h" -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include "selfdrive/ui/ui.h" const int FRAME_BUFFER_SIZE = 5; diff --git a/selfdrive/ui/replay/logreader.h b/selfdrive/ui/replay/logreader.h index 7ada20605e964d..b4a38a57210721 100644 --- a/selfdrive/ui/replay/logreader.h +++ b/selfdrive/ui/replay/logreader.h @@ -6,7 +6,7 @@ #endif #include "cereal/gen/cpp/log.capnp.h" -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include "selfdrive/ui/replay/filereader.h" const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam}; diff --git a/selfdrive/camerad/SConscript b/system/camerad/SConscript similarity index 100% rename from selfdrive/camerad/SConscript rename to system/camerad/SConscript diff --git a/selfdrive/camerad/__init__.py b/system/camerad/__init__.py similarity index 100% rename from selfdrive/camerad/__init__.py rename to system/camerad/__init__.py diff --git a/selfdrive/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc similarity index 98% rename from selfdrive/camerad/cameras/camera_common.cc rename to system/camerad/cameras/camera_common.cc index a94cfedf1a0a88..34dde9389c55ef 100644 --- a/selfdrive/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -1,4 +1,4 @@ -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include @@ -10,7 +10,7 @@ #include "libyuv.h" #include -#include "selfdrive/camerad/imgproc/utils.h" +#include "system/camerad/imgproc/utils.h" #include "common/clutil.h" #include "common/modeldata.h" #include "common/swaglog.h" @@ -20,9 +20,9 @@ #ifdef QCOM2 #include "CL/cl_ext_qcom.h" -#include "selfdrive/camerad/cameras/camera_qcom2.h" +#include "system/camerad/cameras/camera_qcom2.h" #else -#include "selfdrive/camerad/test/camera_test.h" +#include "system/camerad/test/camera_test.h" #endif ExitHandler do_exit; diff --git a/selfdrive/camerad/cameras/camera_common.h b/system/camerad/cameras/camera_common.h similarity index 98% rename from selfdrive/camerad/cameras/camera_common.h rename to system/camerad/cameras/camera_common.h index e9c7ccd757520c..6b483372bb70a7 100644 --- a/selfdrive/camerad/cameras/camera_common.h +++ b/system/camerad/cameras/camera_common.h @@ -9,7 +9,7 @@ #include "cereal/visionipc/visionbuf.h" #include "cereal/visionipc/visionipc.h" #include "cereal/visionipc/visionipc_server.h" -#include "selfdrive/camerad/transforms/rgb_to_yuv.h" +#include "system/camerad/transforms/rgb_to_yuv.h" #include "common/mat.h" #include "common/queue.h" #include "common/swaglog.h" diff --git a/selfdrive/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc similarity index 99% rename from selfdrive/camerad/cameras/camera_qcom2.cc rename to system/camerad/cameras/camera_qcom2.cc index c1ffc1275a178d..f001009b9a4665 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -1,4 +1,4 @@ -#include "selfdrive/camerad/cameras/camera_qcom2.h" +#include "system/camerad/cameras/camera_qcom2.h" #include #include @@ -20,7 +20,7 @@ #include "media/cam_sensor_cmn_header.h" #include "media/cam_sync.h" #include "common/swaglog.h" -#include "selfdrive/camerad/cameras/sensor2_i2c.h" +#include "system/camerad/cameras/sensor2_i2c.h" // For debugging: // echo "4294967295" > /sys/module/cam_debug_util/parameters/debug_mdl diff --git a/selfdrive/camerad/cameras/camera_qcom2.h b/system/camerad/cameras/camera_qcom2.h similarity index 98% rename from selfdrive/camerad/cameras/camera_qcom2.h rename to system/camerad/cameras/camera_qcom2.h index 88766a68e99e6a..57fef8d49a4362 100644 --- a/selfdrive/camerad/cameras/camera_qcom2.h +++ b/system/camerad/cameras/camera_qcom2.h @@ -6,7 +6,7 @@ #include -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include "common/util.h" #define FRAME_BUF_COUNT 4 diff --git a/selfdrive/camerad/cameras/real_debayer.cl b/system/camerad/cameras/real_debayer.cl similarity index 100% rename from selfdrive/camerad/cameras/real_debayer.cl rename to system/camerad/cameras/real_debayer.cl diff --git a/selfdrive/camerad/cameras/sensor2_i2c.h b/system/camerad/cameras/sensor2_i2c.h similarity index 100% rename from selfdrive/camerad/cameras/sensor2_i2c.h rename to system/camerad/cameras/sensor2_i2c.h diff --git a/selfdrive/camerad/imgproc/conv.cl b/system/camerad/imgproc/conv.cl similarity index 100% rename from selfdrive/camerad/imgproc/conv.cl rename to system/camerad/imgproc/conv.cl diff --git a/selfdrive/camerad/imgproc/pool.cl b/system/camerad/imgproc/pool.cl similarity index 100% rename from selfdrive/camerad/imgproc/pool.cl rename to system/camerad/imgproc/pool.cl diff --git a/selfdrive/camerad/imgproc/utils.cc b/system/camerad/imgproc/utils.cc similarity index 98% rename from selfdrive/camerad/imgproc/utils.cc rename to system/camerad/imgproc/utils.cc index a88b8f4bb11d01..a7bbeb9e8673c4 100644 --- a/selfdrive/camerad/imgproc/utils.cc +++ b/system/camerad/imgproc/utils.cc @@ -1,4 +1,4 @@ -#include "selfdrive/camerad/imgproc/utils.h" +#include "system/camerad/imgproc/utils.h" #include #include diff --git a/selfdrive/camerad/imgproc/utils.h b/system/camerad/imgproc/utils.h similarity index 100% rename from selfdrive/camerad/imgproc/utils.h rename to system/camerad/imgproc/utils.h diff --git a/selfdrive/camerad/include/media/cam_cpas.h b/system/camerad/include/media/cam_cpas.h similarity index 100% rename from selfdrive/camerad/include/media/cam_cpas.h rename to system/camerad/include/media/cam_cpas.h diff --git a/selfdrive/camerad/include/media/cam_defs.h b/system/camerad/include/media/cam_defs.h similarity index 100% rename from selfdrive/camerad/include/media/cam_defs.h rename to system/camerad/include/media/cam_defs.h diff --git a/selfdrive/camerad/include/media/cam_fd.h b/system/camerad/include/media/cam_fd.h similarity index 100% rename from selfdrive/camerad/include/media/cam_fd.h rename to system/camerad/include/media/cam_fd.h diff --git a/selfdrive/camerad/include/media/cam_icp.h b/system/camerad/include/media/cam_icp.h similarity index 100% rename from selfdrive/camerad/include/media/cam_icp.h rename to system/camerad/include/media/cam_icp.h diff --git a/selfdrive/camerad/include/media/cam_isp.h b/system/camerad/include/media/cam_isp.h similarity index 100% rename from selfdrive/camerad/include/media/cam_isp.h rename to system/camerad/include/media/cam_isp.h diff --git a/selfdrive/camerad/include/media/cam_isp_ife.h b/system/camerad/include/media/cam_isp_ife.h similarity index 100% rename from selfdrive/camerad/include/media/cam_isp_ife.h rename to system/camerad/include/media/cam_isp_ife.h diff --git a/selfdrive/camerad/include/media/cam_isp_vfe.h b/system/camerad/include/media/cam_isp_vfe.h similarity index 100% rename from selfdrive/camerad/include/media/cam_isp_vfe.h rename to system/camerad/include/media/cam_isp_vfe.h diff --git a/selfdrive/camerad/include/media/cam_jpeg.h b/system/camerad/include/media/cam_jpeg.h similarity index 100% rename from selfdrive/camerad/include/media/cam_jpeg.h rename to system/camerad/include/media/cam_jpeg.h diff --git a/selfdrive/camerad/include/media/cam_lrme.h b/system/camerad/include/media/cam_lrme.h similarity index 100% rename from selfdrive/camerad/include/media/cam_lrme.h rename to system/camerad/include/media/cam_lrme.h diff --git a/selfdrive/camerad/include/media/cam_req_mgr.h b/system/camerad/include/media/cam_req_mgr.h similarity index 100% rename from selfdrive/camerad/include/media/cam_req_mgr.h rename to system/camerad/include/media/cam_req_mgr.h diff --git a/selfdrive/camerad/include/media/cam_sensor.h b/system/camerad/include/media/cam_sensor.h similarity index 100% rename from selfdrive/camerad/include/media/cam_sensor.h rename to system/camerad/include/media/cam_sensor.h diff --git a/selfdrive/camerad/include/media/cam_sensor_cmn_header.h b/system/camerad/include/media/cam_sensor_cmn_header.h similarity index 100% rename from selfdrive/camerad/include/media/cam_sensor_cmn_header.h rename to system/camerad/include/media/cam_sensor_cmn_header.h diff --git a/selfdrive/camerad/include/media/cam_sync.h b/system/camerad/include/media/cam_sync.h similarity index 100% rename from selfdrive/camerad/include/media/cam_sync.h rename to system/camerad/include/media/cam_sync.h diff --git a/selfdrive/camerad/include/msm_cam_sensor.h b/system/camerad/include/msm_cam_sensor.h similarity index 100% rename from selfdrive/camerad/include/msm_cam_sensor.h rename to system/camerad/include/msm_cam_sensor.h diff --git a/selfdrive/camerad/include/msm_camsensor_sdk.h b/system/camerad/include/msm_camsensor_sdk.h similarity index 100% rename from selfdrive/camerad/include/msm_camsensor_sdk.h rename to system/camerad/include/msm_camsensor_sdk.h diff --git a/selfdrive/camerad/include/msmb_camera.h b/system/camerad/include/msmb_camera.h similarity index 100% rename from selfdrive/camerad/include/msmb_camera.h rename to system/camerad/include/msmb_camera.h diff --git a/selfdrive/camerad/include/msmb_isp.h b/system/camerad/include/msmb_isp.h similarity index 100% rename from selfdrive/camerad/include/msmb_isp.h rename to system/camerad/include/msmb_isp.h diff --git a/selfdrive/camerad/include/msmb_ispif.h b/system/camerad/include/msmb_ispif.h similarity index 100% rename from selfdrive/camerad/include/msmb_ispif.h rename to system/camerad/include/msmb_ispif.h diff --git a/selfdrive/camerad/main.cc b/system/camerad/main.cc similarity index 89% rename from selfdrive/camerad/main.cc rename to system/camerad/main.cc index 32899a8547d29f..c1f38f2224e392 100644 --- a/selfdrive/camerad/main.cc +++ b/system/camerad/main.cc @@ -1,4 +1,4 @@ -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" #include diff --git a/selfdrive/camerad/snapshot/__init__.py b/system/camerad/snapshot/__init__.py similarity index 100% rename from selfdrive/camerad/snapshot/__init__.py rename to system/camerad/snapshot/__init__.py diff --git a/selfdrive/camerad/snapshot/snapshot.py b/system/camerad/snapshot/snapshot.py similarity index 98% rename from selfdrive/camerad/snapshot/snapshot.py rename to system/camerad/snapshot/snapshot.py index fa88849b69f317..9a81937eec3ceb 100755 --- a/selfdrive/camerad/snapshot/snapshot.py +++ b/system/camerad/snapshot/snapshot.py @@ -13,7 +13,7 @@ from selfdrive.controls.lib.alertmanager import set_offroad_alert from selfdrive.manager.process_config import managed_processes -LM_THRESH = 120 # defined in selfdrive/camerad/imgproc/utils.h +LM_THRESH = 120 # defined in system/camerad/imgproc/utils.h VISION_STREAMS = { "roadCameraState": VisionStreamType.VISION_STREAM_ROAD, diff --git a/selfdrive/camerad/test/.gitignore b/system/camerad/test/.gitignore similarity index 100% rename from selfdrive/camerad/test/.gitignore rename to system/camerad/test/.gitignore diff --git a/selfdrive/camerad/test/ae_gray_test.cc b/system/camerad/test/ae_gray_test.cc similarity index 97% rename from selfdrive/camerad/test/ae_gray_test.cc rename to system/camerad/test/ae_gray_test.cc index 69ef2ac6de285f..aabd7534ee44c6 100644 --- a/selfdrive/camerad/test/ae_gray_test.cc +++ b/system/camerad/test/ae_gray_test.cc @@ -8,7 +8,7 @@ #include #include "common/util.h" -#include "selfdrive/camerad/cameras/camera_common.h" +#include "system/camerad/cameras/camera_common.h" int main() { // set up fake camerabuf diff --git a/selfdrive/camerad/test/ae_gray_test.h b/system/camerad/test/ae_gray_test.h similarity index 100% rename from selfdrive/camerad/test/ae_gray_test.h rename to system/camerad/test/ae_gray_test.h diff --git a/selfdrive/camerad/test/camera_test.h b/system/camerad/test/camera_test.h similarity index 100% rename from selfdrive/camerad/test/camera_test.h rename to system/camerad/test/camera_test.h diff --git a/selfdrive/camerad/test/check_skips.py b/system/camerad/test/check_skips.py similarity index 100% rename from selfdrive/camerad/test/check_skips.py rename to system/camerad/test/check_skips.py diff --git a/selfdrive/camerad/test/frame_test.py b/system/camerad/test/frame_test.py similarity index 100% rename from selfdrive/camerad/test/frame_test.py rename to system/camerad/test/frame_test.py diff --git a/selfdrive/camerad/test/get_thumbnails_for_segment.py b/system/camerad/test/get_thumbnails_for_segment.py similarity index 100% rename from selfdrive/camerad/test/get_thumbnails_for_segment.py rename to system/camerad/test/get_thumbnails_for_segment.py diff --git a/selfdrive/camerad/test/stress_restart.sh b/system/camerad/test/stress_restart.sh similarity index 100% rename from selfdrive/camerad/test/stress_restart.sh rename to system/camerad/test/stress_restart.sh diff --git a/selfdrive/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py similarity index 100% rename from selfdrive/camerad/test/test_camerad.py rename to system/camerad/test/test_camerad.py diff --git a/selfdrive/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py similarity index 96% rename from selfdrive/camerad/test/test_exposure.py rename to system/camerad/test/test_exposure.py index f42d0bfbe3e60c..8cce7e7ffa592b 100755 --- a/selfdrive/camerad/test/test_exposure.py +++ b/system/camerad/test/test_exposure.py @@ -4,7 +4,7 @@ import numpy as np from selfdrive.test.helpers import with_processes -from selfdrive.camerad.snapshot.snapshot import get_snapshots +from system.camerad.snapshot.snapshot import get_snapshots from system.hardware import TICI diff --git a/selfdrive/camerad/transforms/rgb_to_yuv.cc b/system/camerad/transforms/rgb_to_yuv.cc similarity index 96% rename from selfdrive/camerad/transforms/rgb_to_yuv.cc rename to system/camerad/transforms/rgb_to_yuv.cc index 63e032e2dc15c7..5e51579cf9d89f 100644 --- a/selfdrive/camerad/transforms/rgb_to_yuv.cc +++ b/system/camerad/transforms/rgb_to_yuv.cc @@ -1,4 +1,4 @@ -#include "selfdrive/camerad/transforms/rgb_to_yuv.h" +#include "system/camerad/transforms/rgb_to_yuv.h" #include #include diff --git a/selfdrive/camerad/transforms/rgb_to_yuv.cl b/system/camerad/transforms/rgb_to_yuv.cl similarity index 100% rename from selfdrive/camerad/transforms/rgb_to_yuv.cl rename to system/camerad/transforms/rgb_to_yuv.cl diff --git a/selfdrive/camerad/transforms/rgb_to_yuv.h b/system/camerad/transforms/rgb_to_yuv.h similarity index 100% rename from selfdrive/camerad/transforms/rgb_to_yuv.h rename to system/camerad/transforms/rgb_to_yuv.h diff --git a/selfdrive/camerad/transforms/rgb_to_yuv_test.cc b/system/camerad/transforms/rgb_to_yuv_test.cc similarity index 99% rename from selfdrive/camerad/transforms/rgb_to_yuv_test.cc rename to system/camerad/transforms/rgb_to_yuv_test.cc index c960d168debbc6..2f909e3b73c145 100644 --- a/selfdrive/camerad/transforms/rgb_to_yuv_test.cc +++ b/system/camerad/transforms/rgb_to_yuv_test.cc @@ -30,7 +30,7 @@ #include #include "libyuv.h" -#include "selfdrive/camerad/transforms/rgb_to_yuv.h" +#include "system/camerad/transforms/rgb_to_yuv.h" #include "common/clutil.h" static inline double millis_since_boot() { diff --git a/tools/webcam/README.md b/tools/webcam/README.md index 48e5accb2ab7ae..1e07bdbbe68f71 100644 --- a/tools/webcam/README.md +++ b/tools/webcam/README.md @@ -23,14 +23,14 @@ git clone https://github.com/commaai/openpilot.git ``` cd ~/openpilot ``` -- check out selfdrive/camerad/cameras/camera_webcam.cc lines 72 and 146 before building if any camera is upside down +- check out system/camerad/cameras/camera_webcam.cc lines 72 and 146 before building if any camera is upside down ``` USE_WEBCAM=1 scons -j$(nproc) ``` ## Connect the hardware - Connect the road facing camera first, then the driver facing camera -- (default indexes are 1 and 2; can be modified in selfdrive/camerad/cameras/camera_webcam.cc) +- (default indexes are 1 and 2; can be modified in system/camerad/cameras/camera_webcam.cc) - Connect your computer to panda ## GO From 1baf0e3f4a40084a43c6a6d4d1a43b99e4e90532 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 19 Jun 2022 15:49:38 -0700 Subject: [PATCH 125/436] sim: update cl kernel path --- tools/sim/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py index ae65e8deb845e8..faf67c3ef52ce1 100755 --- a/tools/sim/bridge.py +++ b/tools/sim/bridge.py @@ -81,7 +81,7 @@ def __init__(self): cl_arg = f" -DHEIGHT={H} -DWIDTH={W} -DRGB_STRIDE={W * 3} -DUV_WIDTH={W // 2} -DUV_HEIGHT={H // 2} -DRGB_SIZE={W * H} -DCL_DEBUG " # TODO: move rgb_to_yuv.cl to local dir once the frame stream camera is removed - kernel_fn = os.path.join(BASEDIR, "selfdrive", "camerad", "transforms", "rgb_to_yuv.cl") + kernel_fn = os.path.join(BASEDIR, "system", "camerad", "transforms", "rgb_to_yuv.cl") with open(kernel_fn) as f: prg = cl.Program(self.ctx, f.read()).build(cl_arg) self.krnl = prg.rgb_to_yuv From 7a815418d600bed1e2500b4fdbe4f1958f432555 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 20 Jun 2022 12:12:18 +0200 Subject: [PATCH 126/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 6faf34064b70ab..f445245a8fbe01 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 6faf34064b70ab98c241d4a1cd9006b09ecaadfc +Subproject commit f445245a8fbe01c33a372d82897575e7acc08ffc From e2757208622e29a00d5191d7d2f971cafcf61602 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Mon, 20 Jun 2022 12:15:39 +0200 Subject: [PATCH 127/436] Add new message to Tesla AP2 fingerprint (#24912) * add new message * fix spacing * add space --- selfdrive/car/tesla/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/tesla/values.py b/selfdrive/car/tesla/values.py index 2ed4c963ee4563..296169587adeeb 100644 --- a/selfdrive/car/tesla/values.py +++ b/selfdrive/car/tesla/values.py @@ -22,7 +22,7 @@ class CAR: FINGERPRINTS = { CAR.AP2_MODELS: [ { - 1: 8, 3: 8, 14: 8, 21: 4, 69: 8, 109: 4, 257: 3, 264: 8, 277: 6, 280: 6, 293: 4, 296: 4, 309: 5, 325: 8, 328: 5, 336: 8, 341: 8, 360: 7, 373: 8, 389: 8, 415: 8, 513: 5, 516: 8, 518: 8, 520: 4, 522: 8, 524: 8, 526: 8, 532: 3, 536: 8, 537: 3, 538: 8, 542: 8, 551: 5, 552: 2, 556: 8, 558: 8, 568: 8, 569: 8, 574: 8, 576: 3, 577: 8, 582: 5, 583: 8, 584: 4, 585: 8, 590: 8, 601: 8, 606: 8, 608: 1, 622: 8, 627: 6, 638: 8, 641: 8, 643: 8, 692: 8, 693: 8, 695: 8, 696: 8, 697: 8, 699: 8, 700: 8, 701: 8, 702: 8, 703: 8, 704: 8, 708: 8, 709: 8, 710: 8, 711: 8, 712: 8, 728: 8, 744: 8, 760: 8, 772: 8, 775: 8, 776: 8, 777: 8, 778: 8, 782: 8, 788: 8, 791: 8, 792: 8, 796: 2, 797: 8, 798: 6, 799: 8, 804: 8, 805: 8, 807: 8, 808: 1, 811: 8, 812: 8, 813: 8, 814: 5, 815: 8, 820: 8, 823: 8, 824: 8, 829: 8, 830: 5, 836: 8, 840: 8, 845: 8, 846: 5, 848: 8, 852: 8, 853: 8, 856: 4, 857: 6, 861: 8, 862: 5, 872: 8, 876: 8, 877: 8, 879: 8, 880: 8, 882: 8, 884: 8, 888: 8, 893: 8, 894: 8, 901: 6, 904: 3, 905: 8, 906: 8, 908: 2, 909: 8, 910: 8, 912: 8, 920: 8, 921: 8, 925: 4, 926: 6, 936: 8, 941: 8, 949: 8, 952: 8, 953: 6, 968: 8, 969: 6, 970: 8, 971: 8, 977: 8, 984: 8, 987: 8, 990: 8, 1000: 8, 1001: 8, 1006: 8, 1007: 8, 1008: 8, 1010: 6, 1014: 1, 1015: 8, 1016: 8, 1017: 8, 1018: 8, 1020: 8, 1026: 8, 1028: 8, 1029: 8, 1030: 8, 1032: 1, 1033: 1, 1034: 8, 1048: 1, 1049: 8, 1061: 8, 1064: 8, 1065: 8, 1070: 8, 1080: 8, 1081: 8, 1097: 8, 1113: 8, 1129: 8, 1145: 8, 1160: 4, 1177: 8, 1281: 8, 1328: 8, 1329: 8, 1332: 8, 1335: 8, 1337: 8, 1353: 8, 1368: 8, 1412: 8, 1436: 8, 1476: 8, 1481: 8, 1497: 8, 1513: 8, 1519: 8, 1601: 8, 1605: 8, 1617: 8, 1621: 8, 1625: 8, 1800: 4, 1804: 8, 1812: 8, 1815: 8, 1816: 8, 1824: 8, 1828: 8, 1831: 8, 1832: 8, 1840: 8, 1848: 8, 1864: 8, 1880: 8, 1892: 8, 1896: 8, 1912: 8, 1960: 8, 1992: 8, 2008: 3, 2015: 8, 2043: 5, 2045: 4 + 1: 8, 3: 8, 14: 8, 21: 4, 69: 8, 109: 4, 257: 3, 264: 8, 277: 6, 280: 6, 293: 4, 296: 4, 309: 5, 325: 8, 328: 5, 336: 8, 341: 8, 360: 7, 373: 8, 389: 8, 415: 8, 513: 5, 516: 8, 518: 8, 520: 4, 522: 8, 524: 8, 526: 8, 532: 3, 536: 8, 537: 3, 538: 8, 542: 8, 551: 5, 552: 2, 556: 8, 558: 8, 568: 8, 569: 8, 574: 8, 576: 3, 577: 8, 582: 5, 583: 8, 584: 4, 585: 8, 590: 8, 601: 8, 606: 8, 608: 1, 622: 8, 627: 6, 638: 8, 641: 8, 643: 8, 692: 8, 693: 8, 695: 8, 696: 8, 697: 8, 699: 8, 700: 8, 701: 8, 702: 8, 703: 8, 704: 8, 708: 8, 709: 8, 710: 8, 711: 8, 712: 8, 728: 8, 744: 8, 760: 8, 772: 8, 775: 8, 776: 8, 777: 8, 778: 8, 782: 8, 788: 8, 791: 8, 792: 8, 796: 2, 797: 8, 798: 6, 799: 8, 804: 8, 805: 8, 807: 8, 808: 1, 811: 8, 812: 8, 813: 8, 814: 5, 815: 8, 820: 8, 823: 8, 824: 8, 829: 8, 830: 5, 836: 8, 840: 8, 845: 8, 846: 5, 848: 8, 852: 8, 853: 8, 856: 4, 857: 6, 861: 8, 862: 5, 872: 8, 876: 8, 877: 8, 879: 8, 880: 8, 882: 8, 884: 8, 888: 8, 893: 8, 894: 8, 901: 6, 904: 3, 905: 8, 906: 8, 908: 2, 909: 8, 910: 8, 912: 8, 920: 8, 921: 8, 925: 4, 926: 6, 936: 8, 941: 8, 949: 8, 952: 8, 953: 6, 968: 8, 969: 6, 970: 8, 971: 8, 977: 8, 984: 8, 987: 8, 990: 8, 1000: 8, 1001: 8, 1006: 8, 1007: 8, 1008: 8, 1010: 6, 1014: 1, 1015: 8, 1016: 8, 1017: 8, 1018: 8, 1020: 8, 1026: 8, 1028: 8, 1029: 8, 1030: 8, 1032: 1, 1033: 1, 1034: 8, 1048: 1, 1049: 8, 1061: 8, 1064: 8, 1065: 8, 1070: 8, 1080: 8, 1081: 8, 1097: 8, 1113: 8, 1129: 8, 1145: 8, 1160: 4, 1177: 8, 1281: 8, 1328: 8, 1329: 8, 1332: 8, 1335: 8, 1337: 8, 1353: 8, 1368: 8, 1412: 8, 1436: 8, 1476: 8, 1481: 8, 1497: 8, 1513: 8, 1519: 8, 1601: 8, 1605: 8, 1617: 8, 1621: 8, 1625: 8, 1665: 8, 1800: 4, 1804: 8, 1812: 8, 1815: 8, 1816: 8, 1824: 8, 1828: 8, 1831: 8, 1832: 8, 1840: 8, 1848: 8, 1864: 8, 1880: 8, 1892: 8, 1896: 8, 1912: 8, 1960: 8, 1992: 8, 2008: 3, 2015: 8, 2043: 5, 2045: 4 }, ], CAR.AP1_MODELS: [ From 1c4b145aa4d85b7107254be6618592781ff7323e Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Mon, 20 Jun 2022 14:24:11 +0200 Subject: [PATCH 128/436] Improve laikad cpu usage. Less pos fix. Less local imports (#24887) * Improve laikad cpu usage. Less pos fix. Not local imports * Add laika to files_common * Add laika to dockerfile sim * Fix * Fix * undo --- release/files_common | 1 + selfdrive/locationd/laikad.py | 35 ++++++++++++---------- selfdrive/locationd/models/gnss_helpers.py | 4 +-- tools/sim/Dockerfile.sim | 1 + 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/release/files_common b/release/files_common index ecb71b4e692a84..7274ee9bf6c3be 100644 --- a/release/files_common +++ b/release/files_common @@ -414,6 +414,7 @@ pyextra/.gitignore pyextra/acados_template/** rednose/** +laika/** cereal/.gitignore cereal/__init__.py diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 6fd70875acdd4e..f1997bceb0634b 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -39,7 +39,9 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.save_ephemeris = save_ephemeris self.load_cache() self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} - self.last_pos_fix = last_known_position + self.last_pos_fix = last_known_position if last_known_position is not None else [] + self.last_pos_residual = [] + self.last_pos_fix_t = None def load_cache(self): cache = Params().get(EPHEMERIS_CACHE) @@ -62,23 +64,26 @@ def cache_ephemeris(self, t: GPSTime): def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': + t = ublox_mono_time * 1e-9 report = ublox_msg.measurementReport if report.gpsWeek > 0: latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) + new_meas = read_raw_ublox(report) processed_measurements = process_measurements(new_meas, self.astro_dog) - min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 - pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) - if len(pos_fix) > 0: - self.last_pos_fix = pos_fix[:3] - est_pos = self.last_pos_fix + if self.last_pos_fix_t is None or abs(self.last_pos_fix_t - t) >= 2: + min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 + pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) + if len(pos_fix) > 0: + self.last_pos_fix = pos_fix[:3] + self.last_pos_residual = pos_fix_residual + self.last_pos_fix_t = t - corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) if est_pos is not None else [] + corrected_measurements = correct_measurements(processed_measurements, self.last_pos_fix, self.astro_dog) if self.last_pos_fix_t is not None else [] - t = ublox_mono_time * 1e-9 - self.update_localizer(est_pos, t, corrected_measurements) + self.update_localizer(self.last_pos_fix, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() @@ -94,7 +99,7 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): "gpsTimeOfWeek": report.rcvTow, "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), - "positionFixECEF": measurement_msg(value=pos_fix, std=pos_fix_residual, valid=len(pos_fix) > 0), + "positionFixECEF": measurement_msg(value=self.last_pos_fix, std=self.last_pos_residual, valid=self.last_pos_fix_t == t), "ubloxMonoTime": ublox_mono_time, "correctedMeasurements": meas_msgs } @@ -116,7 +121,7 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") - if est_pos is not None: + if len(est_pos) > 0: cloudlog.info(f"Reset kalman filter with {est_pos}") self.init_gnss_localizer(est_pos) else: @@ -211,13 +216,13 @@ def kf_add_observations(gnss_kf: GNSSKalman, t: float, measurements: List[GNSSMe m_arr = m.as_array() if m.constellation_id == ConstellationId.GPS: ekf_data[ObservationKind.PSEUDORANGE_GPS].append(m_arr) - ekf_data[ObservationKind.PSEUDORANGE_RATE_GPS].append(m_arr) elif m.constellation_id == ConstellationId.GLONASS: ekf_data[ObservationKind.PSEUDORANGE_GLONASS].append(m_arr) - ekf_data[ObservationKind.PSEUDORANGE_RATE_GLONASS].append(m_arr) - + ekf_data[ObservationKind.PSEUDORANGE_RATE_GPS] = ekf_data[ObservationKind.PSEUDORANGE_GPS] + ekf_data[ObservationKind.PSEUDORANGE_RATE_GLONASS] = ekf_data[ObservationKind.PSEUDORANGE_GLONASS] for kind, data in ekf_data.items(): - gnss_kf.predict_and_observe(t, kind, data) + if len(data) >0: + gnss_kf.predict_and_observe(t, kind, data) class CacheSerializer(json.JSONEncoder): diff --git a/selfdrive/locationd/models/gnss_helpers.py b/selfdrive/locationd/models/gnss_helpers.py index a3bcbc00028f83..b6c1771ec60e72 100644 --- a/selfdrive/locationd/models/gnss_helpers.py +++ b/selfdrive/locationd/models/gnss_helpers.py @@ -1,16 +1,14 @@ import numpy as np +from laika.raw_gnss import GNSSMeasurement def parse_prr(m): - from laika.raw_gnss import GNSSMeasurement sat_pos_vel_i = np.concatenate((m[GNSSMeasurement.SAT_POS], m[GNSSMeasurement.SAT_VEL])) R_i = np.atleast_2d(m[GNSSMeasurement.PRR_STD]**2) z_i = m[GNSSMeasurement.PRR] return z_i, R_i, sat_pos_vel_i - def parse_pr(m): - from laika.raw_gnss import GNSSMeasurement pseudorange = m[GNSSMeasurement.PR] pseudorange_stdev = m[GNSSMeasurement.PR_STD] sat_pos_freq_i = np.concatenate((m[GNSSMeasurement.SAT_POS], diff --git a/tools/sim/Dockerfile.sim b/tools/sim/Dockerfile.sim index 0d6e8e584c120b..3692d48e0f3a69 100644 --- a/tools/sim/Dockerfile.sim +++ b/tools/sim/Dockerfile.sim @@ -18,6 +18,7 @@ COPY ./third_party $HOME/openpilot/third_party COPY ./pyextra $HOME/openpilot/pyextra COPY ./site_scons $HOME/openpilot/site_scons COPY ./rednose $HOME/openpilot/rednose +COPY ./laika $HOME/openpilot/laika COPY ./common $HOME/openpilot/common COPY ./opendbc $HOME/openpilot/opendbc COPY ./cereal $HOME/openpilot/cereal From 8270c1c1a3584c1244f12143fc1314cdbbb05541 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 20 Jun 2022 19:14:43 +0200 Subject: [PATCH 129/436] ui: onroad widget check if sockets alive (#24913) * ui: onroad widget check if socket alive * cleanup --- selfdrive/ui/qt/onroad.cc | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 84b538411bd3fb..45bf41ba2df2cd 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -172,22 +172,28 @@ NvgWindow::NvgWindow(VisionStreamType type, QWidget* parent) : fps_filter(UI_FRE void NvgWindow::updateState(const UIState &s) { const int SET_SPEED_NA = 255; const SubMaster &sm = *(s.sm); + + const bool cs_alive = sm.alive("controlsState"); + const bool nav_alive = sm.alive("navInstruction") && sm["navInstruction"].getValid(); + const auto cs = sm["controlsState"].getControlsState(); - float set_speed = cs.getVCruise(); + float set_speed = cs_alive ? cs.getVCruise() : SET_SPEED_NA; bool cruise_set = set_speed > 0 && (int)set_speed != SET_SPEED_NA; if (cruise_set && !s.scene.is_metric) { set_speed *= KM_TO_MILE; } - float cur_speed = std::max(0.0, sm["carState"].getCarState().getVEgo() * (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH)); + + float cur_speed = cs_alive ? std::max(0.0, sm["carState"].getCarState().getVEgo()) : 0.0; + cur_speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH; auto speed_limit_sign = sm["navInstruction"].getNavInstruction().getSpeedLimitSign(); - float speed_limit = sm["navInstruction"].getValid() ? sm["navInstruction"].getNavInstruction().getSpeedLimit() : 0.0; + float speed_limit = nav_alive ? sm["navInstruction"].getNavInstruction().getSpeedLimit() : 0.0; speed_limit *= (s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH); setProperty("speedLimit", speed_limit); - setProperty("has_us_speed_limit", sm["navInstruction"].getValid() && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); - setProperty("has_eu_speed_limit", sm["navInstruction"].getValid() && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); + setProperty("has_us_speed_limit", nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::MUTCD); + setProperty("has_eu_speed_limit", nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); setProperty("is_cruise_set", cruise_set); setProperty("speed", cur_speed); From 1f2f9ea9c9dc37bdea9c6e32e4cb8f88ea0a34bf Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Mon, 20 Jun 2022 16:24:51 -0700 Subject: [PATCH 130/436] fullframe DM model (#24860) * Revert "put cereal on master" This reverts commit a8ccd8f838aff958a60546de04bf42220567b288. * Revert "Revert fullframe DM model (#24812)" This reverts commit c646eeee0ac54925db5afc51b95c5d869d6dba68. * revert revert cereal * clip6 * 0.8 is fair * Fiction compensation should be based on error * Update refs * Add deadzone * not that * good mg * ref * ref * ee8f * minor tweak * ref * recompile * ref * cereal * match driverstatus * new ref * new ref * pass token through jenkins credentials * quote * fix snpe dead weights * final ref Co-authored-by: Harald Schafer Co-authored-by: Adeeb Shihadeh --- Jenkinsfile | 2 + cereal | 2 +- common/modeldata.h | 6 - selfdrive/modeld/dmonitoringmodeld.cc | 6 +- selfdrive/modeld/models/dmonitoring.cc | 234 ++++++------------ selfdrive/modeld/models/dmonitoring.h | 33 ++- .../modeld/models/dmonitoring_model.current | 4 +- .../modeld/models/dmonitoring_model.onnx | 4 +- .../modeld/models/dmonitoring_model_q.dlc | 4 +- selfdrive/modeld/runners/onnx_runner.py | 21 +- selfdrive/modeld/runners/onnxmodel.cc | 6 +- selfdrive/modeld/runners/onnxmodel.h | 3 +- selfdrive/modeld/runners/snpemodel.cc | 12 +- selfdrive/modeld/runners/snpemodel.h | 3 +- selfdrive/monitoring/dmonitoringd.py | 7 +- selfdrive/monitoring/driver_monitor.py | 94 +++---- selfdrive/monitoring/test_monitoring.py | 24 +- selfdrive/test/process_replay/model_replay.py | 8 +- .../process_replay/model_replay_ref_commit | 2 +- .../test/process_replay/process_replay.py | 2 +- selfdrive/test/test_onroad.py | 6 +- selfdrive/ui/qt/offroad/driverview.cc | 43 ++-- selfdrive/ui/qt/widgets/cameraview.cc | 9 +- system/hardware/tici/test_power_draw.py | 2 +- 24 files changed, 229 insertions(+), 308 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index dba1f257d6e136..0fa623fbcd2a24 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,6 +10,7 @@ export TEST_DIR=${env.TEST_DIR} export SOURCE_DIR=${env.SOURCE_DIR} export GIT_BRANCH=${env.GIT_BRANCH} export GIT_COMMIT=${env.GIT_COMMIT} +export AZURE_TOKEN='${env.AZURE_TOKEN}' source ~/.bash_profile if [ -f /TICI ]; then @@ -45,6 +46,7 @@ pipeline { CI = "1" TEST_DIR = "/data/openpilot" SOURCE_DIR = "/data/openpilot_source/" + AZURE_TOKEN = credentials('azure_token') } options { timeout(time: 4, unit: 'HOURS') diff --git a/cereal b/cereal index f445245a8fbe01..05bbdafb67060f 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit f445245a8fbe01c33a372d82897575e7acc08ffc +Subproject commit 05bbdafb67060ffb62e6e0af812935494ad91933 diff --git a/common/modeldata.h b/common/modeldata.h index aee9fdfd8376ee..b1938414164150 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -24,12 +24,6 @@ constexpr auto T_IDXS_FLOAT = build_idxs(10.0); constexpr auto X_IDXS = build_idxs(192.0); constexpr auto X_IDXS_FLOAT = build_idxs(192.0); -namespace tici_dm_crop { - const int x_offset = -72; - const int y_offset = -144; - const int width = 954; -}; - const mat3 fcam_intrinsic_matrix = (mat3){{2648.0, 0.0, 1928.0 / 2, 0.0, 2648.0, 1208.0 / 2, 0.0, 0.0, 1.0}}; diff --git a/selfdrive/modeld/dmonitoringmodeld.cc b/selfdrive/modeld/dmonitoringmodeld.cc index 68c49572e66696..cde13a9beeb6ba 100644 --- a/selfdrive/modeld/dmonitoringmodeld.cc +++ b/selfdrive/modeld/dmonitoringmodeld.cc @@ -12,7 +12,7 @@ ExitHandler do_exit; void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { - PubMaster pm({"driverState"}); + PubMaster pm({"driverStateV2"}); SubMaster sm({"liveCalibration"}); float calib[CALIB_LEN] = {0}; double last = 0; @@ -31,11 +31,11 @@ void run_model(DMonitoringModelState &model, VisionIpcClient &vipc_client) { } double t1 = millis_since_boot(); - DMonitoringResult res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); + DMonitoringModelResult model_res = dmonitoring_eval_frame(&model, buf->addr, buf->width, buf->height, buf->stride, buf->uv_offset, calib); double t2 = millis_since_boot(); // send dm packet - dmonitoring_publish(pm, extra.frame_id, res, (t2 - t1) / 1000.0, model.output); + dmonitoring_publish(pm, extra.frame_id, model_res, (t2 - t1) / 1000.0, model.output); //printf("dmonitoring process: %.2fms, from last %.2fms\n", t2 - t1, t1 - last); last = t1; diff --git a/selfdrive/modeld/models/dmonitoring.cc b/selfdrive/modeld/models/dmonitoring.cc index 71da8dad53921b..8c7e14edb2dc56 100644 --- a/selfdrive/modeld/models/dmonitoring.cc +++ b/selfdrive/modeld/models/dmonitoring.cc @@ -10,8 +10,8 @@ #include "selfdrive/modeld/models/dmonitoring.h" -constexpr int MODEL_WIDTH = 320; -constexpr int MODEL_HEIGHT = 640; +constexpr int MODEL_WIDTH = 1440; +constexpr int MODEL_HEIGHT = 960; template static inline T *get_buffer(std::vector &buf, const size_t size) { @@ -19,199 +19,115 @@ static inline T *get_buffer(std::vector &buf, const size_t size) { return buf.data(); } -static inline void init_yuv_buf(std::vector &buf, const int width, int height) { - uint8_t *y = get_buffer(buf, width * height * 3 / 2); - uint8_t *u = y + width * height; - uint8_t *v = u + (width / 2) * (height / 2); - - // needed on comma two to make the padded border black - // equivalent to RGB(0,0,0) in YUV space - memset(y, 16, width * height); - memset(u, 128, (width / 2) * (height / 2)); - memset(v, 128, (width / 2) * (height / 2)); -} - void dmonitoring_init(DMonitoringModelState* s) { s->is_rhd = Params().getBool("IsRHD"); - for (int x = 0; x < std::size(s->tensor); ++x) { - s->tensor[x] = (x - 128.f) * 0.0078125f; - } - init_yuv_buf(s->resized_buf, MODEL_WIDTH, MODEL_HEIGHT); #ifdef USE_ONNX_MODEL - s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); + s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); #else - s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME); + s->m = new SNPEModel("models/dmonitoring_model_q.dlc", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); #endif s->m->addCalib(s->calib, CALIB_LEN); } -static inline auto get_yuv_buf(std::vector &buf, const int width, int height) { - uint8_t *y = get_buffer(buf, width * height * 3 / 2); - uint8_t *u = y + width * height; - uint8_t *v = u + (width /2) * (height / 2); - return std::make_tuple(y, u, v); +void parse_driver_data(DriverStateResult &ds_res, const DMonitoringModelState* s, int out_idx_offset) { + for (int i = 0; i < 3; ++i) { + ds_res.face_orientation[i] = s->output[out_idx_offset+i] * REG_SCALE; + ds_res.face_orientation_std[i] = exp(s->output[out_idx_offset+6+i]); + } + for (int i = 0; i < 2; ++i) { + ds_res.face_position[i] = s->output[out_idx_offset+3+i] * REG_SCALE; + ds_res.face_position_std[i] = exp(s->output[out_idx_offset+9+i]); + } + for (int i = 0; i < 4; ++i) { + ds_res.ready_prob[i] = sigmoid(s->output[out_idx_offset+35+i]); + } + for (int i = 0; i < 2; ++i) { + ds_res.not_ready_prob[i] = sigmoid(s->output[out_idx_offset+39+i]); + } + ds_res.face_prob = sigmoid(s->output[out_idx_offset+12]); + ds_res.left_eye_prob = sigmoid(s->output[out_idx_offset+21]); + ds_res.right_eye_prob = sigmoid(s->output[out_idx_offset+30]); + ds_res.left_blink_prob = sigmoid(s->output[out_idx_offset+31]); + ds_res.right_blink_prob = sigmoid(s->output[out_idx_offset+32]); + ds_res.sunglasses_prob = sigmoid(s->output[out_idx_offset+33]); + ds_res.occluded_prob = sigmoid(s->output[out_idx_offset+34]); } -struct Rect {int x, y, w, h;}; -void crop_nv12_to_yuv(uint8_t *raw, int stride, int uv_offset, uint8_t *y, uint8_t *u, uint8_t *v, const Rect &rect) { - uint8_t *raw_y = raw; - uint8_t *raw_uv = raw_y + uv_offset; - for (int r = 0; r < rect.h / 2; r++) { - memcpy(y + 2 * r * rect.w, raw_y + (2 * r + rect.y) * stride + rect.x, rect.w); - memcpy(y + (2 * r + 1) * rect.w, raw_y + (2 * r + rect.y + 1) * stride + rect.x, rect.w); - for (int h = 0; h < rect.w / 2; h++) { - u[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2]; - v[r * rect.w/2 + h] = raw_uv[(r + (rect.y/2)) * stride + (rect.x/2 + h)*2 + 1]; - } - } +void fill_driver_data(cereal::DriverStateV2::DriverData::Builder ddata, const DriverStateResult &ds_res) { + ddata.setFaceOrientation(ds_res.face_orientation); + ddata.setFaceOrientationStd(ds_res.face_orientation_std); + ddata.setFacePosition(ds_res.face_position); + ddata.setFacePositionStd(ds_res.face_position_std); + ddata.setFaceProb(ds_res.face_prob); + ddata.setLeftEyeProb(ds_res.left_eye_prob); + ddata.setRightEyeProb(ds_res.right_eye_prob); + ddata.setLeftBlinkProb(ds_res.left_blink_prob); + ddata.setRightBlinkProb(ds_res.right_blink_prob); + ddata.setSunglassesProb(ds_res.sunglasses_prob); + ddata.setOccludedProb(ds_res.occluded_prob); + ddata.setReadyProb(ds_res.ready_prob); + ddata.setNotReadyProb(ds_res.not_ready_prob); } -DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { - const int cropped_height = tici_dm_crop::width / 1.33; - Rect crop_rect = {width / 2 - tici_dm_crop::width / 2 + tici_dm_crop::x_offset, - height / 2 - cropped_height / 2 + tici_dm_crop::y_offset, - cropped_height / 2, - cropped_height}; - if (!s->is_rhd) { - crop_rect.x += tici_dm_crop::width - crop_rect.w; - } +DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib) { + int v_off = height - MODEL_HEIGHT; + int h_off = (width - MODEL_WIDTH) / 2; + int yuv_buf_len = MODEL_WIDTH * MODEL_HEIGHT; - int resized_width = MODEL_WIDTH; - int resized_height = MODEL_HEIGHT; - - auto [cropped_y, cropped_u, cropped_v] = get_yuv_buf(s->cropped_buf, crop_rect.w, crop_rect.h); - if (!s->is_rhd) { - crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, cropped_y, cropped_u, cropped_v, crop_rect); - } else { - auto [mirror_y, mirror_u, mirror_v] = get_yuv_buf(s->premirror_cropped_buf, crop_rect.w, crop_rect.h); - crop_nv12_to_yuv((uint8_t *)stream_buf, stride, uv_offset, mirror_y, mirror_u, mirror_v, crop_rect); - libyuv::I420Mirror(mirror_y, crop_rect.w, - mirror_u, crop_rect.w / 2, - mirror_v, crop_rect.w / 2, - cropped_y, crop_rect.w, - cropped_u, crop_rect.w / 2, - cropped_v, crop_rect.w / 2, - crop_rect.w, crop_rect.h); - } + uint8_t *raw_buf = (uint8_t *) stream_buf; + // vertical crop free + uint8_t *raw_y_start = raw_buf + stride * v_off; - auto [resized_buf, resized_u, resized_v] = get_yuv_buf(s->resized_buf, resized_width, resized_height); - uint8_t *resized_y = resized_buf; - libyuv::FilterMode mode = libyuv::FilterModeEnum::kFilterBilinear; - libyuv::I420Scale(cropped_y, crop_rect.w, - cropped_u, crop_rect.w / 2, - cropped_v, crop_rect.w / 2, - crop_rect.w, crop_rect.h, - resized_y, resized_width, - resized_u, resized_width / 2, - resized_v, resized_width / 2, - resized_width, resized_height, - mode); - - - int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v - float *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); - // one shot conversion, O(n) anyway - // yuvframe2tensor, normalize - for (int r = 0; r < MODEL_HEIGHT/2; r++) { - for (int c = 0; c < MODEL_WIDTH/2; c++) { - // Y_ul - net_input_buf[(r*MODEL_WIDTH/2) + c + (0*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c]]; - // Y_dl - net_input_buf[(r*MODEL_WIDTH/2) + c + (1*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c]]; - // Y_ur - net_input_buf[(r*MODEL_WIDTH/2) + c + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r)*resized_width + 2*c+1]]; - // Y_dr - net_input_buf[(r*MODEL_WIDTH/2) + c + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_y[(2*r+1)*resized_width + 2*c+1]]; - // U - net_input_buf[(r*MODEL_WIDTH/2) + c + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_u[r*resized_width/2 + c]]; - // V - net_input_buf[(r*MODEL_WIDTH/2) + c + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = s->tensor[resized_v[r*resized_width/2 + c]]; - } - } - - //printf("preprocess completed. %d \n", yuv_buf_len); - //FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); - //fwrite(resized_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); - //fclose(dump_yuv_file); + uint8_t *net_input_buf = get_buffer(s->net_input_buf, yuv_buf_len); - // *** testing *** - // idat = np.frombuffer(open("/tmp/inputdump.yuv", "rb").read(), np.float32).reshape(6, 160, 320) - // imshow(cv2.cvtColor(tensor_to_frames(idat[None]/0.0078125+128)[0], cv2.COLOR_YUV2RGB_I420)) + // here makes a uint8 copy + for (int r = 0; r < MODEL_HEIGHT; ++r) { + memcpy(net_input_buf + r * MODEL_WIDTH, raw_y_start + r * stride + h_off, MODEL_WIDTH); + } - //FILE *dump_yuv_file2 = fopen("/tmp/inputdump.yuv", "wb"); - //fwrite(net_input_buf, MODEL_HEIGHT*MODEL_WIDTH*3/2, sizeof(float), dump_yuv_file2); - //fclose(dump_yuv_file2); + // printf("preprocess completed. %d \n", yuv_buf_len); + // FILE *dump_yuv_file = fopen("/tmp/rawdump.yuv", "wb"); + // fwrite(net_input_buf, yuv_buf_len, sizeof(uint8_t), dump_yuv_file); + // fclose(dump_yuv_file); double t1 = millis_since_boot(); - s->m->addImage(net_input_buf, yuv_buf_len); + s->m->addImage((float*)net_input_buf, yuv_buf_len / 4); for (int i = 0; i < CALIB_LEN; i++) { s->calib[i] = calib[i]; } s->m->execute(); double t2 = millis_since_boot(); - DMonitoringResult ret = {0}; - for (int i = 0; i < 3; ++i) { - ret.face_orientation[i] = s->output[i] * REG_SCALE; - ret.face_orientation_meta[i] = exp(s->output[6 + i]); - } - for (int i = 0; i < 2; ++i) { - ret.face_position[i] = s->output[3 + i] * REG_SCALE; - ret.face_position_meta[i] = exp(s->output[9 + i]); - } - for (int i = 0; i < 4; ++i) { - ret.ready_prob[i] = sigmoid(s->output[39 + i]); - } - for (int i = 0; i < 2; ++i) { - ret.not_ready_prob[i] = sigmoid(s->output[43 + i]); - } - ret.face_prob = sigmoid(s->output[12]); - ret.left_eye_prob = sigmoid(s->output[21]); - ret.right_eye_prob = sigmoid(s->output[30]); - ret.left_blink_prob = sigmoid(s->output[31]); - ret.right_blink_prob = sigmoid(s->output[32]); - ret.sg_prob = sigmoid(s->output[33]); - ret.poor_vision = sigmoid(s->output[34]); - ret.partial_face = sigmoid(s->output[35]); - ret.distracted_pose = sigmoid(s->output[36]); - ret.distracted_eyes = sigmoid(s->output[37]); - ret.occluded_prob = sigmoid(s->output[38]); - ret.dsp_execution_time = (t2 - t1) / 1000.; - return ret; + DMonitoringModelResult model_res = {0}; + parse_driver_data(model_res.driver_state_lhd, s, 0); + parse_driver_data(model_res.driver_state_rhd, s, 41); + model_res.poor_vision_prob = sigmoid(s->output[82]); + model_res.wheel_on_right_prob = sigmoid(s->output[83]); + model_res.dsp_execution_time = (t2 - t1) / 1000.; + + return model_res; } -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred) { +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred) { // make msg MessageBuilder msg; - auto framed = msg.initEvent().initDriverState(); + auto framed = msg.initEvent().initDriverStateV2(); framed.setFrameId(frame_id); framed.setModelExecutionTime(execution_time); - framed.setDspExecutionTime(res.dsp_execution_time); - - framed.setFaceOrientation(res.face_orientation); - framed.setFaceOrientationStd(res.face_orientation_meta); - framed.setFacePosition(res.face_position); - framed.setFacePositionStd(res.face_position_meta); - framed.setFaceProb(res.face_prob); - framed.setLeftEyeProb(res.left_eye_prob); - framed.setRightEyeProb(res.right_eye_prob); - framed.setLeftBlinkProb(res.left_blink_prob); - framed.setRightBlinkProb(res.right_blink_prob); - framed.setSunglassesProb(res.sg_prob); - framed.setPoorVision(res.poor_vision); - framed.setPartialFace(res.partial_face); - framed.setDistractedPose(res.distracted_pose); - framed.setDistractedEyes(res.distracted_eyes); - framed.setOccludedProb(res.occluded_prob); - framed.setReadyProb(res.ready_prob); - framed.setNotReadyProb(res.not_ready_prob); + framed.setDspExecutionTime(model_res.dsp_execution_time); + + framed.setPoorVisionProb(model_res.poor_vision_prob); + framed.setWheelOnRightProb(model_res.wheel_on_right_prob); + fill_driver_data(framed.initLeftDriverData(), model_res.driver_state_lhd); + fill_driver_data(framed.initRightDriverData(), model_res.driver_state_rhd); + if (send_raw_pred) { framed.setRawPredictions(raw_pred.asBytes()); } - pm.send("driverState", msg); + pm.send("driverStateV2", msg); } void dmonitoring_free(DMonitoringModelState* s) { diff --git a/selfdrive/modeld/models/dmonitoring.h b/selfdrive/modeld/models/dmonitoring.h index a1be91e3bb20a8..874722cd93fb88 100644 --- a/selfdrive/modeld/models/dmonitoring.h +++ b/selfdrive/modeld/models/dmonitoring.h @@ -9,44 +9,43 @@ #define CALIB_LEN 3 -#define OUTPUT_SIZE 45 +#define OUTPUT_SIZE 84 #define REG_SCALE 0.25f -typedef struct DMonitoringResult { +typedef struct DriverStateResult { float face_orientation[3]; - float face_orientation_meta[3]; + float face_orientation_std[3]; float face_position[2]; - float face_position_meta[2]; + float face_position_std[2]; float face_prob; float left_eye_prob; float right_eye_prob; float left_blink_prob; float right_blink_prob; - float sg_prob; - float poor_vision; - float partial_face; - float distracted_pose; - float distracted_eyes; + float sunglasses_prob; float occluded_prob; float ready_prob[4]; float not_ready_prob[2]; +} DriverStateResult; + +typedef struct DMonitoringModelResult { + DriverStateResult driver_state_lhd; + DriverStateResult driver_state_rhd; + float poor_vision_prob; + float wheel_on_right_prob; float dsp_execution_time; -} DMonitoringResult; +} DMonitoringModelResult; typedef struct DMonitoringModelState { RunModel *m; bool is_rhd; float output[OUTPUT_SIZE]; - std::vector resized_buf; - std::vector cropped_buf; - std::vector premirror_cropped_buf; - std::vector net_input_buf; + std::vector net_input_buf; float calib[CALIB_LEN]; - float tensor[UINT8_MAX + 1]; } DMonitoringModelState; void dmonitoring_init(DMonitoringModelState* s); -DMonitoringResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); -void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringResult &res, float execution_time, kj::ArrayPtr raw_pred); +DMonitoringModelResult dmonitoring_eval_frame(DMonitoringModelState* s, void* stream_buf, int width, int height, int stride, int uv_offset, float *calib); +void dmonitoring_publish(PubMaster &pm, uint32_t frame_id, const DMonitoringModelResult &model_res, float execution_time, kj::ArrayPtr raw_pred); void dmonitoring_free(DMonitoringModelState* s); diff --git a/selfdrive/modeld/models/dmonitoring_model.current b/selfdrive/modeld/models/dmonitoring_model.current index 74bcfe17a4ebe4..d1e7d1136fa1e4 100644 --- a/selfdrive/modeld/models/dmonitoring_model.current +++ b/selfdrive/modeld/models/dmonitoring_model.current @@ -1,2 +1,2 @@ -a8236e30-5bee-4689-8ea0-fc102e2770e5 -d508c79bae1c1c451f3af3e2bc231ce33678cb43 \ No newline at end of file +ee8f830b-d6a1-42ef-9b1b-50fd0b2faae4 +cac8f7b69d420506707ff7a19d573d5011ef2533 \ No newline at end of file diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 51b0d1ed760980..4cbd6bb7dd64b1 100644 --- a/selfdrive/modeld/models/dmonitoring_model.onnx +++ b/selfdrive/modeld/models/dmonitoring_model.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00731ebd06fcff7e5837607b91bc56cad3bed5d7ee89052c911c981e8f665308 -size 3679940 +oid sha256:932e589e5cce66e5d9f48492426a33c74cd7f352a870d3ddafcede3e9156f30d +size 9157561 diff --git a/selfdrive/modeld/models/dmonitoring_model_q.dlc b/selfdrive/modeld/models/dmonitoring_model_q.dlc index 2e54f7ee4b098e..94632030edb80d 100644 --- a/selfdrive/modeld/models/dmonitoring_model_q.dlc +++ b/selfdrive/modeld/models/dmonitoring_model_q.dlc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:667df5e925570a0f6a33dfb890e186a1f13f101885b46db47ec45305737dffb6 -size 1145921 +oid sha256:3587976a8b7e3be274fa86c2e2233e3e464cad713f5077c4394cd1ddd3c7c6c5 +size 2636965 diff --git a/selfdrive/modeld/runners/onnx_runner.py b/selfdrive/modeld/runners/onnx_runner.py index e282a66b66a8d0..ac7cc68814df22 100755 --- a/selfdrive/modeld/runners/onnx_runner.py +++ b/selfdrive/modeld/runners/onnx_runner.py @@ -9,20 +9,24 @@ import onnxruntime as ort # pylint: disable=import-error -def read(sz): +def read(sz, tf8=False): dd = [] gt = 0 - while gt < sz * 4: - st = os.read(0, sz * 4 - gt) + szof = 1 if tf8 else 4 + while gt < sz * szof: + st = os.read(0, sz * szof - gt) assert(len(st) > 0) dd.append(st) gt += len(st) - return np.frombuffer(b''.join(dd), dtype=np.float32) + r = np.frombuffer(b''.join(dd), dtype=np.uint8 if tf8 else np.float32).astype(np.float32) + if tf8: + r = r / 255. + return r def write(d): os.write(1, d.tobytes()) -def run_loop(m): +def run_loop(m, tf8_input=False): ishapes = [[1]+ii.shape[1:] for ii in m.get_inputs()] keys = [x.name for x in m.get_inputs()] @@ -33,10 +37,10 @@ def run_loop(m): print("ready to run onnx model", keys, ishapes, file=sys.stderr) while 1: inputs = [] - for shp in ishapes: + for k, shp in zip(keys, ishapes): ts = np.product(shp) #print("reshaping %s with offset %d" % (str(shp), offset), file=sys.stderr) - inputs.append(read(ts).reshape(shp)) + inputs.append(read(ts, (k=='input_img' and tf8_input)).reshape(shp)) ret = m.run(None, dict(zip(keys, inputs))) #print(ret, file=sys.stderr) for r in ret: @@ -44,6 +48,7 @@ def run_loop(m): if __name__ == "__main__": + print(sys.argv, file=sys.stderr) print("Onnx available providers: ", ort.get_available_providers(), file=sys.stderr) options = ort.SessionOptions() options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL @@ -63,6 +68,6 @@ def run_loop(m): print("Onnx selected provider: ", [provider], file=sys.stderr) ort_session = ort.InferenceSession(sys.argv[1], options, providers=[provider]) print("Onnx using ", ort_session.get_providers(), file=sys.stderr) - run_loop(ort_session) + run_loop(ort_session, tf8_input=("--use_tf8" in sys.argv)) except KeyboardInterrupt: pass diff --git a/selfdrive/modeld/runners/onnxmodel.cc b/selfdrive/modeld/runners/onnxmodel.cc index 9b4d6fd0152b95..1f9f551abc5351 100644 --- a/selfdrive/modeld/runners/onnxmodel.cc +++ b/selfdrive/modeld/runners/onnxmodel.cc @@ -14,12 +14,13 @@ #include "common/swaglog.h" #include "common/util.h" -ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra) { +ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int runtime, bool _use_extra, bool _use_tf8) { LOGD("loading model %s", path); output = _output; output_size = _output_size; use_extra = _use_extra; + use_tf8 = _use_tf8; int err = pipe(pipein); assert(err == 0); @@ -28,11 +29,12 @@ ONNXModel::ONNXModel(const char *path, float *_output, size_t _output_size, int std::string exe_dir = util::dir_name(util::readlink("/proc/self/exe")); std::string onnx_runner = exe_dir + "/runners/onnx_runner.py"; + std::string tf8_arg = use_tf8 ? "--use_tf8" : ""; proc_pid = fork(); if (proc_pid == 0) { LOGD("spawning onnx process %s", onnx_runner.c_str()); - char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, nullptr}; + char *argv[] = {(char*)onnx_runner.c_str(), (char*)path, (char*)tf8_arg.c_str(), nullptr}; dup2(pipein[0], 0); dup2(pipeout[1], 1); close(pipein[0]); diff --git a/selfdrive/modeld/runners/onnxmodel.h b/selfdrive/modeld/runners/onnxmodel.h index 567d81d29ef8f7..4ac599e2afbbf0 100644 --- a/selfdrive/modeld/runners/onnxmodel.h +++ b/selfdrive/modeld/runners/onnxmodel.h @@ -6,7 +6,7 @@ class ONNXModel : public RunModel { public: - ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false); + ONNXModel(const char *path, float *output, size_t output_size, int runtime, bool use_extra = false, bool _use_tf8 = false); ~ONNXModel(); void addRecurrent(float *state, int state_size); void addDesire(float *state, int state_size); @@ -31,6 +31,7 @@ class ONNXModel : public RunModel { int calib_size; float *image_input_buf = NULL; int image_buf_size; + bool use_tf8; float *extra_input_buf = NULL; int extra_buf_size; bool use_extra; diff --git a/selfdrive/modeld/runners/snpemodel.cc b/selfdrive/modeld/runners/snpemodel.cc index 1861494d591e3b..4d6917e894c539 100644 --- a/selfdrive/modeld/runners/snpemodel.cc +++ b/selfdrive/modeld/runners/snpemodel.cc @@ -14,10 +14,11 @@ void PrintErrorStringAndExit() { std::exit(EXIT_FAILURE); } -SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra) { +SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra, bool luse_tf8) { output = loutput; output_size = loutput_size; use_extra = luse_extra; + use_tf8 = luse_tf8; #ifdef QCOM2 if (runtime==USE_GPU_RUNTIME) { Runtime = zdl::DlSystem::Runtime_t::GPU; @@ -70,14 +71,16 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int printf("model: %s -> %s\n", input_tensor_name, output_tensor_name); zdl::DlSystem::UserBufferEncodingFloat userBufferEncodingFloat; + zdl::DlSystem::UserBufferEncodingTf8 userBufferEncodingTf8(0, 1./255); // network takes 0-1 zdl::DlSystem::IUserBufferFactory& ubFactory = zdl::SNPE::SNPEFactory::getUserBufferFactory(); + size_t size_of_input = use_tf8 ? sizeof(uint8_t) : sizeof(float); // create input buffer { const auto &inputDims_opt = snpe->getInputDimensions(input_tensor_name); const zdl::DlSystem::TensorShape& bufferShape = *inputDims_opt; std::vector strides(bufferShape.rank()); - strides[strides.size() - 1] = sizeof(float); + strides[strides.size() - 1] = size_of_input; size_t product = 1; for (size_t i = 0; i < bufferShape.rank(); i++) product *= bufferShape[i]; size_t stride = strides[strides.size() - 1]; @@ -86,7 +89,10 @@ SNPEModel::SNPEModel(const char *path, float *loutput, size_t loutput_size, int strides[i-1] = stride; } printf("input product is %lu\n", product); - inputBuffer = ubFactory.createUserBuffer(NULL, product*sizeof(float), strides, &userBufferEncodingFloat); + inputBuffer = ubFactory.createUserBuffer(NULL, + product*size_of_input, + strides, + use_tf8 ? (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingTf8 : (zdl::DlSystem::UserBufferEncoding*)&userBufferEncodingFloat); inputMap.add(input_tensor_name, inputBuffer.get()); } diff --git a/selfdrive/modeld/runners/snpemodel.h b/selfdrive/modeld/runners/snpemodel.h index ba51fdced079f6..ed9d58d1e11335 100644 --- a/selfdrive/modeld/runners/snpemodel.h +++ b/selfdrive/modeld/runners/snpemodel.h @@ -23,7 +23,7 @@ class SNPEModel : public RunModel { public: - SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false); + SNPEModel(const char *path, float *loutput, size_t loutput_size, int runtime, bool luse_extra = false, bool use_tf8 = false); void addRecurrent(float *state, int state_size); void addTrafficConvention(float *state, int state_size); void addCalib(float *state, int state_size); @@ -52,6 +52,7 @@ class SNPEModel : public RunModel { std::unique_ptr inputBuffer; float *input; size_t input_size; + bool use_tf8; // snpe output stuff zdl::DlSystem::UserBufferMap outputMap; diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index 9a3196030ba643..c31e9da43b9f20 100755 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -18,7 +18,7 @@ def dmonitoringd_thread(sm=None, pm=None): pm = messaging.PubMaster(['driverMonitoringState']) if sm is None: - sm = messaging.SubMaster(['driverState', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverState']) + sm = messaging.SubMaster(['driverStateV2', 'liveCalibration', 'carState', 'controlsState', 'modelV2'], poll=['driverStateV2']) driver_status = DriverStatus(rhd=Params().get_bool("IsRHD")) @@ -34,7 +34,7 @@ def dmonitoringd_thread(sm=None, pm=None): while True: sm.update() - if not sm.updated['driverState']: + if not sm.updated['driverStateV2']: continue # Get interaction @@ -51,7 +51,7 @@ def dmonitoringd_thread(sm=None, pm=None): # Get data from dmonitoringmodeld events = Events() - driver_status.update_states(sm['driverState'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) + driver_status.update_states(sm['driverStateV2'], sm['liveCalibration'].rpyCalib, sm['carState'].vEgo, sm['controlsState'].enabled) # Block engaging after max number of distrations if driver_status.terminal_alert_cnt >= driver_status.settings._MAX_TERMINAL_ALERTS or \ @@ -79,6 +79,7 @@ def dmonitoringd_thread(sm=None, pm=None): "isLowStd": driver_status.pose.low_std, "hiStdCount": driver_status.hi_stds, "isActiveMode": driver_status.active_monitoring_mode, + "isRHD": driver_status.wheel_on_right, } pm.send('driverMonitoringState', dat) diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index 662e0d76ce7167..f2d0524422d123 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -5,6 +5,7 @@ from common.realtime import DT_DMON from common.filter_simple import FirstOrderFilter from common.stat_live import RunningStatFilter +from common.transformations.camera import tici_d_frame_size EventName = car.CarEvent.EventName @@ -25,33 +26,30 @@ def __init__(self): self._DISTRACTED_PRE_TIME_TILL_TERMINAL = 8. self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6. - self._FACE_THRESHOLD = 0.5 - self._PARTIAL_FACE_THRESHOLD = 0.8 + self._FACE_THRESHOLD = 0.7 self._EYE_THRESHOLD = 0.65 - self._SG_THRESHOLD = 0.925 - self._BLINK_THRESHOLD = 0.8 - self._BLINK_THRESHOLD_SLACK = 0.9 - self._BLINK_THRESHOLD_STRICT = self._BLINK_THRESHOLD + self._SG_THRESHOLD = 0.9 + self._BLINK_THRESHOLD = 0.87 self._EE_THRESH11 = 0.75 self._EE_THRESH12 = 3.25 self._EE_THRESH21 = 0.01 self._EE_THRESH22 = 0.35 - self._POSE_PITCH_THRESHOLD = 0.3237 - self._POSE_PITCH_THRESHOLD_SLACK = 0.3657 + self._POSE_PITCH_THRESHOLD = 0.3133 + self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 self._POSE_PITCH_THRESHOLD_STRICT = self._POSE_PITCH_THRESHOLD - self._POSE_YAW_THRESHOLD = 0.3109 - self._POSE_YAW_THRESHOLD_SLACK = 0.4294 + self._POSE_YAW_THRESHOLD = 0.4020 + self._POSE_YAW_THRESHOLD_SLACK = 0.5042 self._POSE_YAW_THRESHOLD_STRICT = self._POSE_YAW_THRESHOLD - self._PITCH_NATURAL_OFFSET = 0.057 # initial value before offset is learned - self._YAW_NATURAL_OFFSET = 0.11 # initial value before offset is learned + self._PITCH_NATURAL_OFFSET = 0.029 # initial value before offset is learned + self._YAW_NATURAL_OFFSET = 0.097 # initial value before offset is learned self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 self._YAW_MIN_OFFSET = -0.0246 - self._POSESTD_THRESHOLD = 0.315 + self._POSESTD_THRESHOLD = 0.3 self._HI_STD_FALLBACK_TIME = int(10 / self._DT_DMON) # fall back to wheel touch if model is uncertain for 10s self._DISTRACTED_FILTER_TS = 0.25 # 0.6Hz @@ -59,6 +57,9 @@ def __init__(self): self._POSE_OFFSET_MIN_COUNT = int(60 / self._DT_DMON) # valid data counts before calibration completes, 1min cumulative self._POSE_OFFSET_MAX_COUNT = int(360 / self._DT_DMON) # stop deweighting new data after 6 min, aka "short term memory" + self._WHEELPOS_THRESHOLD = 0.5 + self._WHEELPOS_FILTER_MIN_COUNT = int(5 / self._DT_DMON) + self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change @@ -66,9 +67,9 @@ def __init__(self): self._MAX_TERMINAL_DURATION = int(30 / self._DT_DMON) # not allowed to engage after 30s of terminal alerts -# model output refers to center of cropped image, so need to apply the x displacement offset -RESIZED_FOCAL = 320.0 -H, W, FULL_W = 320, 160, 426 +# model output refers to center of undistorted+leveled image +EFL = 598.0 # focal length in K +W, H = tici_d_frame_size # corrected image has same size as raw class DistractedType: NOT_DISTRACTED = 0 @@ -76,22 +77,22 @@ class DistractedType: DISTRACTED_BLINK = 2 DISTRACTED_E2E = 4 -def face_orientation_from_net(angles_desc, pos_desc, rpy_calib, is_rhd): +def face_orientation_from_net(angles_desc, pos_desc, rpy_calib): # the output of these angles are in device frame # so from driver's perspective, pitch is up and yaw is right pitch_net, yaw_net, roll_net = angles_desc - face_pixel_position = ((pos_desc[0] + .5)*W - W + FULL_W, (pos_desc[1]+.5)*H) - yaw_focal_angle = atan2(face_pixel_position[0] - FULL_W//2, RESIZED_FOCAL) - pitch_focal_angle = atan2(face_pixel_position[1] - H//2, RESIZED_FOCAL) + face_pixel_position = ((pos_desc[0]+0.5)*W, (pos_desc[1]+0.5)*H) + yaw_focal_angle = atan2(face_pixel_position[0] - W//2, EFL) + pitch_focal_angle = atan2(face_pixel_position[1] - H//2, EFL) pitch = pitch_net + pitch_focal_angle yaw = -yaw_net + yaw_focal_angle # no calib for roll pitch -= rpy_calib[1] - yaw -= rpy_calib[2] * (1 - 2 * int(is_rhd)) # lhd -> -=, rhd -> += + yaw -= rpy_calib[2] return roll_net, pitch, yaw class DriverPose(): @@ -112,7 +113,6 @@ class DriverBlink(): def __init__(self): self.left_blink = 0. self.right_blink = 0. - self.cfactor = 1. class DriverStatus(): def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): @@ -120,7 +120,7 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.settings = settings # init driver status - self.is_rhd_region = rhd + # self.wheelpos_learner = RunningStatFilter() self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) self.pose_calibrated = False self.blink = DriverBlink() @@ -137,8 +137,8 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.distracted_types = [] self.driver_distracted = False self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON) + self.wheel_on_right = rhd self.face_detected = False - self.face_partial = False self.terminal_alert_cnt = 0 self.terminal_time = 0 self.step_change = 0. @@ -197,7 +197,7 @@ def _get_distracted_types(self): yaw_error > self.settings._POSE_YAW_THRESHOLD*self.pose.cfactor_yaw: distracted_types.append(DistractedType.DISTRACTED_POSE) - if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD*self.blink.cfactor: + if (self.blink.left_blink + self.blink.right_blink)*0.5 > self.settings._BLINK_THRESHOLD: distracted_types.append(DistractedType.DISTRACTED_BLINK) if self.ee1_calibrated: @@ -214,13 +214,7 @@ def _get_distracted_types(self): return distracted_types def set_policy(self, model_data, car_speed): - ep = min(model_data.meta.engagedProb, 0.8) / 0.8 # engaged prob bp = model_data.meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s - # TODO: retune adaptive blink - self.blink.cfactor = interp(ep, [0, 0.5, 1], - [self.settings._BLINK_THRESHOLD_STRICT, - self.settings._BLINK_THRESHOLD, - self.settings._BLINK_THRESHOLD_SLACK]) / self.settings._BLINK_THRESHOLD k1 = max(-0.00156*((car_speed-16)**2)+0.6, 0.2) bp_normal = max(min(bp / k1, 0.5),0) self.pose.cfactor_pitch = interp(bp_normal, [0, 0.5], @@ -231,28 +225,36 @@ def set_policy(self, model_data, car_speed): self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD def update_states(self, driver_state, cal_rpy, car_speed, op_engaged): - if not all(len(x) > 0 for x in (driver_state.faceOrientation, driver_state.facePosition, - driver_state.faceOrientationStd, driver_state.facePositionStd, - driver_state.readyProb, driver_state.notReadyProb)): + # rhd_pred = driver_state.wheelOnRightProb + # if car_speed > 0.01: + # self.wheelpos_learner.push_and_update(rhd_pred) + # if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT: + # self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD + # else: + # self.wheel_on_right = rhd_pred > self.settings._WHEELPOS_THRESHOLD + driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData + if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition, + driver_data.faceOrientationStd, driver_data.facePositionStd, + driver_data.readyProb, driver_data.notReadyProb)): return - self.face_partial = driver_state.partialFace > self.settings._PARTIAL_FACE_THRESHOLD - self.face_detected = driver_state.faceProb > self.settings._FACE_THRESHOLD or self.face_partial - self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_state.faceOrientation, driver_state.facePosition, cal_rpy, self.is_rhd_region) - self.pose.pitch_std = driver_state.faceOrientationStd[0] - self.pose.yaw_std = driver_state.faceOrientationStd[1] - # self.pose.roll_std = driver_state.faceOrientationStd[2] + self.face_detected = driver_data.faceProb > self.settings._FACE_THRESHOLD + self.pose.roll, self.pose.pitch, self.pose.yaw = face_orientation_from_net(driver_data.faceOrientation, driver_data.facePosition, cal_rpy) + if self.wheel_on_right: + self.pose.yaw *= -1 + self.pose.pitch_std = driver_data.faceOrientationStd[0] + self.pose.yaw_std = driver_data.faceOrientationStd[1] model_std_max = max(self.pose.pitch_std, self.pose.yaw_std) - self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD and not self.face_partial - self.blink.left_blink = driver_state.leftBlinkProb * (driver_state.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) - self.blink.right_blink = driver_state.rightBlinkProb * (driver_state.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_state.sunglassesProb < self.settings._SG_THRESHOLD) - self.eev1 = driver_state.notReadyProb[1] - self.eev2 = driver_state.readyProb[0] + self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD + self.blink.left_blink = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) + self.blink.right_blink = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) + self.eev1 = driver_data.notReadyProb[1] + self.eev2 = driver_data.readyProb[0] self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_POSE in self.distracted_types or DistractedType.DISTRACTED_BLINK in self.distracted_types) and \ - driver_state.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std + driver_data.faceProb > self.settings._FACE_THRESHOLD and self.pose.low_std self.driver_distraction_filter.update(self.driver_distracted) # update offseter diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index a84ed242ba0a68..43b5e7747e66ba 100755 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -17,19 +17,19 @@ INVISIBLE_SECONDS_TO_RED = dm_settings._AWARENESS_TIME + 1 def make_msg(face_detected, distracted=False, model_uncertain=False): - ds = log.DriverState.new_message() - ds.faceOrientation = [0., 0., 0.] - ds.facePosition = [0., 0.] - ds.faceProb = 1. * face_detected - ds.leftEyeProb = 1. - ds.rightEyeProb = 1. - ds.leftBlinkProb = 1. * distracted - ds.rightBlinkProb = 1. * distracted - ds.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] - ds.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] + ds = log.DriverStateV2.new_message() + ds.leftDriverData.faceOrientation = [0., 0., 0.] + ds.leftDriverData.facePosition = [0., 0.] + ds.leftDriverData.faceProb = 1. * face_detected + ds.leftDriverData.leftEyeProb = 1. + ds.leftDriverData.rightEyeProb = 1. + ds.leftDriverData.leftBlinkProb = 1. * distracted + ds.leftDriverData.rightBlinkProb = 1. * distracted + ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] + ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] # TODO: test both separately when e2e is used - ds.readyProb = [0., 0., 0., 0.] - ds.notReadyProb = [0., 0.] + ds.leftDriverData.readyProb = [0., 0., 0., 0.] + ds.leftDriverData.notReadyProb = [0., 0.] return ds diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index f3bb28663537b2..bccad5d92cbc88 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -52,7 +52,7 @@ def model_replay(lr, frs): vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, False, *(tici_f_frame_size)) vipc_server.start_listener() - sm = messaging.SubMaster(['modelV2', 'driverState']) + sm = messaging.SubMaster(['modelV2', 'driverStateV2']) pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'driverCameraState', 'liveCalibration', 'lateralPlan']) try: @@ -112,7 +112,7 @@ def model_replay(lr, frs): if min(frame_idxs['roadCameraState'], frame_idxs['wideRoadCameraState']) > recv_cnt['modelV2']: recv = "modelV2" elif msg.which() == 'driverCameraState': - recv = "driverState" + recv = "driverStateV2" # wait for a response with Timeout(15, f"timed out waiting for {recv}"): @@ -170,8 +170,8 @@ def model_replay(lr, frs): 'logMonoTime', 'modelV2.frameDropPerc', 'modelV2.modelExecutionTime', - 'driverState.modelExecutionTime', - 'driverState.dspExecutionTime' + 'driverStateV2.modelExecutionTime', + 'driverStateV2.dspExecutionTime' ] # TODO this tolerence is absurdly large tolerance = 5e-1 if PC else None diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 54c2ed54ad877f..0c4c2305ebc292 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -629eaa7b26d1721a71547f9de880f99732cb27f3 +5434b3c1696554e9a889e77f794d80cd1cb0a7ec diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index c368e6b575917b..62c5eb4826ac53 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -293,7 +293,7 @@ def ublox_rcv_callback(msg): ProcessConfig( proc_name="dmonitoringd", pub_sub={ - "driverState": ["driverMonitoringState"], + "driverStateV2": ["driverMonitoringState"], "liveCalibration": [], "carState": [], "modelV2": [], "controlsState": [], }, ignore=["logMonoTime", "valid"], diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 3586f86f12e749..f10c79313b124c 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -33,7 +33,7 @@ "selfdrive.controls.radard": 4.5, "./_modeld": 4.48, "./boardd": 3.63, - "./_dmonitoringmodeld": 10.0, + "./_dmonitoringmodeld": 5.0, "selfdrive.thermald.thermald": 3.87, "selfdrive.locationd.calibrationd": 2.0, "./_soundd": 1.0, @@ -60,7 +60,7 @@ "roadCameraState": [2.5, 0.35], "driverCameraState": [2.5, 0.35], "modelV2": [2.5, 0.35], - "driverState": [2.5, 0.40], + "driverStateV2": [2.5, 0.40], "liveLocationKalman": [2.5, 0.35], "wideRoadCameraState": [1.5, 0.35], } @@ -221,7 +221,7 @@ def test_model_execution_timings(self): # TODO: this went up when plannerd cpu usage increased, why? cfgs = [ ("modelV2", 0.050, 0.036), - ("driverState", 0.050, 0.026), + ("driverStateV2", 0.050, 0.026), ] for (s, instant_max, avg_max) in cfgs: ts = [getattr(getattr(m, s), "modelExecutionTime") for m in self.lr if m.which() == s] diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc index 06578b455a7ca3..dc9f292df19ca4 100644 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -25,7 +25,7 @@ void DriverViewWindow::mouseReleaseEvent(QMouseEvent* e) { emit done(); } -DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverState"}), QWidget(parent) { +DriverViewScene::DriverViewScene(QWidget* parent) : sm({"driverStateV2"}), QWidget(parent) { face_img = loadPixmap("../assets/img_driver_face.png", {FACE_IMG_SIZE, FACE_IMG_SIZE}); } @@ -57,43 +57,36 @@ void DriverViewScene::paintEvent(QPaintEvent* event) { return; } - const int width = 4 * height() / 3; - const QRect rect2 = {rect().center().x() - width / 2, rect().top(), width, rect().height()}; - const QRect valid_rect = {is_rhd ? rect2.right() - rect2.height() / 2 : rect2.left(), rect2.top(), rect2.height() / 2, rect2.height()}; + cereal::DriverStateV2::Reader driver_state = sm["driverStateV2"].getDriverStateV2(); + cereal::DriverStateV2::DriverData::Reader driver_data; - // blackout - const QColor bg(0, 0, 0, 140); - const QRect& blackout_rect = rect(); - p.fillRect(blackout_rect.adjusted(0, 0, valid_rect.left() - blackout_rect.right(), 0), bg); - p.fillRect(blackout_rect.adjusted(valid_rect.right() - blackout_rect.left(), 0, 0, 0), bg); - p.fillRect(blackout_rect.adjusted(valid_rect.left()-blackout_rect.left()+1, 0, valid_rect.right()-blackout_rect.right()-1, -valid_rect.height()*7/10), bg); // top dz + // is_rhd = driver_state.getWheelOnRightProb() > 0.5; + driver_data = is_rhd ? driver_state.getRightDriverData() : driver_state.getLeftDriverData(); - // face bounding box - cereal::DriverState::Reader driver_state = sm["driverState"].getDriverState(); - bool face_detected = driver_state.getFaceProb() > 0.5; + bool face_detected = driver_data.getFaceProb() > 0.7; if (face_detected) { - auto fxy_list = driver_state.getFacePosition(); - auto std_list = driver_state.getFaceOrientationStd(); + auto fxy_list = driver_data.getFacePosition(); + auto std_list = driver_data.getFaceOrientationStd(); float face_x = fxy_list[0]; float face_y = fxy_list[1]; float face_std = std::max(std_list[0], std_list[1]); float alpha = 0.7; - if (face_std > 0.08) { - alpha = std::max(0.7 - (face_std-0.08)*7, 0.0); + if (face_std > 0.15) { + alpha = std::max(0.7 - (face_std-0.15)*3.5, 0.0); } - const int box_size = 0.6 * rect2.height() / 2; - const float rhd_offset = 0.05; // lhd is shifted, so rhd is not mirrored - int fbox_x = valid_rect.center().x() + (is_rhd ? (face_x + rhd_offset) : -face_x) * valid_rect.width(); - int fbox_y = valid_rect.center().y() + face_y * valid_rect.height(); + const int box_size = 220; + // use approx instead of distort_points + int fbox_x = 1080.0 - 1714.0 * face_x; + int fbox_y = -135.0 + (504.0 + std::abs(face_x)*112.0) + (1205.0 - std::abs(face_x)*724.0) * face_y; p.setPen(QPen(QColor(255, 255, 255, alpha * 255), 10)); p.drawRoundedRect(fbox_x - box_size / 2, fbox_y - box_size / 2, box_size, box_size, 35.0, 35.0); } // icon - const int img_offset = 30; - const int img_x = is_rhd ? rect2.right() - FACE_IMG_SIZE - img_offset : rect2.left() + img_offset; - const int img_y = rect2.bottom() - FACE_IMG_SIZE - img_offset; - p.setOpacity(face_detected ? 1.0 : 0.3); + const int img_offset = 60; + const int img_x = rect().left() + img_offset; + const int img_y = rect().bottom() - FACE_IMG_SIZE - img_offset; + p.setOpacity(face_detected ? 1.0 : 0.2); p.drawPixmap(img_x, img_y, face_img); } diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index 000ac4847566a8..8666eb1d4e450e 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -67,16 +67,15 @@ const mat4 device_transform = {{ }}; mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) { - const float driver_view_ratio = 1.333; - const float yscale = stream_height * driver_view_ratio / tici_dm_crop::width; + const float driver_view_ratio = 2.0; + const float yscale = stream_height * driver_view_ratio / stream_width; const float xscale = yscale*screen_height/screen_width*stream_width/stream_height; mat4 transform = (mat4){{ - xscale, 0.0, 0.0, xscale*tici_dm_crop::x_offset/stream_width*2, - 0.0, yscale, 0.0, yscale*tici_dm_crop::y_offset/stream_height*2, + xscale, 0.0, 0.0, 0.0, + 0.0, yscale, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, }}; - return transform; } diff --git a/system/hardware/tici/test_power_draw.py b/system/hardware/tici/test_power_draw.py index b6b5c735fa77b9..4277bb927390eb 100755 --- a/system/hardware/tici/test_power_draw.py +++ b/system/hardware/tici/test_power_draw.py @@ -21,7 +21,7 @@ class Proc: PROCS = [ Proc('camerad', 2.15), Proc('modeld', 1.0, atol=0.15), - Proc('dmonitoringmodeld', 0.25), + Proc('dmonitoringmodeld', 0.35), Proc('encoderd', 0.23), ] From 2e2118dac33f51b5f03d7e2a5512fc9154c12122 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jun 2022 16:32:59 -0700 Subject: [PATCH 131/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 05bbdafb67060f..c6acc0698a604e 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 05bbdafb67060ffb62e6e0af812935494ad91933 +Subproject commit c6acc0698a604e715e960250359b6bf97e4987e3 From 5e0713122115604ccdbbb3844a6ab8d49657bc6a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jun 2022 19:16:49 -0700 Subject: [PATCH 132/436] Toyota: international corolla cross is supported --- docs/CARS.md | 3 ++- selfdrive/car/toyota/values.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/CARS.md b/docs/CARS.md index 77b1c5e787ef6a..7c73bd3403e08f 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -35,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -# Gold - 28 cars +# Gold - 29 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -58,6 +58,7 @@ How We Rate The Cars |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| |Toyota|Corolla 2020-22|All|||||| +|Toyota|Corolla Cross 2020-21 (Non-US only)|All|||||| |Toyota|Corolla Hatchback 2019-22|All|||||| |Toyota|Corolla Hybrid 2020-22|All|||||| |Toyota|Highlander 2020-22|All|||||| diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 2c151266cff82c..b1c6da922bc843 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -119,6 +119,7 @@ class ToyotaCarInfo(CarInfo): CAR.COROLLA: ToyotaCarInfo("Toyota Corolla 2017-19", footnotes=[Footnote.DSU]), CAR.COROLLA_TSS2: [ ToyotaCarInfo("Toyota Corolla 2020-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), + ToyotaCarInfo("Toyota Corolla Cross 2020-21 (Non-US only)"), ToyotaCarInfo("Toyota Corolla Hatchback 2019-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), ], CAR.COROLLAH_TSS2: [ From 04b8e1b6a03aa0ad39b424f6c899365d0bc98604 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 20 Jun 2022 19:37:02 -0700 Subject: [PATCH 133/436] FPv2: log correct response address (#24918) log correct response address (29 bit addresses have a different standard) --- selfdrive/car/fw_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 81883cecaaaff3..a03b6b881d588b 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -350,7 +350,7 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr f.ecu = ecu_types[addr] f.fwVersion = version f.address = addr[0] - f.responseAddress = addr[0] + rx_offset + f.responseAddress = uds.get_rx_addr_for_tx_addr(addr[0], rx_offset) f.request = request if addr[1] is not None: From 59055724d6069471618a7090270797e50d59f798 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 20 Jun 2022 20:20:24 -0700 Subject: [PATCH 134/436] 22 civic hatcback is supported --- docs/CARS.md | 3 ++- selfdrive/car/honda/values.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 7c73bd3403e08f..73ffcc0e0abc36 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -150,7 +150,7 @@ How We Rate The Cars |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 70 cars +# Bronze - 71 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -171,6 +171,7 @@ How We Rate The Cars |Honda|Civic 2019-20|All|||[2](#footnotes)||| |Honda|Civic 2022|All|||||| |Honda|Civic Hatchback 2017-21|Honda Sensing|||||| +|Honda|Civic Hatchback 2022|All|||||| |Honda|CR-V 2015-16|Touring|||||| |Honda|CR-V 2017-21|Honda Sensing|||||| |Honda|CR-V Hybrid 2017-19|Honda Sensing|||||| diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 0bdb8ccd912e0e..c6e20f2d835ca1 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -119,7 +119,10 @@ class HondaCarInfo(CarInfo): HondaCarInfo("Honda Civic Hatchback 2017-21", harness=Harness.bosch_a), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform - CAR.CIVIC_2022: HondaCarInfo("Honda Civic 2022", "All", min_steer_speed=0., harness=Harness.bosch_b), + CAR.CIVIC_2022: [ + HondaCarInfo("Honda Civic 2022", "All", min_steer_speed=0., harness=Harness.bosch_b), + HondaCarInfo("Honda Civic Hatchback 2022", "All", min_steer_speed=0., harness=Harness.bosch_b), + ], CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS, harness=Harness.nidec), CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring", harness=Harness.nidec), CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-21", harness=Harness.bosch_a), From cccab50b166272d13ab51eab1eb10e21beca604e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 20 Jun 2022 22:14:13 -0700 Subject: [PATCH 135/436] FPv2: log all present ECU addresses (#24916) * eliminate brands based on ECUs that respond to tester present * make it work * Add type hint for can message Use make_can_msg * Only query for addresses in fingerprints, and account for different busses * These need to be addresses, not response addresses * We need to listen to response addresses, not query addresses * add to files_common * Unused Optional Drain sock raw * add logging * only query essential ecus comments * simplify get_brand_candidates(), keep track of multiple request variants per make and request each subaddress * fixes make dat bytes bus is src Fix check * (addr, subaddr, bus) can be common across brands, add a match to each brand * fix length * query subaddrs in sequence * fix * candidate if a platform is a subset of responding ecu addresses comment comment * do logging for shadow mode * log responses so we can calculate candidates offline * get has_subaddress from response set * one liner * fix mypy * set to default at top * always log for now * log to make sure it's taking exactly timeout time * import time * fix logging * 0.1 timeout * clean up Co-authored-by: Greg Hogan --- release/files_common | 1 + selfdrive/car/car_helpers.py | 8 ++-- selfdrive/car/ecu_addrs.py | 90 ++++++++++++++++++++++++++++++++++++ selfdrive/car/fw_versions.py | 42 +++++++++++++++-- 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100755 selfdrive/car/ecu_addrs.py diff --git a/release/files_common b/release/files_common index 7274ee9bf6c3be..96649b9f161721 100644 --- a/release/files_common +++ b/release/files_common @@ -101,6 +101,7 @@ selfdrive/car/interfaces.py selfdrive/car/vin.py selfdrive/car/disable_ecu.py selfdrive/car/fw_versions.py +selfdrive/car/ecu_addrs.py selfdrive/car/isotp_parallel_query.py selfdrive/car/tests/__init__.py selfdrive/car/tests/test_car_interfaces.py diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 3bfd99f88def6f..8c0fd7c90d6cdd 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -8,7 +8,7 @@ from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from selfdrive.car.vin import get_vin, VIN_UNKNOWN -from selfdrive.car.fw_versions import get_fw_versions, match_fw_to_car +from selfdrive.car.fw_versions import get_fw_versions, match_fw_to_car, get_present_ecus from system.swaglog import cloudlog import cereal.messaging as messaging from selfdrive.car import gen_empty_fingerprint @@ -79,6 +79,7 @@ def _get_interface_names() -> Dict[str, List[str]]: def fingerprint(logcan, sendcan): fixed_fingerprint = os.environ.get('FINGERPRINT', "") skip_fw_query = os.environ.get('SKIP_FW_QUERY', False) + ecu_responses = set() if not fixed_fingerprint and not skip_fw_query: # Vin query only reliably works thorugh OBDII @@ -97,6 +98,7 @@ def fingerprint(logcan, sendcan): else: cloudlog.warning("Getting VIN & FW versions") _, vin = get_vin(logcan, sendcan, bus) + ecu_responses = get_present_ecus(logcan, sendcan) car_fw = get_fw_versions(logcan, sendcan) exact_fw_match, fw_candidates = match_fw_to_car(car_fw) @@ -163,8 +165,8 @@ def fingerprint(logcan, sendcan): car_fingerprint = fixed_fingerprint source = car.CarParams.FingerprintSource.fixed - cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, - source=source, fuzzy=not exact_match, fw_count=len(car_fw)) + cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, + fw_count=len(car_fw), ecu_responses=ecu_responses, error=True) return car_fingerprint, finger, vin, car_fw, source, exact_match diff --git a/selfdrive/car/ecu_addrs.py b/selfdrive/car/ecu_addrs.py new file mode 100755 index 00000000000000..267701509a6926 --- /dev/null +++ b/selfdrive/car/ecu_addrs.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import capnp +import time +import traceback +from typing import Optional, Set, Tuple + +import cereal.messaging as messaging +from panda.python.uds import SERVICE_TYPE +from selfdrive.car import make_can_msg +from selfdrive.boardd.boardd import can_list_to_can_capnp +from system.swaglog import cloudlog + + +def make_tester_present_msg(addr, bus, subaddr=None): + dat = [0x02, SERVICE_TYPE.TESTER_PRESENT, 0x0] + if subaddr is not None: + dat.insert(0, subaddr) + + dat.extend([0x0] * (8 - len(dat))) + return make_can_msg(addr, bytes(dat), bus) + + +def is_tester_present_response(msg: capnp.lib.capnp._DynamicStructReader, subaddr: Optional[int] = None) -> bool: + # ISO-TP messages are always padded to 8 bytes + # tester present response is always a single frame + dat_offset = 1 if subaddr is not None else 0 + if len(msg.dat) == 8 and 1 <= msg.dat[dat_offset] <= 7: + # success response + if msg.dat[dat_offset + 1] == (SERVICE_TYPE.TESTER_PRESENT + 0x40): + return True + # error response + if msg.dat[dat_offset + 1] == 0x7F and msg.dat[dat_offset + 2] == SERVICE_TYPE.TESTER_PRESENT: + return True + return False + + +def get_all_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, bus: int, timeout: float = 1, debug: bool = True) -> Set[Tuple[int, Optional[int], int]]: + addr_list = [0x700 + i for i in range(256)] + [0x18da00f1 + (i << 8) for i in range(256)] + queries: Set[Tuple[int, Optional[int], int]] = {(addr, None, bus) for addr in addr_list} + responses = queries + return get_ecu_addrs(logcan, sendcan, queries, responses, timeout=timeout, debug=debug) + + +def get_ecu_addrs(logcan: messaging.SubSocket, sendcan: messaging.PubSocket, queries: Set[Tuple[int, Optional[int], int]], + responses: Set[Tuple[int, Optional[int], int]], timeout: float = 1, debug: bool = False) -> Set[Tuple[int, Optional[int], int]]: + ecu_responses: Set[Tuple[int, Optional[int], int]] = set() # set((addr, subaddr, bus),) + try: + msgs = [make_tester_present_msg(addr, bus, subaddr) for addr, subaddr, bus in queries] + + messaging.drain_sock_raw(logcan) + sendcan.send(can_list_to_can_capnp(msgs, msgtype='sendcan')) + start_time = time.monotonic() + while time.monotonic() - start_time < timeout: + can_packets = messaging.drain_sock(logcan, wait_for_one=True) + for packet in can_packets: + for msg in packet.can: + subaddr = None if (msg.address, None, msg.src) in responses else msg.dat[0] + if (msg.address, subaddr, msg.src) in responses and is_tester_present_response(msg, subaddr): + if debug: + print(f"CAN-RX: {hex(msg.address)} - 0x{bytes.hex(msg.dat)}") + if (msg.address, subaddr, msg.src) in ecu_responses: + print(f"Duplicate ECU address: {hex(msg.address)}") + ecu_responses.add((msg.address, subaddr, msg.src)) + except Exception: + cloudlog.warning(f"ECU addr scan exception: {traceback.format_exc()}") + return ecu_responses + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description='Get addresses of all ECUs') + parser.add_argument('--debug', action='store_true') + args = parser.parse_args() + + logcan = messaging.sub_sock('can') + sendcan = messaging.pub_sock('sendcan') + + time.sleep(1.0) + + print("Getting ECU addresses ...") + ecu_addrs = get_all_ecu_addrs(logcan, sendcan, 1, debug=args.debug) + + print() + print("Found ECUs on addresses:") + for addr, subaddr, bus in ecu_addrs: + msg = f" 0x{hex(addr)}" + if subaddr is not None: + msg += f" (sub-address: 0x{hex(subaddr)})" + print(msg) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index a03b6b881d588b..66f0564fca6fda 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -3,11 +3,12 @@ import traceback from collections import defaultdict from dataclasses import dataclass, field -from typing import Any, List +from typing import Any, List, Optional, Set, Tuple from tqdm import tqdm import panda.python.uds as uds from cereal import car +from selfdrive.car.ecu_addrs import get_ecu_addrs from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import FW_VERSIONS from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery @@ -15,6 +16,7 @@ from system.swaglog import cloudlog Ecu = car.CarParams.Ecu +ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.esp, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa] def p16(val): @@ -261,7 +263,6 @@ def match_fw_to_car_exact(fw_versions_dict): ecu_type = ecu[0] addr = ecu[1:] found_version = fw_versions_dict.get(addr, None) - ESSENTIAL_ECUS = [Ecu.engine, Ecu.eps, Ecu.esp, Ecu.fwdRadar, Ecu.fwdCamera, Ecu.vsa] if ecu_type == Ecu.esp and candidate in (TOYOTA.RAV4, TOYOTA.COROLLA, TOYOTA.HIGHLANDER, TOYOTA.SIENNA, TOYOTA.LEXUS_IS) and found_version is None: continue @@ -299,11 +300,46 @@ def match_fw_to_car(fw_versions, allow_fuzzy=True): return exact_match, matches +def get_present_ecus(logcan, sendcan): + queries = list() + parallel_queries = list() + responses = set() + versions = get_interface_attr('FW_VERSIONS', ignore_none=True) + + for r in REQUESTS: + if r.brand not in versions: + continue + + for brand_versions in versions[r.brand].values(): + for ecu_type, addr, sub_addr in brand_versions: + # Only query ecus in whitelist if whitelist is not empty + if len(r.whitelist_ecus) == 0 or ecu_type in r.whitelist_ecus: + a = (addr, sub_addr, r.bus) + # Build set of queries + if sub_addr is None: + if a not in parallel_queries: + parallel_queries.append(a) + else: # subaddresses must be queried one by one + if [a] not in queries: + queries.append([a]) + + # Build set of expected responses to filter + response_addr = uds.get_rx_addr_for_tx_addr(addr, r.rx_offset) + responses.add((response_addr, sub_addr, r.bus)) + + queries.insert(0, parallel_queries) + + ecu_responses: Set[Tuple[int, Optional[int], int]] = set() + for query in queries: + ecu_responses.update(get_ecu_addrs(logcan, sendcan, set(query), responses, timeout=0.1)) + return ecu_responses + + def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progress=False): ecu_types = {} # Extract ECU addresses to query from fingerprints - # ECUs using a subadress need be queried one by one, the rest can be done in parallel + # ECUs using a subaddress need be queried one by one, the rest can be done in parallel addrs = [] parallel_addrs = [] From bd782c982f17221a33b660d82dab7aa297e10c60 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 20 Jun 2022 23:23:14 -0700 Subject: [PATCH 136/436] Mazda: log standstill bit (#24923) * set standstill bit for mazda * use bit in carcontroller * Revert "use bit in carcontroller" This reverts commit f38210a191bcd6e34ff81626e857f778207a55e7. --- selfdrive/car/mazda/carstate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/mazda/carstate.py b/selfdrive/car/mazda/carstate.py index 6e1ad3e481a0c8..944d79809b1606 100644 --- a/selfdrive/car/mazda/carstate.py +++ b/selfdrive/car/mazda/carstate.py @@ -79,6 +79,7 @@ def update(self, cp, cp_cam): # it should be used for carState.cruiseState.nonAdaptive instead ret.cruiseState.available = cp.vl["CRZ_CTRL"]["CRZ_AVAILABLE"] == 1 ret.cruiseState.enabled = cp.vl["CRZ_CTRL"]["CRZ_ACTIVE"] == 1 + ret.cruiseState.standstill = cp.vl["PEDALS"]["STANDSTILL"] == 1 ret.cruiseState.speed = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] * CV.KPH_TO_MS if ret.cruiseState.enabled: From 5ce9b5d38cbba4d59935ef8504ec2a8cfb0eb149 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Tue, 21 Jun 2022 00:11:13 -0700 Subject: [PATCH 137/436] Bump laika (#24920) --- laika_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laika_repo b/laika_repo index 44f048bc1f58ae..824b49327aa2f4 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 44f048bc1f58ae9e28dfdeb98e40aea3e0f2b699 +Subproject commit 824b49327aa2f41eb0dafbe3806e6c4841b19f11 From e45eb1bd28623571079122030392fa0fe03e7a42 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 21 Jun 2022 00:11:55 -0700 Subject: [PATCH 138/436] test_models: check cruiseState.available (#24924) * check available is true if enabled is true * remove extra line --- selfdrive/car/tests/test_models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 27dc4ba776096a..cb4b86858282de 100755 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -221,6 +221,7 @@ def test_panda_safety_carstate(self): # TODO: check rest of panda's carstate (steering, ACC main on, etc.) checks['gasPressed'] += CS.gasPressed != self.safety.get_gas_pressed_prev() + checks['cruiseState'] += CS.cruiseState.enabled and not CS.cruiseState.available # TODO: remove this exception once this mismatch is resolved brake_pressed = CS.brakePressed From d261ff6d0395f3344e41fbf47b6d9aa149ac642f Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 21 Jun 2022 11:01:44 +0200 Subject: [PATCH 139/436] bump laika --- laika_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laika_repo b/laika_repo index 824b49327aa2f4..27a0d8a776fc8c 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 824b49327aa2f41eb0dafbe3806e6c4841b19f11 +Subproject commit 27a0d8a776fc8c1eaf8608d17ce81a00136f8bd0 From f7205f3b07d15ff4e9d840a689792d7f5b5be0d8 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 21 Jun 2022 11:14:25 +0200 Subject: [PATCH 140/436] ui: metric wider set speed box (#24890) --- selfdrive/ui/qt/onroad.cc | 3 ++- selfdrive/ui/qt/onroad.h | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 45bf41ba2df2cd..482f04c03269a4 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -196,6 +196,7 @@ void NvgWindow::updateState(const UIState &s) { setProperty("has_eu_speed_limit", nav_alive && speed_limit_sign == cereal::NavInstruction::SpeedLimitSign::VIENNA); setProperty("is_cruise_set", cruise_set); + setProperty("is_metric", s.scene.is_metric); setProperty("speed", cur_speed); setProperty("setSpeed", set_speed); setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); @@ -225,8 +226,8 @@ void NvgWindow::drawHud(QPainter &p) { // Draw outer box + border to contain set speed and speed limit int default_rect_width = 172; int rect_width = default_rect_width; + if (is_metric || has_eu_speed_limit) rect_width = 200; if (has_us_speed_limit && speedLimitStr.size() >= 3) rect_width = 223; - else if (has_eu_speed_limit) rect_width = 200; int rect_height = 204; if (has_us_speed_limit) rect_height = 402; diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index e58d7a79729130..4cd88fc9e3f642 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -34,6 +34,7 @@ class NvgWindow : public CameraViewWidget { Q_PROPERTY(bool is_cruise_set MEMBER is_cruise_set); Q_PROPERTY(bool has_eu_speed_limit MEMBER has_eu_speed_limit); Q_PROPERTY(bool has_us_speed_limit MEMBER has_us_speed_limit); + Q_PROPERTY(bool is_metric MEMBER is_metric); Q_PROPERTY(bool engageable MEMBER engageable); Q_PROPERTY(bool dmActive MEMBER dmActive); @@ -57,6 +58,7 @@ class NvgWindow : public CameraViewWidget { float setSpeed; float speedLimit; bool is_cruise_set = false; + bool is_metric = false; bool engageable = false; bool dmActive = false; bool hideDM = false; From 21dd464fd3b0e23e2277479532c56230fe1e66bd Mon Sep 17 00:00:00 2001 From: sshane Date: Tue, 21 Jun 2022 03:22:38 -0700 Subject: [PATCH 141/436] Revert "VW FPv2: reduce number of ECU queries (#24706)" This reverts commit 6c02e554e7174745c7605f4e8f0e3c1aea637091. --- selfdrive/car/fw_versions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 66f0564fca6fda..758485c393b384 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -148,14 +148,12 @@ class Request: "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], - whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], rx_offset=VOLKSWAGEN_RX_OFFSET, ), Request( "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], - whitelist_ecus=[Ecu.engine, Ecu.transmission], ), # Mazda Request( From 278f7b9e8a54839989d6a5bd8d37d6aeca789d2a Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 21 Jun 2022 13:15:26 +0200 Subject: [PATCH 142/436] ui: draw MAX above set speed (#24930) --- selfdrive/ui/qt/onroad.cc | 44 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 482f04c03269a4..d92cf36b894fad 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -241,26 +241,6 @@ void NvgWindow::drawHud(QPainter &p) { p.setBrush(blackColor(166)); drawRoundedRect(p, set_speed_rect, top_radius, top_radius, bottom_radius, bottom_radius); - // Draw set speed - if (is_cruise_set) { - if (speedLimit > 0 && status != STATUS_DISENGAGED && status != STATUS_OVERRIDE) { - p.setPen(interpColor( - setSpeed, - {speedLimit + 5, speedLimit + 15, speedLimit + 25}, - {whiteColor(), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} - )); - } else { - p.setPen(whiteColor()); - } - } else { - p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); - } - configFont(p, "Open Sans", 90, "Bold"); - QRect speed_rect = getTextRect(p, Qt::AlignCenter, setSpeedStr); - speed_rect.moveCenter({set_speed_rect.center().x(), 0}); - speed_rect.moveTop(set_speed_rect.top() + 8); - p.drawText(speed_rect, Qt::AlignCenter, setSpeedStr); - // Draw MAX if (is_cruise_set) { if (status == STATUS_DISENGAGED) { @@ -282,9 +262,31 @@ void NvgWindow::drawHud(QPainter &p) { configFont(p, "Open Sans", 40, "SemiBold"); QRect max_rect = getTextRect(p, Qt::AlignCenter, "MAX"); max_rect.moveCenter({set_speed_rect.center().x(), 0}); - max_rect.moveTop(set_speed_rect.top() + 123); + max_rect.moveTop(set_speed_rect.top() + 23); p.drawText(max_rect, Qt::AlignCenter, "MAX"); + // Draw set speed + if (is_cruise_set) { + if (speedLimit > 0 && status != STATUS_DISENGAGED && status != STATUS_OVERRIDE) { + p.setPen(interpColor( + setSpeed, + {speedLimit + 5, speedLimit + 15, speedLimit + 25}, + {whiteColor(), QColor(0xff, 0x95, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff)} + )); + } else { + p.setPen(whiteColor()); + } + } else { + p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); + } + configFont(p, "Open Sans", 90, "Bold"); + QRect speed_rect = getTextRect(p, Qt::AlignCenter, setSpeedStr); + speed_rect.moveCenter({set_speed_rect.center().x(), 0}); + speed_rect.moveTop(set_speed_rect.top() + 67); + p.drawText(speed_rect, Qt::AlignCenter, setSpeedStr); + + + // US/Canada (MUTCD style) sign if (has_us_speed_limit) { const int border_width = 6; From 67b601e0efb5f33697cadadb50d32debb4325d60 Mon Sep 17 00:00:00 2001 From: Anton Rudomanenko Date: Tue, 21 Jun 2022 16:23:43 +0300 Subject: [PATCH 143/436] replay: handle missing socket while replaying route log with --allow flag (#24933) * fix: fix the problem with replay routes locally * fix: Exception with --allow flag in replay.cc Co-authored-by: Anton Rudomaneko --- selfdrive/ui/replay/replay.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/replay/replay.cc b/selfdrive/ui/replay/replay.cc index 5eb7469c92b37e..cf7520b361b236 100644 --- a/selfdrive/ui/replay/replay.cc +++ b/selfdrive/ui/replay/replay.cc @@ -360,7 +360,8 @@ void Replay::stream() { setCurrentSegment(toSeconds(cur_mono_time_) / 60); // migration for pandaState -> pandaStates to keep UI working for old segments - if (cur_which == cereal::Event::Which::PANDA_STATE_D_E_P_R_E_C_A_T_E_D) { + if (cur_which == cereal::Event::Which::PANDA_STATE_D_E_P_R_E_C_A_T_E_D && + sockets_[cereal::Event::Which::PANDA_STATES] != nullptr) { MessageBuilder msg; auto ps = msg.initEvent().initPandaStates(1); ps[0].setIgnitionLine(true); From 95d8517a81d23e0c385b5f931e7e89a3d3e52b17 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 21 Jun 2022 16:26:40 +0200 Subject: [PATCH 144/436] car_bug_report.yml: fix labels --- .github/ISSUE_TEMPLATE/car_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/car_bug_report.yml b/.github/ISSUE_TEMPLATE/car_bug_report.yml index 23527c3a4327c6..7f368f11b49a76 100644 --- a/.github/ISSUE_TEMPLATE/car_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/car_bug_report.yml @@ -1,6 +1,6 @@ name: Car bug report description: For issues with a particular car make or model -labels: ["car bug"] +labels: ["car", "bug"] body: - type: markdown From 0e0b5c4e24a8da64362c8dbf851dbe0f806916c0 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Tue, 21 Jun 2022 12:07:00 -0700 Subject: [PATCH 145/436] Revert "Rocket league model (#24869)" (#24936) * Revert rocket league * revert ref commit * New model ref commit --- selfdrive/modeld/models/driving.h | 2 +- selfdrive/modeld/models/supercombo.dlc | 4 ++-- selfdrive/modeld/models/supercombo.onnx | 4 ++-- selfdrive/modeld/thneed/compile.cc | 2 +- selfdrive/modeld/thneed/optimizer.cc | 8 ++++---- selfdrive/test/process_replay/model_replay_ref_commit | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/selfdrive/modeld/models/driving.h b/selfdrive/modeld/models/driving.h index a6910516364d38..97e65fbc0e4b4e 100644 --- a/selfdrive/modeld/models/driving.h +++ b/selfdrive/modeld/models/driving.h @@ -245,7 +245,7 @@ struct ModelOutput { constexpr int OUTPUT_SIZE = sizeof(ModelOutput) / sizeof(float); #ifdef TEMPORAL - constexpr int TEMPORAL_SIZE = 512+256; + constexpr int TEMPORAL_SIZE = 512; #else constexpr int TEMPORAL_SIZE = 0; #endif diff --git a/selfdrive/modeld/models/supercombo.dlc b/selfdrive/modeld/models/supercombo.dlc index 90f7a2e65b94da..23f6d904fb238b 100644 --- a/selfdrive/modeld/models/supercombo.dlc +++ b/selfdrive/modeld/models/supercombo.dlc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c2cb3a3054f3292bbe538d6b793908dc2e234c200802d41b6766d3cb51b0b44 -size 101662751 +oid sha256:027cbb1fabae369878271cb0e3505071a8bdaa07473fad9a0b2e8d695c5dc1ff +size 76725611 diff --git a/selfdrive/modeld/models/supercombo.onnx b/selfdrive/modeld/models/supercombo.onnx index 0493398560cd27..9023c18dd7f970 100644 --- a/selfdrive/modeld/models/supercombo.onnx +++ b/selfdrive/modeld/models/supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96b60d0bfd1386c93b4f79195aa1c5e77b23e0250578a308ee2c58857ed5eb49 -size 102570834 +oid sha256:484976ea5bd4ddcabc82e95faf30d7311a27802c1e337472558699fa2395a499 +size 77472267 diff --git a/selfdrive/modeld/thneed/compile.cc b/selfdrive/modeld/thneed/compile.cc index a2f55ffd972b0b..f76c63b2b917cd 100644 --- a/selfdrive/modeld/thneed/compile.cc +++ b/selfdrive/modeld/thneed/compile.cc @@ -5,7 +5,7 @@ #include "selfdrive/modeld/thneed/thneed.h" #include "system/hardware/hw.h" -#define TEMPORAL_SIZE 512+256 +#define TEMPORAL_SIZE 512 #define DESIRE_LEN 8 #define TRAFFIC_CONVENTION_LEN 2 diff --git a/selfdrive/modeld/thneed/optimizer.cc b/selfdrive/modeld/thneed/optimizer.cc index 39737d3d7623ba..03d20ff3861332 100644 --- a/selfdrive/modeld/thneed/optimizer.cc +++ b/selfdrive/modeld/thneed/optimizer.cc @@ -9,7 +9,7 @@ extern map g_program_source; -/*static int is_same_size_image(cl_mem a, cl_mem b) { +static int is_same_size_image(cl_mem a, cl_mem b) { size_t a_width, a_height, a_depth, a_array_size, a_row_pitch, a_slice_pitch; clGetImageInfo(a, CL_IMAGE_WIDTH, sizeof(a_width), &a_width, NULL); clGetImageInfo(a, CL_IMAGE_HEIGHT, sizeof(a_height), &a_height, NULL); @@ -29,7 +29,7 @@ extern map g_program_source; return (a_width == b_width) && (a_height == b_height) && (a_depth == b_depth) && (a_array_size == b_array_size) && (a_row_pitch == b_row_pitch) && (a_slice_pitch == b_slice_pitch); -}*/ +} static cl_mem make_image_like(cl_context context, cl_mem val) { cl_image_format format; @@ -138,7 +138,7 @@ int Thneed::optimize() { // delete useless copy layers // saves ~0.7 ms - /*if (kq[i]->name == "concatenation" || kq[i]->name == "flatten") { + if (kq[i]->name == "concatenation" || kq[i]->name == "flatten") { string in = kq[i]->args[kq[i]->get_arg_num("input")]; string out = kq[i]->args[kq[i]->get_arg_num("output")]; if (is_same_size_image(*(cl_mem*)in.data(), *(cl_mem*)out.data())) { @@ -148,7 +148,7 @@ int Thneed::optimize() { kq.erase(kq.begin()+i); --i; } - }*/ + } // NOTE: if activations/accumulation are done in the wrong order, this will be wrong diff --git a/selfdrive/test/process_replay/model_replay_ref_commit b/selfdrive/test/process_replay/model_replay_ref_commit index 0c4c2305ebc292..9f9b822693fcda 100644 --- a/selfdrive/test/process_replay/model_replay_ref_commit +++ b/selfdrive/test/process_replay/model_replay_ref_commit @@ -1 +1 @@ -5434b3c1696554e9a889e77f794d80cd1cb0a7ec +df0ce74929dd6b5fa7a55224baefeff4bac6d785 From 1d447441239b3086e1f37185e2e5d210c80c8164 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jun 2022 19:26:20 -0700 Subject: [PATCH 146/436] jenkins: set successful boot flag --- selfdrive/test/setup_device_ci.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index 99acc050ea64f6..2e5ffeacc66cca 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -36,6 +36,8 @@ fi tee $CONTINUE_PATH << EOF #!/usr/bin/bash +sudo abctl --set_success + while true; do if ! sudo systemctl is-active -q ssh; then sudo systemctl start ssh From 4efb28766e386de3117819b8a128aa782aaadf2a Mon Sep 17 00:00:00 2001 From: Jason Wen <47793918+sunnyhaibin@users.noreply.github.com> Date: Wed, 22 Jun 2022 01:17:13 -0400 Subject: [PATCH 147/436] Honda Longitudinal: fix HUD max distance setting (#24915) Fix max distance setting on display --- selfdrive/car/honda/hondacan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 6a1fb2e4597011..5de29b4f37c374 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -111,7 +111,7 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc acc_hud_values = { 'CRUISE_SPEED': hud.v_cruise, 'ENABLE_MINI_CAR': 1, - 'HUD_DISTANCE': 3, # max distance setting on display + 'HUD_DISTANCE': 0, # max distance setting on display 'IMPERIAL_UNIT': int(not is_metric), 'HUD_LEAD': 2 if enabled and hud.lead_visible else 1 if enabled else 0, 'SET_ME_X01_2': 1, From dd67853526cc2cda20edc51c45bd9c65a1fa931b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 21 Jun 2022 22:40:16 -0700 Subject: [PATCH 148/436] update refs --- selfdrive/test/process_replay/ref_commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 8ed68d5bddf082..b2623dc8dce9d2 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -ed1dfb8b155ebcd8fdad4e06462b3bb7869fc67b +4efb28766e386de3117819b8a128aa782aaadf2a \ No newline at end of file From f99a091eaaaa65c4a7ec3e2e22e045e6263f6bcd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 21 Jun 2022 23:39:26 -0700 Subject: [PATCH 149/436] update camerad gitignores --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 334b1b4fedf304..0092c4dc946bab 100644 --- a/.gitignore +++ b/.gitignore @@ -49,8 +49,8 @@ selfdrive/loggerd/loggerd selfdrive/loggerd/bootlog selfdrive/sensord/_gpsd selfdrive/sensord/_sensord -selfdrive/camerad/camerad -selfdrive/camerad/test/ae_gray_test +system/camerad/camerad +system/camerad/test/ae_gray_test selfdrive/modeld/_modeld selfdrive/modeld/_dmonitoringmodeld /src/ From 789f2d195cee7b7dd8dd07d36a4e2e9b6b442e38 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 22 Jun 2022 01:03:29 -0700 Subject: [PATCH 150/436] compatibility docs: fixup steering torque star (#24940) * Ascent has good torque, hard code Toyota, print all unexpected torque star cars * update docs * Use subtests * hardcode CHR for now generate * Hard code Impreza * update refs --- docs/CARS.md | 32 ++++++++++---------- selfdrive/car/subaru/values.py | 2 +- selfdrive/car/tests/test_docs.py | 37 ++++++++++++++---------- selfdrive/car/torque_data.json | 2 +- selfdrive/car/toyota/values.py | 1 + selfdrive/test/process_replay/ref_commit | 2 +- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 73ffcc0e0abc36..de7dbacf281585 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -35,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -# Gold - 29 cars +# Gold - 33 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -52,9 +52,13 @@ How We Rate The Cars |Lexus|ES 2019-21|All|||||| |Lexus|ES Hybrid 2019-22|All|||||| |Lexus|NX 2020|All|||||| +|Lexus|NX Hybrid 2020|All|||||| |Lexus|RX 2020-22|All|||||| |Lexus|UX Hybrid 2019-21|All|||||| +|Toyota|Alphard 2019-20|All|||||| +|Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Avalon 2022|All|||||| +|Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| |Toyota|Corolla 2020-22|All|||||| @@ -104,13 +108,15 @@ How We Rate The Cars |Kia|Sorento 2018|SCC + LKAS|||||| |Kia|Sorento 2019|SCC + LKAS|||||| |Kia|Stinger 2018|SCC + LKAS|||||| +|Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| +|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|NX 2018-19|All|[3](#footnotes)||||| |Lexus|NX Hybrid 2018-19|All|[3](#footnotes)||||| -|Lexus|NX Hybrid 2020|All|||||| |Lexus|RX Hybrid 2020-21|All|||||| |Mazda|CX-5 2022|All|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| +|Subaru|Ascent 2019-20|All|||||| |Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Forester 2019-21|All|||||| |Subaru|Impreza 2020-21|EyeSight|||||| @@ -121,10 +127,8 @@ How We Rate The Cars |Škoda|Octavia RS 2016|Driver Assistance|||||| |Škoda|Scala 2020|Driver Assistance|||||| |Škoda|Superb 2015-18|Driver Assistance|||||| -|Toyota|Alphard 2019-20|All|||||| -|Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2022|All|||||| +|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| |Toyota|Highlander 2017-19|All|[3](#footnotes)||||| @@ -150,7 +154,7 @@ How We Rate The Cars |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 71 cars +# Bronze - 67 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -197,10 +201,8 @@ How We Rate The Cars |Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| |Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| |Kia|Optima 2017|SCC + LKAS|||||| -|Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| -|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| -|Lexus|IS 2017-19|All|||||| -|Lexus|RC 2020|All|||||| +|Lexus|IS 2017-19|All|||||| +|Lexus|RC 2020|All|||||| |Lexus|RX 2016-18|All|[3](#footnotes)||||| |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| |Mazda|CX-9 2021|All|||||| @@ -208,13 +210,11 @@ How We Rate The Cars |Nissan|Leaf 2018-22|ProPILOT|||||| |Nissan|Rogue 2018-20|ProPILOT|||||| |Nissan|X-Trail 2017|ProPILOT|||||| -|Subaru|Ascent 2019-20|All|||||| -|Subaru|Crosstrek 2018-19|EyeSight|||||| -|Subaru|Impreza 2017-19|EyeSight|||||| -|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| +|Subaru|Crosstrek 2018-19|EyeSight|||||| +|Subaru|Impreza 2017-19|EyeSight|||||| +|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|C-HR 2017-21|All|||||| -|Toyota|C-HR Hybrid 2017-19|All|||||| +|Toyota|C-HR Hybrid 2017-19|All|||||| |Toyota|Corolla 2017-19|All|[3](#footnotes)||||| |Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 45358eb3a43cdd..9badc3ca50f79c 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -41,7 +41,7 @@ class SubaruCarInfo(CarInfo): CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { - CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All"), + CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All", good_torque=True), CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index b4bc14ef005aab..98c909a9be9d97 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -16,32 +16,37 @@ def test_generator(self): current_cars_md = f.read() self.assertEqual(generated_cars_md, current_cars_md, - "Run selfdrive/car/docs.py to generate new supported cars documentation") + "Run selfdrive/car/docs.py to update the compatibility documentation") def test_missing_car_info(self): all_car_info_platforms = get_interface_attr("CAR_INFO", combine_brands=True).keys() for platform in sorted(interfaces.keys()): - if platform not in all_car_info_platforms: - self.fail("Platform: {} doesn't exist in CarInfo".format(platform)) + with self.subTest(platform=platform): + self.assertTrue(platform in all_car_info_platforms, "Platform: {} doesn't exist in CarInfo".format(platform)) def test_naming_conventions(self): - # Asserts market-standard car naming conventions by make + # Asserts market-standard car naming conventions by brand for car in self.all_cars: - tokens = car.model.lower().split(" ") - if car.car_name == "hyundai": - self.assertNotIn("phev", tokens, "Use `Plug-in Hybrid`") - self.assertNotIn("hev", tokens, "Use `Hybrid`") - self.assertNotIn("ev", tokens, "Use `Electric`") - if "plug-in hybrid" in car.model.lower(): - self.assertIn("Plug-in Hybrid", car.model, "Use correct capitalization") - elif car.car_name == "toyota": - if "rav4" in tokens: - self.assertIn("RAV4", car.model, "Use correct capitalization") + with self.subTest(car=car): + tokens = car.model.lower().split(" ") + if car.car_name == "hyundai": + self.assertNotIn("phev", tokens, "Use `Plug-in Hybrid`") + self.assertNotIn("hev", tokens, "Use `Hybrid`") + self.assertNotIn("ev", tokens, "Use `Electric`") + if "plug-in hybrid" in car.model.lower(): + self.assertIn("Plug-in Hybrid", car.model, "Use correct capitalization") + elif car.car_name == "toyota": + if "rav4" in tokens: + self.assertIn("RAV4", car.model, "Use correct capitalization") def test_torque_star(self): + # Asserts brand-specific assumptions around steering torque star for car in self.all_cars: - if car.car_name == "honda": - self.assertTrue(car.row[Column.STEERING_TORQUE] in (Star.EMPTY, Star.HALF), f"{car.name} has full torque star") + with self.subTest(car=car): + if car.car_name == "honda": + self.assertIn(car.row[Column.STEERING_TORQUE], (Star.EMPTY, Star.HALF), f"{car.name} has full torque star") + elif car.car_name in ("toyota", "hyundai"): + self.assertNotEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has no torque star") if __name__ == "__main__": diff --git a/selfdrive/car/torque_data.json b/selfdrive/car/torque_data.json index 63ee0e9bcd9e12..4917cba9bc38ca 100644 --- a/selfdrive/car/torque_data.json +++ b/selfdrive/car/torque_data.json @@ -1 +1 @@ -{"LAT_ACCEL_FACTOR": {"HONDA PILOT 2017": 1.682289482065265, "HONDA CIVIC 2016": 1.5248128495527884, "TOYOTA CAMRY 2018": 2.1115709806216447, "TOYOTA COROLLA HYBRID TSS2 2019": 2.3250600977240077, "TOYOTA RAV4 2019": 2.625504029066767, "HYUNDAI PALISADE 2020": 2.5250855675875634, "TOYOTA SIENNA 2018": 1.8254254785341577, "ACURA RDX 2020": 1.3998101622214894, "TOYOTA RAV4 2017": 1.948190869577896, "HONDA RIDGELINE 2017": 1.4158181862793415, "TOYOTA PRIUS 2017": 1.9142926195557595, "TOYOTA HIGHLANDER HYBRID 2020": 2.1097056247344392, "HYUNDAI SONATA 2020": 3.2488989629905944, "KIA STINGER GT2 2018": 2.7592622336517834, "TOYOTA HIGHLANDER 2020": 2.0408544157877055, "HONDA ACCORD 2018": 1.6374118241564064, "TOYOTA PRIUS TSS2 2021": 2.3207270770298365, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.46050785084946, "LEXUS NX 2020": 2.29533657249232, "TOYOTA RAV4 HYBRID 2019": 2.4003012079562085, "HONDA CIVIC (BOSCH) 2019": 1.6523031416671652, "KIA NIRO HYBRID 2021": 2.743464625803003, "HONDA ACCORD HYBRID 2018": 1.5904016830979033, "LEXUS NX HYBRID 2018": 2.398678119681945, "TOYOTA COROLLA TSS2 2019": 2.3859244449846466, "VOLKSWAGEN ARTEON 1ST GEN": 1.4249208219414902, "TOYOTA CAMRY HYBRID 2021": 2.5434553806317055, "VOLKSWAGEN JETTA 7TH GEN": 1.2228130240634283, "HONDA INSIGHT 2019": 1.468352089969897, "SUBARU FORESTER 2019": 3.6185035528523546, "HYUNDAI ELANTRA 2021": 3.5294999663335185, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.2179616966432905, "HYUNDAI KONA HYBRID 2020": 4.493208192966529, "HONDA ODYSSEY 2018": 1.8838175399087222, "LEXUS RX 2016": 1.3912132245094184, "TOYOTA COROLLA 2017": 3.0143547548384735, "LEXUS ES 2019": 2.012201253045193, "HYUNDAI SANTA FE 2019": 3.039728566484244, "TOYOTA AVALON 2022": 2.4619858654670885, "JEEP GRAND CHEROKEE V6 2018": 1.8411674990629987, "CHEVROLET VOLT PREMIER 2017": 1.5943438675127841, "TOYOTA RAV4 HYBRID 2017": 1.9803053616868995, "LEXUS RX 2020": 1.664616846377383, "TOYOTA HIGHLANDER HYBRID 2018": 1.8866764457400844, "TOYOTA CAMRY HYBRID 2018": 2.014213351947917, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.4428896585442685, "TOYOTA MIRAI 2021": 2.7217623852898853, "LEXUS IS 2018": 3.5624668608596837, "TOYOTA HIGHLANDER 2017": 1.9199133105823853, "HYUNDAI SONATA HYBRID 2021": 2.7313907441569554, "VOLKSWAGEN ATLAS 1ST GEN": 1.4483948408160645, "LEXUS ES HYBRID 2019": 2.4138026617523547, "HYUNDAI GENESIS 2015-2016": 1.7636839808044658, "JEEP GRAND CHEROKEE 2019": 1.787264083939164, "SUBARU ASCENT LIMITED 2019": 3.0494069339774565, "HONDA CR-V 2017": 1.9828470679233807, "HONDA FIT 2018": 1.594940026552055, "TOYOTA CAMRY 2021": 2.5057990840460342, "AUDI Q3 2ND GEN": 1.4558300885316715, "AUDI A3 3RD GEN": 1.5304173783542625, "LEXUS RX HYBRID 2017": 1.577216425446677, "HONDA CIVIC 2022": 2.69252285552613, "GENESIS G70 2018": 3.866842361627636, "CHRYSLER PACIFICA HYBRID 2018": 1.5771851419640903, "VOLKSWAGEN PASSAT 8TH GEN": 1.2985597059739313, "HONDA CR-V 2016": 0.7745984062630755, "HYUNDAI IONIQ PHEV 2020": 2.5696218908589383, "GMC ACADIA DENALI 2018": 1.3310088601868082, "HYUNDAI SONATA 2019": 1.9736552675022665, "TOYOTA AVALON 2019": 1.7245149905226294, "TOYOTA C-HR 2018": 1.5895016960662856, "HONDA CR-V HYBRID 2019": 2.0687746810729193, "CHRYSLER PACIFICA 2020": 1.40536880000744, "HYUNDAI IONIQ ELECTRIC 2020": 3.3220838625838667, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 1.7753192756242595, "KIA OPTIMA SX 2019 & 2016": 3.12625562280304, "TOYOTA AVALON HYBRID 2019": 1.7681286449373381, "TOYOTA RAV4 HYBRID 2022": 2.5518187542231816, "HONDA PASSPORT 2021": 1.5174924139130355, "KIA K5 2021": 2.482916204106975, "ACURA ILX 2016": 1.5237423964720282, "HYUNDAI IONIQ HYBRID 2017-2019": 2.3723887901632645, "KIA NIRO EV 2020": 2.924651969180446, "SUBARU IMPREZA SPORT 2020": 2.5317689549587694, "CHRYSLER PACIFICA HYBRID 2017": 1.167126725149114, "HYUNDAI KONA ELECTRIC 2019": 4.201092987427836, "HYUNDAI ELANTRA HYBRID 2021": 3.7153193626001926, "HYUNDAI SANTA FE HYBRID 2022": 3.3049230586030545, "CHRYSLER PACIFICA 2018": 1.524867383058782, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 2.5970979517766213, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.5460982690267173, "MAZDA CX-9 2021": 1.9514800984278198, "HYUNDAI SANTA FE 2022": 3.5354982200434524, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.8902492836532216, "HONDA HRV 2019": 2.1262957371020352, "TOYOTA AVALON HYBRID 2022": 2.4142150048378683, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": 2.4086794443413906, "VOLKSWAGEN TAOS 1ST GEN": 2.0031666974545947, "KIA FORTE E 2018 & GT 2021": 2.022553820222557, "CADILLAC ESCALADE ESV 2016": 1.5522339636408988, "TOYOTA C-HR 2021": 1.6519334844316687, "TOYOTA C-HR HYBRID 2018": 1.3193315010905482}, "MAX_LAT_ACCEL_MEASURED": {"HONDA PILOT 2017": 0.9069354290994807, "HONDA CIVIC 2016": 0.4030275472529351, "TOYOTA CAMRY 2018": 1.686123168195758, "TOYOTA COROLLA HYBRID TSS2 2019": 1.9139332621491167, "TOYOTA RAV4 2019": 2.234047196286479, "HYUNDAI PALISADE 2020": 1.8303582523301922, "TOYOTA SIENNA 2018": 1.4752503435300715, "ACURA RDX 2020": 0.40911581320000334, "TOYOTA RAV4 2017": 1.6622227720995595, "HONDA RIDGELINE 2017": 0.8224685813281227, "TOYOTA PRIUS 2017": 1.4548827870876067, "TOYOTA HIGHLANDER HYBRID 2020": 2.0649784271823037, "HYUNDAI SONATA 2020": 2.243989856570093, "KIA STINGER GT2 2018": 1.9531287107084392, "TOYOTA HIGHLANDER 2020": 1.659381392090836, "HONDA ACCORD 2018": 0.40486739531686267, "TOYOTA PRIUS TSS2 2021": 1.861541601048098, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.1930739374812243, "LEXUS NX 2020": 1.565268724838564, "TOYOTA RAV4 HYBRID 2019": 2.0915384047218426, "HONDA CIVIC (BOSCH) 2019": 0.4062886118517984, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 0.35128914564548286, "LEXUS NX HYBRID 2018": 1.81821359787186, "TOYOTA COROLLA TSS2 2019": 1.911280958056631, "VOLKSWAGEN ARTEON 1ST GEN": 1.2587939472578302, "TOYOTA CAMRY HYBRID 2021": 2.312510643730013, "VOLKSWAGEN JETTA 7TH GEN": 1.232161945396623, "HONDA INSIGHT 2019": 0.5174836462945298, "SUBARU FORESTER 2019": 2.29255993930968, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.133978602602408, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 0.8254773781363679, "LEXUS RX 2016": 1.0954776820595344, "TOYOTA COROLLA 2017": 2.2012870528168964, "LEXUS ES 2019": 2.069508805495439, "HYUNDAI SANTA FE 2019": 2.3763720477660253, "TOYOTA AVALON 2022": 2.531962323786023, "JEEP GRAND CHEROKEE V6 2018": 1.4193323242487865, "CHEVROLET VOLT PREMIER 2017": 1.8576430337666092, "TOYOTA RAV4 HYBRID 2017": 1.7425797219020926, "LEXUS RX 2020": 1.5118835180227874, "TOYOTA HIGHLANDER HYBRID 2018": 1.6872527654528833, "TOYOTA CAMRY HYBRID 2018": 1.6793468378089895, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.5614447712441282, "TOYOTA MIRAI 2021": 2.271146483563897, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 1.6573774863189379, "HYUNDAI SONATA HYBRID 2021": 1.9464120717803253, "VOLKSWAGEN ATLAS 1ST GEN": 1.6867005451451638, "LEXUS ES HYBRID 2019": 1.956450687999482, "HYUNDAI GENESIS 2015-2016": 1.5359761378898085, "JEEP GRAND CHEROKEE 2019": 1.2418961305308847, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 0.2642062271814174, "HONDA FIT 2018": 0.5896345937094754, "TOYOTA CAMRY 2021": 2.1783533980215166, "AUDI Q3 2ND GEN": 1.1582239457022647, "AUDI A3 3RD GEN": 1.598699116126939, "LEXUS RX HYBRID 2017": 1.319771127672888, "HONDA CIVIC 2022": 1.1806949852580793, "GENESIS G70 2018": 2.2496820850331134, "CHRYSLER PACIFICA HYBRID 2018": 1.294798200968084, "VOLKSWAGEN PASSAT 8TH GEN": 1.247540921731637, "HONDA CR-V 2016": 0.6991119250342539, "HYUNDAI IONIQ PHEV 2020": 1.9062392690595655, "GMC ACADIA DENALI 2018": 1.2986994230652662, "HYUNDAI SONATA 2019": 1.257445187146704, "TOYOTA AVALON 2019": 1.664577368475227, "TOYOTA C-HR 2018": 1.308490445144888, "HONDA CR-V HYBRID 2019": 0.4693072746041504, "CHRYSLER PACIFICA 2020": 1.1712413003138664, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": 1.1573057001955744, "LEXUS NX 2018": 1.9457312007432144, "KIA OPTIMA SX 2019 & 2016": 2.0928228595938845, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 1.7647290773049569, "HONDA PASSPORT 2021": 0.8248357750132685, "KIA K5 2021": 1.4628018983720577, "ACURA ILX 2016": 0.6330753921140401, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 2.020186575503497, "SUBARU IMPREZA SPORT 2020": 2.136786720514988, "CHRYSLER PACIFICA HYBRID 2017": 1.0642918033307907, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 1.3654603720349934, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.255230465866663, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 3.3823387508235827, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.544104124172169, "HONDA HRV 2019": 0.7492792210307291, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.8127509604734238, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 1.6590543949912684, "KIA FORTE E 2018 & GT 2021": 2.3970573789339786, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 1.3559230155096402, "TOYOTA C-HR HYBRID 2018": 0.8910235787356033}, "FRICTION": {"HONDA PILOT 2017": 0.2168217463499328, "HONDA CIVIC 2016": 0.28406761310944795, "TOYOTA CAMRY 2018": 0.1327947477896041, "TOYOTA COROLLA HYBRID TSS2 2019": 0.21792021497538405, "TOYOTA RAV4 2019": 0.12757022360707945, "HYUNDAI PALISADE 2020": 0.13391574986922777, "TOYOTA SIENNA 2018": 0.1853443239485906, "ACURA RDX 2020": 0.18058553315570297, "TOYOTA RAV4 2017": 0.14319170324556796, "HONDA RIDGELINE 2017": 0.2380553573913589, "TOYOTA PRIUS 2017": 0.2079869382946584, "TOYOTA HIGHLANDER HYBRID 2020": 0.14038812589302646, "HYUNDAI SONATA 2020": 0.08266051305053168, "KIA STINGER GT2 2018": 0.11909534626930472, "TOYOTA HIGHLANDER 2020": 0.14658637853444048, "HONDA ACCORD 2018": 0.21616610462729247, "TOYOTA PRIUS TSS2 2021": 0.20613763260512002, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.16250373743651828, "LEXUS NX 2020": 0.14404022591302845, "TOYOTA RAV4 HYBRID 2019": 0.1319247989758836, "HONDA CIVIC (BOSCH) 2019": 0.2575217845562353, "KIA NIRO HYBRID 2021": 0.14468633728800306, "HONDA ACCORD HYBRID 2018": 0.21150723931119184, "LEXUS NX HYBRID 2018": 0.16117151597250162, "TOYOTA COROLLA TSS2 2019": 0.21045927995242877, "VOLKSWAGEN ARTEON 1ST GEN": 0.17828895368353925, "TOYOTA CAMRY HYBRID 2021": 0.16283734136957057, "VOLKSWAGEN JETTA 7TH GEN": 0.19508489725001105, "HONDA INSIGHT 2019": 0.25750800088299297, "SUBARU FORESTER 2019": 0.11783702069698135, "HYUNDAI ELANTRA 2021": 0.09377564130711125, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.14740189509875762, "HYUNDAI KONA HYBRID 2020": 0.0863709736632968, "HONDA ODYSSEY 2018": 0.2125595696498247, "LEXUS RX 2016": 0.21475140622981923, "TOYOTA COROLLA 2017": 0.12325064090161544, "LEXUS ES 2019": 0.12757526660498053, "HYUNDAI SANTA FE 2019": 0.12230125806479573, "TOYOTA AVALON 2022": 0.11030226705639488, "JEEP GRAND CHEROKEE V6 2018": 0.12871972792344108, "CHEVROLET VOLT PREMIER 2017": 0.16697256960295873, "TOYOTA RAV4 HYBRID 2017": 0.14074453855329072, "LEXUS RX 2020": 0.2249895411716623, "TOYOTA HIGHLANDER HYBRID 2018": 0.16692807938039034, "TOYOTA CAMRY HYBRID 2018": 0.13418904852016877, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.19324413131475543, "TOYOTA MIRAI 2021": 0.20035237756713503, "LEXUS IS 2018": 0.073103111226694, "TOYOTA HIGHLANDER 2017": 0.17502689439420385, "HYUNDAI SONATA HYBRID 2021": 0.09518615688045734, "VOLKSWAGEN ATLAS 1ST GEN": 0.12761803335799474, "LEXUS ES HYBRID 2019": 0.1682771025433274, "HYUNDAI GENESIS 2015-2016": 0.10254237048034251, "JEEP GRAND CHEROKEE 2019": 0.15702739382013717, "SUBARU ASCENT LIMITED 2019": 0.12936982863095342, "HONDA CR-V 2017": 0.22518506713451308, "HONDA FIT 2018": 0.10803295063463647, "TOYOTA CAMRY 2021": 0.15512845523424743, "AUDI Q3 2ND GEN": 0.14083949977629878, "AUDI A3 3RD GEN": 0.1611945965384188, "LEXUS RX HYBRID 2017": 0.19322020114452776, "HONDA CIVIC 2022": 0.24279247053469405, "GENESIS G70 2018": 0.06869638264150804, "CHRYSLER PACIFICA HYBRID 2018": 0.13887505891474383, "VOLKSWAGEN PASSAT 8TH GEN": 0.21714039653367842, "HONDA CR-V 2016": 0.41726236462791455, "HYUNDAI IONIQ PHEV 2020": 0.13800461817330806, "GMC ACADIA DENALI 2018": 0.3447163106452739, "HYUNDAI SONATA 2019": 0.15371520337813344, "TOYOTA AVALON 2019": 0.10392921606262978, "TOYOTA C-HR 2018": 0.2015190716953846, "HONDA CR-V HYBRID 2019": 0.19595630321202379, "CHRYSLER PACIFICA 2020": 0.14337114313208268, "HYUNDAI IONIQ ELECTRIC 2020": 0.08104502306679212, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.1471001686544422, "KIA OPTIMA SX 2019 & 2016": 0.11703652166984638, "TOYOTA AVALON HYBRID 2019": 0.10863628402866225, "TOYOTA RAV4 HYBRID 2022": 0.14334213238415072, "HONDA PASSPORT 2021": 0.19826160782809032, "KIA K5 2021": 0.1027179720106188, "ACURA ILX 2016": 0.35663988815912573, "HYUNDAI IONIQ HYBRID 2017-2019": 0.12332151728479951, "KIA NIRO EV 2020": 0.0892074288578785, "SUBARU IMPREZA SPORT 2020": 0.15841234487251604, "CHRYSLER PACIFICA HYBRID 2017": 0.1345638758810282, "HYUNDAI KONA ELECTRIC 2019": 0.08503096350356723, "HYUNDAI ELANTRA HYBRID 2021": 0.09887804390243872, "HYUNDAI SANTA FE HYBRID 2022": 0.11171499761140577, "CHRYSLER PACIFICA 2018": 0.13611561752951415, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 0.10502695501512567, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.21818156330777305, "MAZDA CX-9 2021": 0.1793735649504697, "HYUNDAI SANTA FE 2022": 0.09184808719698756, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.14050744688135813, "HONDA HRV 2019": 0.17840321248608593, "TOYOTA AVALON HYBRID 2022": 0.16159049452515487, "SUBARU IMPREZA LIMITED 2019": 0.20322553080306893, "GENESIS G80 2017": 0.07934444681782107, "VOLKSWAGEN TAOS 1ST GEN": 0.18276122764341485, "KIA FORTE E 2018 & GT 2021": 0.11406160665068436, "CADILLAC ESCALADE ESV 2016": 0.15063766975884627, "TOYOTA C-HR 2021": 0.22798633346500694, "TOYOTA C-HR HYBRID 2018": 0.2036375866375624}, "ERROR_RATIO": {"HONDA PILOT 2017": 0.6158457247286419, "HONDA CIVIC 2016": 2.0785618623350928, "TOYOTA CAMRY 2018": 0.17356565057429169, "TOYOTA COROLLA HYBRID TSS2 2019": 0.10094741777075293, "TOYOTA RAV4 2019": 0.11812042718338775, "HYUNDAI PALISADE 2020": 0.30639442561268304, "TOYOTA SIENNA 2018": 0.1117307389748361, "ACURA RDX 2020": 1.9801454495960717, "TOYOTA RAV4 2017": 0.08589486116378196, "HONDA RIDGELINE 2017": 0.4319851914417577, "TOYOTA PRIUS 2017": 0.17281316158588575, "TOYOTA HIGHLANDER HYBRID 2020": 0.046325388721577, "HYUNDAI SONATA 2020": 0.4109860794021653, "KIA STINGER GT2 2018": 0.3517628781488943, "TOYOTA HIGHLANDER 2020": 0.14155072865224166, "HONDA ACCORD 2018": 2.510398061115294, "TOYOTA PRIUS TSS2 2021": 0.13593456264106363, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.08794943266738546, "LEXUS NX 2020": 0.3743942573190866, "TOYOTA RAV4 HYBRID 2019": 0.0845492503791727, "HONDA CIVIC (BOSCH) 2019": 2.4329816697390063, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 2.9252406767451804, "LEXUS NX HYBRID 2018": 0.23060712246809048, "TOYOTA COROLLA TSS2 2019": 0.13822363784977285, "VOLKSWAGEN ARTEON 1ST GEN": 0.009661691674299285, "TOYOTA CAMRY HYBRID 2021": 0.029451711159377333, "VOLKSWAGEN JETTA 7TH GEN": 0.16591473170144055, "HONDA INSIGHT 2019": 1.3398692842898896, "SUBARU FORESTER 2019": 0.5269683780697442, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.02971857401969039, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 1.0245957242729038, "LEXUS RX 2016": 0.07392586589971588, "TOYOTA COROLLA 2017": 0.31336988069649124, "LEXUS ES 2019": 0.08933657038050916, "HYUNDAI SANTA FE 2019": 0.2276812089092099, "TOYOTA AVALON 2022": 0.07120118798045925, "JEEP GRAND CHEROKEE V6 2018": 0.2065164316228118, "CHEVROLET VOLT PREMIER 2017": 0.2316223989408518, "TOYOTA RAV4 HYBRID 2017": 0.055653752888652736, "LEXUS RX 2020": 0.047792182371008345, "TOYOTA HIGHLANDER HYBRID 2018": 0.019259474082467202, "TOYOTA CAMRY HYBRID 2018": 0.11949733140330816, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.1996863736436734, "TOYOTA MIRAI 2021": 0.11019259478417197, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 0.05279963713251727, "HYUNDAI SONATA HYBRID 2021": 0.3543918194389536, "VOLKSWAGEN ATLAS 1ST GEN": 0.21694647502209782, "LEXUS ES HYBRID 2019": 0.14775474433507507, "HYUNDAI GENESIS 2015-2016": 0.0814892037361157, "JEEP GRAND CHEROKEE 2019": 0.3126997097753535, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 5.652613829506629, "HONDA FIT 2018": 1.5217432826711779, "TOYOTA CAMRY 2021": 0.07910435053686729, "AUDI Q3 2ND GEN": 0.13535089102138698, "AUDI A3 3RD GEN": 0.14353941401245793, "LEXUS RX HYBRID 2017": 0.048663813961824696, "HONDA CIVIC 2022": 1.0748206908458815, "GENESIS G70 2018": 0.688303429295532, "CHRYSLER PACIFICA HYBRID 2018": 0.11083725786301112, "VOLKSWAGEN PASSAT 8TH GEN": 0.13315924904555493, "HONDA CR-V 2016": 0.488871482749128, "HYUNDAI IONIQ PHEV 2020": 0.2756096845519595, "GMC ACADIA DENALI 2018": 0.24055364003040136, "HYUNDAI SONATA 2019": 0.4473315280277132, "TOYOTA AVALON 2019": 0.026428086100632363, "TOYOTA C-HR 2018": 0.06075105822970755, "HONDA CR-V HYBRID 2019": 2.9906016360828276, "CHRYSLER PACIFICA 2020": 0.07748732608487266, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.16318394527060903, "KIA OPTIMA SX 2019 & 2016": 0.4378756841929454, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 0.36478548056633514, "HONDA PASSPORT 2021": 0.5993860184637646, "KIA K5 2021": 0.6271500841947655, "ACURA ILX 2016": 0.8435442647921855, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 0.40355577782011604, "SUBARU IMPREZA SPORT 2020": 0.11071291640854522, "CHRYSLER PACIFICA HYBRID 2017": 0.029812269495458284, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 0.01705753895996445, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.05790668871480552, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 0.018126919430513307, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.1331760659016062, "HONDA HRV 2019": 1.599688433820939, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.2514545160390271, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 0.09725484306423876, "KIA FORTE E 2018 & GT 2021": 0.20381871942480628, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 0.05016813984196128, "TOYOTA C-HR HYBRID 2018": 0.2521485862766935}} \ No newline at end of file +{"LAT_ACCEL_FACTOR": {"HONDA PILOT 2017": 1.682289482065265, "HONDA CIVIC 2016": 1.5248128495527884, "TOYOTA CAMRY 2018": 2.1115709806216447, "TOYOTA COROLLA HYBRID TSS2 2019": 2.3250600977240077, "TOYOTA RAV4 2019": 2.625504029066767, "HYUNDAI PALISADE 2020": 2.5250855675875634, "TOYOTA SIENNA 2018": 1.8254254785341577, "ACURA RDX 2020": 1.3998101622214894, "TOYOTA RAV4 2017": 1.948190869577896, "HONDA RIDGELINE 2017": 1.4158181862793415, "TOYOTA PRIUS 2017": 1.9142926195557595, "TOYOTA HIGHLANDER HYBRID 2020": 2.1097056247344392, "HYUNDAI SONATA 2020": 3.2488989629905944, "KIA STINGER GT2 2018": 2.7592622336517834, "TOYOTA HIGHLANDER 2020": 2.0408544157877055, "HONDA ACCORD 2018": 1.6374118241564064, "TOYOTA PRIUS TSS2 2021": 2.3207270770298365, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.46050785084946, "LEXUS NX 2020": 2.29533657249232, "TOYOTA RAV4 HYBRID 2019": 2.4003012079562085, "HONDA CIVIC (BOSCH) 2019": 1.6523031416671652, "KIA NIRO HYBRID 2021": 2.743464625803003, "HONDA ACCORD HYBRID 2018": 1.5904016830979033, "LEXUS NX HYBRID 2018": 2.398678119681945, "TOYOTA COROLLA TSS2 2019": 2.3859244449846466, "VOLKSWAGEN ARTEON 1ST GEN": 1.4249208219414902, "TOYOTA CAMRY HYBRID 2021": 2.5434553806317055, "VOLKSWAGEN JETTA 7TH GEN": 1.2228130240634283, "HONDA INSIGHT 2019": 1.468352089969897, "SUBARU FORESTER 2019": 3.6185035528523546, "HYUNDAI ELANTRA 2021": 3.5294999663335185, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.2179616966432905, "HYUNDAI KONA HYBRID 2020": 4.493208192966529, "HONDA ODYSSEY 2018": 1.8838175399087222, "LEXUS RX 2016": 1.3912132245094184, "TOYOTA COROLLA 2017": 3.0143547548384735, "LEXUS ES 2019": 2.012201253045193, "HYUNDAI SANTA FE 2019": 3.039728566484244, "TOYOTA AVALON 2022": 2.4619858654670885, "JEEP GRAND CHEROKEE V6 2018": 1.8411674990629987, "CHEVROLET VOLT PREMIER 2017": 1.5943438675127841, "TOYOTA RAV4 HYBRID 2017": 1.9803053616868995, "LEXUS RX 2020": 1.664616846377383, "TOYOTA HIGHLANDER HYBRID 2018": 1.8866764457400844, "TOYOTA CAMRY HYBRID 2018": 2.014213351947917, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.4428896585442685, "TOYOTA MIRAI 2021": 2.7217623852898853, "LEXUS IS 2018": 3.5624668608596837, "TOYOTA HIGHLANDER 2017": 1.9199133105823853, "HYUNDAI SONATA HYBRID 2021": 2.7313907441569554, "VOLKSWAGEN ATLAS 1ST GEN": 1.4483948408160645, "LEXUS ES HYBRID 2019": 2.4138026617523547, "HYUNDAI GENESIS 2015-2016": 1.7636839808044658, "JEEP GRAND CHEROKEE 2019": 1.787264083939164, "SUBARU ASCENT LIMITED 2019": 3.0494069339774565, "HONDA CR-V 2017": 1.9828470679233807, "HONDA FIT 2018": 1.594940026552055, "TOYOTA CAMRY 2021": 2.5057990840460342, "AUDI Q3 2ND GEN": 1.4558300885316715, "AUDI A3 3RD GEN": 1.5304173783542625, "LEXUS RX HYBRID 2017": 1.577216425446677, "HONDA CIVIC 2022": 2.69252285552613, "GENESIS G70 2018": 3.866842361627636, "CHRYSLER PACIFICA HYBRID 2018": 1.5771851419640903, "VOLKSWAGEN PASSAT 8TH GEN": 1.2985597059739313, "HONDA CR-V 2016": 0.7745984062630755, "HYUNDAI IONIQ PHEV 2020": 2.5696218908589383, "GMC ACADIA DENALI 2018": 1.3310088601868082, "HYUNDAI SONATA 2019": 1.9736552675022665, "TOYOTA AVALON 2019": 1.7245149905226294, "TOYOTA C-HR 2018": 1.5895016960662856, "HONDA CR-V HYBRID 2019": 2.0687746810729193, "CHRYSLER PACIFICA 2020": 1.40536880000744, "HYUNDAI IONIQ ELECTRIC 2020": 3.3220838625838667, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 1.7753192756242595, "KIA OPTIMA SX 2019 & 2016": 3.12625562280304, "TOYOTA AVALON HYBRID 2019": 1.7681286449373381, "TOYOTA RAV4 HYBRID 2022": 2.5518187542231816, "HONDA PASSPORT 2021": 1.5174924139130355, "KIA K5 2021": 2.482916204106975, "ACURA ILX 2016": 1.5237423964720282, "HYUNDAI IONIQ HYBRID 2017-2019": 2.3723887901632645, "KIA NIRO EV 2020": 2.924651969180446, "SUBARU IMPREZA SPORT 2020": 2.5317689549587694, "CHRYSLER PACIFICA HYBRID 2017": 1.167126725149114, "HYUNDAI KONA ELECTRIC 2019": 4.201092987427836, "HYUNDAI ELANTRA HYBRID 2021": 3.7153193626001926, "HYUNDAI SANTA FE HYBRID 2022": 3.3049230586030545, "CHRYSLER PACIFICA 2018": 1.524867383058782, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 2.5970979517766213, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.5460982690267173, "MAZDA CX-9 2021": 1.9514800984278198, "HYUNDAI SANTA FE 2022": 3.5354982200434524, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.8902492836532216, "HONDA HRV 2019": 2.1262957371020352, "TOYOTA AVALON HYBRID 2022": 2.4142150048378683, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": 2.4086794443413906, "VOLKSWAGEN TAOS 1ST GEN": 2.0031666974545947, "KIA FORTE E 2018 & GT 2021": 2.022553820222557, "CADILLAC ESCALADE ESV 2016": 1.5522339636408988, "TOYOTA C-HR 2021": 1.6519334844316687, "TOYOTA C-HR HYBRID 2018": 1.3193315010905482}, "MAX_LAT_ACCEL_MEASURED": {"HONDA PILOT 2017": 0.9069354290994807, "HONDA CIVIC 2016": 0.4030275472529351, "TOYOTA CAMRY 2018": 1.686123168195758, "TOYOTA COROLLA HYBRID TSS2 2019": 1.9139332621491167, "TOYOTA RAV4 2019": 2.234047196286479, "HYUNDAI PALISADE 2020": 1.8303582523301922, "TOYOTA SIENNA 2018": 1.4752503435300715, "ACURA RDX 2020": 0.40911581320000334, "TOYOTA RAV4 2017": 1.6622227720995595, "HONDA RIDGELINE 2017": 0.8224685813281227, "TOYOTA PRIUS 2017": 1.4548827870876067, "TOYOTA HIGHLANDER HYBRID 2020": 2.0649784271823037, "HYUNDAI SONATA 2020": 2.243989856570093, "KIA STINGER GT2 2018": 1.9531287107084392, "TOYOTA HIGHLANDER 2020": 1.659381392090836, "HONDA ACCORD 2018": 0.40486739531686267, "TOYOTA PRIUS TSS2 2021": 1.861541601048098, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.1930739374812243, "LEXUS NX 2020": 1.565268724838564, "TOYOTA RAV4 HYBRID 2019": 2.0915384047218426, "HONDA CIVIC (BOSCH) 2019": 0.4062886118517984, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 0.35128914564548286, "LEXUS NX HYBRID 2018": 1.81821359787186, "TOYOTA COROLLA TSS2 2019": 1.911280958056631, "VOLKSWAGEN ARTEON 1ST GEN": 1.2587939472578302, "TOYOTA CAMRY HYBRID 2021": 2.312510643730013, "VOLKSWAGEN JETTA 7TH GEN": 1.232161945396623, "HONDA INSIGHT 2019": 0.5174836462945298, "SUBARU FORESTER 2019": 2.29255993930968, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.133978602602408, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 0.8254773781363679, "LEXUS RX 2016": 1.0954776820595344, "TOYOTA COROLLA 2017": 2.2012870528168964, "LEXUS ES 2019": 2.069508805495439, "HYUNDAI SANTA FE 2019": 2.3763720477660253, "TOYOTA AVALON 2022": 2.531962323786023, "JEEP GRAND CHEROKEE V6 2018": 1.4193323242487865, "CHEVROLET VOLT PREMIER 2017": 1.8576430337666092, "TOYOTA RAV4 HYBRID 2017": 1.7425797219020926, "LEXUS RX 2020": 1.5118835180227874, "TOYOTA HIGHLANDER HYBRID 2018": 1.6872527654528833, "TOYOTA CAMRY HYBRID 2018": 1.6793468378089895, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.5614447712441282, "TOYOTA MIRAI 2021": 2.271146483563897, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 1.6573774863189379, "HYUNDAI SONATA HYBRID 2021": 1.9464120717803253, "VOLKSWAGEN ATLAS 1ST GEN": 1.6867005451451638, "LEXUS ES HYBRID 2019": 1.956450687999482, "HYUNDAI GENESIS 2015-2016": 1.5359761378898085, "JEEP GRAND CHEROKEE 2019": 1.2418961305308847, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 0.2642062271814174, "HONDA FIT 2018": 0.5896345937094754, "TOYOTA CAMRY 2021": 2.1783533980215166, "AUDI Q3 2ND GEN": 1.1582239457022647, "AUDI A3 3RD GEN": 1.598699116126939, "LEXUS RX HYBRID 2017": 1.319771127672888, "HONDA CIVIC 2022": 1.1806949852580793, "GENESIS G70 2018": 2.2496820850331134, "CHRYSLER PACIFICA HYBRID 2018": 1.294798200968084, "VOLKSWAGEN PASSAT 8TH GEN": 1.247540921731637, "HONDA CR-V 2016": 0.6991119250342539, "HYUNDAI IONIQ PHEV 2020": 1.9062392690595655, "GMC ACADIA DENALI 2018": 1.2986994230652662, "HYUNDAI SONATA 2019": 1.257445187146704, "TOYOTA AVALON 2019": 1.664577368475227, "TOYOTA C-HR 2018": 1.308490445144888, "HONDA CR-V HYBRID 2019": 0.4693072746041504, "CHRYSLER PACIFICA 2020": 1.1712413003138664, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": 1.1573057001955744, "LEXUS NX 2018": 1.9457312007432144, "KIA OPTIMA SX 2019 & 2016": 2.0928228595938845, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 1.7647290773049569, "HONDA PASSPORT 2021": 0.8248357750132685, "KIA K5 2021": 1.4628018983720577, "ACURA ILX 2016": 0.6330753921140401, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 2.020186575503497, "SUBARU IMPREZA SPORT 2020": 2.136786720514988, "CHRYSLER PACIFICA HYBRID 2017": 1.0642918033307907, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 1.3654603720349934, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.255230465866663, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 3.3823387508235827, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.544104124172169, "HONDA HRV 2019": 0.7492792210307291, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 1.6590543949912684, "KIA FORTE E 2018 & GT 2021": 2.3970573789339786, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 1.3559230155096402, "TOYOTA C-HR HYBRID 2018": 1.271271459066948}, "FRICTION": {"HONDA PILOT 2017": 0.2168217463499328, "HONDA CIVIC 2016": 0.28406761310944795, "TOYOTA CAMRY 2018": 0.1327947477896041, "TOYOTA COROLLA HYBRID TSS2 2019": 0.21792021497538405, "TOYOTA RAV4 2019": 0.12757022360707945, "HYUNDAI PALISADE 2020": 0.13391574986922777, "TOYOTA SIENNA 2018": 0.1853443239485906, "ACURA RDX 2020": 0.18058553315570297, "TOYOTA RAV4 2017": 0.14319170324556796, "HONDA RIDGELINE 2017": 0.2380553573913589, "TOYOTA PRIUS 2017": 0.2079869382946584, "TOYOTA HIGHLANDER HYBRID 2020": 0.14038812589302646, "HYUNDAI SONATA 2020": 0.08266051305053168, "KIA STINGER GT2 2018": 0.11909534626930472, "TOYOTA HIGHLANDER 2020": 0.14658637853444048, "HONDA ACCORD 2018": 0.21616610462729247, "TOYOTA PRIUS TSS2 2021": 0.20613763260512002, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.16250373743651828, "LEXUS NX 2020": 0.14404022591302845, "TOYOTA RAV4 HYBRID 2019": 0.1319247989758836, "HONDA CIVIC (BOSCH) 2019": 0.2575217845562353, "KIA NIRO HYBRID 2021": 0.14468633728800306, "HONDA ACCORD HYBRID 2018": 0.21150723931119184, "LEXUS NX HYBRID 2018": 0.16117151597250162, "TOYOTA COROLLA TSS2 2019": 0.21045927995242877, "VOLKSWAGEN ARTEON 1ST GEN": 0.17828895368353925, "TOYOTA CAMRY HYBRID 2021": 0.16283734136957057, "VOLKSWAGEN JETTA 7TH GEN": 0.19508489725001105, "HONDA INSIGHT 2019": 0.25750800088299297, "SUBARU FORESTER 2019": 0.11783702069698135, "HYUNDAI ELANTRA 2021": 0.09377564130711125, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.14740189509875762, "HYUNDAI KONA HYBRID 2020": 0.0863709736632968, "HONDA ODYSSEY 2018": 0.2125595696498247, "LEXUS RX 2016": 0.21475140622981923, "TOYOTA COROLLA 2017": 0.12325064090161544, "LEXUS ES 2019": 0.12757526660498053, "HYUNDAI SANTA FE 2019": 0.12230125806479573, "TOYOTA AVALON 2022": 0.11030226705639488, "JEEP GRAND CHEROKEE V6 2018": 0.12871972792344108, "CHEVROLET VOLT PREMIER 2017": 0.16697256960295873, "TOYOTA RAV4 HYBRID 2017": 0.14074453855329072, "LEXUS RX 2020": 0.2249895411716623, "TOYOTA HIGHLANDER HYBRID 2018": 0.16692807938039034, "TOYOTA CAMRY HYBRID 2018": 0.13418904852016877, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.19324413131475543, "TOYOTA MIRAI 2021": 0.20035237756713503, "LEXUS IS 2018": 0.073103111226694, "TOYOTA HIGHLANDER 2017": 0.17502689439420385, "HYUNDAI SONATA HYBRID 2021": 0.09518615688045734, "VOLKSWAGEN ATLAS 1ST GEN": 0.12761803335799474, "LEXUS ES HYBRID 2019": 0.1682771025433274, "HYUNDAI GENESIS 2015-2016": 0.10254237048034251, "JEEP GRAND CHEROKEE 2019": 0.15702739382013717, "SUBARU ASCENT LIMITED 2019": 0.12936982863095342, "HONDA CR-V 2017": 0.22518506713451308, "HONDA FIT 2018": 0.10803295063463647, "TOYOTA CAMRY 2021": 0.15512845523424743, "AUDI Q3 2ND GEN": 0.14083949977629878, "AUDI A3 3RD GEN": 0.1611945965384188, "LEXUS RX HYBRID 2017": 0.19322020114452776, "HONDA CIVIC 2022": 0.24279247053469405, "GENESIS G70 2018": 0.06869638264150804, "CHRYSLER PACIFICA HYBRID 2018": 0.13887505891474383, "VOLKSWAGEN PASSAT 8TH GEN": 0.21714039653367842, "HONDA CR-V 2016": 0.41726236462791455, "HYUNDAI IONIQ PHEV 2020": 0.13800461817330806, "GMC ACADIA DENALI 2018": 0.3447163106452739, "HYUNDAI SONATA 2019": 0.15371520337813344, "TOYOTA AVALON 2019": 0.10392921606262978, "TOYOTA C-HR 2018": 0.2015190716953846, "HONDA CR-V HYBRID 2019": 0.19595630321202379, "CHRYSLER PACIFICA 2020": 0.14337114313208268, "HYUNDAI IONIQ ELECTRIC 2020": 0.08104502306679212, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.1471001686544422, "KIA OPTIMA SX 2019 & 2016": 0.11703652166984638, "TOYOTA AVALON HYBRID 2019": 0.10863628402866225, "TOYOTA RAV4 HYBRID 2022": 0.14334213238415072, "HONDA PASSPORT 2021": 0.19826160782809032, "KIA K5 2021": 0.1027179720106188, "ACURA ILX 2016": 0.35663988815912573, "HYUNDAI IONIQ HYBRID 2017-2019": 0.12332151728479951, "KIA NIRO EV 2020": 0.0892074288578785, "SUBARU IMPREZA SPORT 2020": 0.15841234487251604, "CHRYSLER PACIFICA HYBRID 2017": 0.1345638758810282, "HYUNDAI KONA ELECTRIC 2019": 0.08503096350356723, "HYUNDAI ELANTRA HYBRID 2021": 0.09887804390243872, "HYUNDAI SANTA FE HYBRID 2022": 0.11171499761140577, "CHRYSLER PACIFICA 2018": 0.13611561752951415, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 0.10502695501512567, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.21818156330777305, "MAZDA CX-9 2021": 0.1793735649504697, "HYUNDAI SANTA FE 2022": 0.09184808719698756, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.14050744688135813, "HONDA HRV 2019": 0.17840321248608593, "TOYOTA AVALON HYBRID 2022": 0.16159049452515487, "SUBARU IMPREZA LIMITED 2019": 0.20322553080306893, "GENESIS G80 2017": 0.07934444681782107, "VOLKSWAGEN TAOS 1ST GEN": 0.18276122764341485, "KIA FORTE E 2018 & GT 2021": 0.11406160665068436, "CADILLAC ESCALADE ESV 2016": 0.15063766975884627, "TOYOTA C-HR 2021": 0.22798633346500694, "TOYOTA C-HR HYBRID 2018": 0.2036375866375624}, "ERROR_RATIO": {"HONDA PILOT 2017": 0.6158457247286419, "HONDA CIVIC 2016": 2.0785618623350928, "TOYOTA CAMRY 2018": 0.17356565057429169, "TOYOTA COROLLA HYBRID TSS2 2019": 0.10094741777075293, "TOYOTA RAV4 2019": 0.11812042718338775, "HYUNDAI PALISADE 2020": 0.30639442561268304, "TOYOTA SIENNA 2018": 0.1117307389748361, "ACURA RDX 2020": 1.9801454495960717, "TOYOTA RAV4 2017": 0.08589486116378196, "HONDA RIDGELINE 2017": 0.4319851914417577, "TOYOTA PRIUS 2017": 0.17281316158588575, "TOYOTA HIGHLANDER HYBRID 2020": 0.046325388721577, "HYUNDAI SONATA 2020": 0.4109860794021653, "KIA STINGER GT2 2018": 0.3517628781488943, "TOYOTA HIGHLANDER 2020": 0.14155072865224166, "HONDA ACCORD 2018": 2.510398061115294, "TOYOTA PRIUS TSS2 2021": 0.13593456264106363, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.08794943266738546, "LEXUS NX 2020": 0.3743942573190866, "TOYOTA RAV4 HYBRID 2019": 0.0845492503791727, "HONDA CIVIC (BOSCH) 2019": 2.4329816697390063, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 2.9252406767451804, "LEXUS NX HYBRID 2018": 0.23060712246809048, "TOYOTA COROLLA TSS2 2019": 0.13822363784977285, "VOLKSWAGEN ARTEON 1ST GEN": 0.009661691674299285, "TOYOTA CAMRY HYBRID 2021": 0.029451711159377333, "VOLKSWAGEN JETTA 7TH GEN": 0.16591473170144055, "HONDA INSIGHT 2019": 1.3398692842898896, "SUBARU FORESTER 2019": 0.5269683780697442, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.02971857401969039, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 1.0245957242729038, "LEXUS RX 2016": 0.07392586589971588, "TOYOTA COROLLA 2017": 0.31336988069649124, "LEXUS ES 2019": 0.08933657038050916, "HYUNDAI SANTA FE 2019": 0.2276812089092099, "TOYOTA AVALON 2022": 0.07120118798045925, "JEEP GRAND CHEROKEE V6 2018": 0.2065164316228118, "CHEVROLET VOLT PREMIER 2017": 0.2316223989408518, "TOYOTA RAV4 HYBRID 2017": 0.055653752888652736, "LEXUS RX 2020": 0.047792182371008345, "TOYOTA HIGHLANDER HYBRID 2018": 0.019259474082467202, "TOYOTA CAMRY HYBRID 2018": 0.11949733140330816, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.1996863736436734, "TOYOTA MIRAI 2021": 0.11019259478417197, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 0.05279963713251727, "HYUNDAI SONATA HYBRID 2021": 0.3543918194389536, "VOLKSWAGEN ATLAS 1ST GEN": 0.21694647502209782, "LEXUS ES HYBRID 2019": 0.14775474433507507, "HYUNDAI GENESIS 2015-2016": 0.0814892037361157, "JEEP GRAND CHEROKEE 2019": 0.3126997097753535, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 5.652613829506629, "HONDA FIT 2018": 1.5217432826711779, "TOYOTA CAMRY 2021": 0.07910435053686729, "AUDI Q3 2ND GEN": 0.13535089102138698, "AUDI A3 3RD GEN": 0.14353941401245793, "LEXUS RX HYBRID 2017": 0.048663813961824696, "HONDA CIVIC 2022": 1.0748206908458815, "GENESIS G70 2018": 0.688303429295532, "CHRYSLER PACIFICA HYBRID 2018": 0.11083725786301112, "VOLKSWAGEN PASSAT 8TH GEN": 0.13315924904555493, "HONDA CR-V 2016": 0.488871482749128, "HYUNDAI IONIQ PHEV 2020": 0.2756096845519595, "GMC ACADIA DENALI 2018": 0.24055364003040136, "HYUNDAI SONATA 2019": 0.4473315280277132, "TOYOTA AVALON 2019": 0.026428086100632363, "TOYOTA C-HR 2018": 0.06075105822970755, "HONDA CR-V HYBRID 2019": 2.9906016360828276, "CHRYSLER PACIFICA 2020": 0.07748732608487266, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.16318394527060903, "KIA OPTIMA SX 2019 & 2016": 0.4378756841929454, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 0.36478548056633514, "HONDA PASSPORT 2021": 0.5993860184637646, "KIA K5 2021": 0.6271500841947655, "ACURA ILX 2016": 0.8435442647921855, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 0.40355577782011604, "SUBARU IMPREZA SPORT 2020": 0.11071291640854522, "CHRYSLER PACIFICA HYBRID 2017": 0.029812269495458284, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 0.01705753895996445, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.05790668871480552, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 0.018126919430513307, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.1331760659016062, "HONDA HRV 2019": 1.599688433820939, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.2514545160390271, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 0.09725484306423876, "KIA FORTE E 2018 & GT 2021": 0.20381871942480628, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 0.05016813984196128, "TOYOTA C-HR HYBRID 2018": 0.2521485862766935}} \ No newline at end of file diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index b1c6da922bc843..32f66b6fa0cbe5 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -99,6 +99,7 @@ class Footnote(Enum): class ToyotaCarInfo(CarInfo): package: str = "All" harness: Enum = Harness.toyota + good_torque: bool = True CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b2623dc8dce9d2..05d016911b0276 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -4efb28766e386de3117819b8a128aa782aaadf2a \ No newline at end of file +2d736ca51acc1ac06216631b0529b50d9a6d2170 \ No newline at end of file From 88f246b909e3e111ac84e964bb811efefd923163 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 22 Jun 2022 11:45:38 +0200 Subject: [PATCH 151/436] ui: change alerts to Inter (#24937) * ui: change alerts to Inter * fix up test --- selfdrive/controls/tests/test_alerts.py | 17 +++++++---------- selfdrive/ui/qt/onroad.cc | 10 +++++----- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/selfdrive/controls/tests/test_alerts.py b/selfdrive/controls/tests/test_alerts.py index 2bd904b575e7ae..79c56d6bc0a952 100755 --- a/selfdrive/controls/tests/test_alerts.py +++ b/selfdrive/controls/tests/test_alerts.py @@ -48,20 +48,17 @@ def test_events_defined(self): # ensure alert text doesn't exceed allowed width def test_alert_text_length(self): font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts") - regular_font_path = os.path.join(font_path, "opensans_semibold.ttf") - bold_font_path = os.path.join(font_path, "opensans_semibold.ttf") - semibold_font_path = os.path.join(font_path, "opensans_semibold.ttf") - - max_text_width = 1920 - 300 # full screen width is useable, minus sidebar - # TODO: get exact scale factor. found this empirically, works well enough - font_scale_factor = 1.55 # factor to scale from nanovg units to PIL + regular_font_path = os.path.join(font_path, "Inter-SemiBold.ttf") + bold_font_path = os.path.join(font_path, "Inter-Bold.ttf") + semibold_font_path = os.path.join(font_path, "Inter-SemiBold.ttf") + max_text_width = 2160 - 300 # full screen width is useable, minus sidebar draw = ImageDraw.Draw(Image.new('RGB', (0, 0))) fonts = { - AlertSize.small: [ImageFont.truetype(semibold_font_path, int(40 * font_scale_factor))], - AlertSize.mid: [ImageFont.truetype(bold_font_path, int(48 * font_scale_factor)), - ImageFont.truetype(regular_font_path, int(36 * font_scale_factor))], + AlertSize.small: [ImageFont.truetype(semibold_font_path, 74)], + AlertSize.mid: [ImageFont.truetype(bold_font_path, 88), + ImageFont.truetype(regular_font_path, 66)], } for alert in ALERTS: diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index d92cf36b894fad..98b70c6b6ebdfa 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -146,18 +146,18 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { p.setPen(QColor(0xff, 0xff, 0xff)); p.setRenderHint(QPainter::TextAntialiasing); if (alert.size == cereal::ControlsState::AlertSize::SMALL) { - configFont(p, "Open Sans", 74, "SemiBold"); + configFont(p, "Inter", 74, "SemiBold"); p.drawText(r, Qt::AlignCenter, alert.text1); } else if (alert.size == cereal::ControlsState::AlertSize::MID) { - configFont(p, "Open Sans", 88, "Bold"); + configFont(p, "Inter", 88, "Bold"); p.drawText(QRect(0, c.y() - 125, width(), 150), Qt::AlignHCenter | Qt::AlignTop, alert.text1); - configFont(p, "Open Sans", 66, "Regular"); + configFont(p, "Inter", 66, "Regular"); p.drawText(QRect(0, c.y() + 21, width(), 90), Qt::AlignHCenter, alert.text2); } else if (alert.size == cereal::ControlsState::AlertSize::FULL) { bool l = alert.text1.length() > 15; - configFont(p, "Open Sans", l ? 132 : 177, "Bold"); + configFont(p, "Inter", l ? 132 : 177, "Bold"); p.drawText(QRect(0, r.y() + (l ? 240 : 270), width(), 600), Qt::AlignHCenter | Qt::TextWordWrap, alert.text1); - configFont(p, "Open Sans", 88, "Regular"); + configFont(p, "Inter", 88, "Regular"); p.drawText(QRect(0, r.height() - (l ? 361 : 420), width(), 300), Qt::AlignHCenter | Qt::TextWordWrap, alert.text2); } } From 963de402115a4eb8eb7944c5f5233e47dea31543 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 22 Jun 2022 11:46:00 +0200 Subject: [PATCH 152/436] ui: change sidebar font to Inter (#24931) * change sidebar to inter * clean up metric color rect radius * fix text placement * simplify rect placement --- selfdrive/ui/qt/sidebar.cc | 39 ++++++++++++++++++++++---------------- selfdrive/ui/qt/sidebar.h | 4 ++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 312d8d8a5984d5..00e72b352c4d93 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -4,13 +4,13 @@ #include "selfdrive/ui/qt/util.h" -void Sidebar::drawMetric(QPainter &p, const QString &label, QColor c, int y) { - const QRect rect = {30, y, 240, label.contains("\n") ? 124 : 100}; +void Sidebar::drawMetric(QPainter &p, const QPair &label, QColor c, int y) { + const QRect rect = {30, y, 240, 126}; p.setPen(Qt::NoPen); p.setBrush(QBrush(c)); - p.setClipRect(rect.x() + 6, rect.y(), 18, rect.height(), Qt::ClipOperation::ReplaceClip); - p.drawRoundedRect(QRect(rect.x() + 6, rect.y() + 6, 100, rect.height() - 12), 10, 10); + p.setClipRect(rect.x() + 4, rect.y(), 18, rect.height(), Qt::ClipOperation::ReplaceClip); + p.drawRoundedRect(QRect(rect.x() + 4, rect.y() + 4, 100, 118), 18, 18); p.setClipping(false); QPen pen = QPen(QColor(0xff, 0xff, 0xff, 0x55)); @@ -20,9 +20,16 @@ void Sidebar::drawMetric(QPainter &p, const QString &label, QColor c, int y) { p.drawRoundedRect(rect, 20, 20); p.setPen(QColor(0xff, 0xff, 0xff)); - configFont(p, "Open Sans", 35, "Bold"); - const QRect r = QRect(rect.x() + 30, rect.y(), rect.width() - 40, rect.height()); - p.drawText(r, Qt::AlignCenter, label); + configFont(p, "Inter", 35, "SemiBold"); + + QRect label_rect = getTextRect(p, Qt::AlignCenter, label.first); + label_rect.setWidth(218); + label_rect.moveLeft(rect.left() + 22); + label_rect.moveTop(rect.top() + 19); + p.drawText(label_rect, Qt::AlignCenter, label.first); + + label_rect.moveTop(rect.top() + 65); + p.drawText(label_rect, Qt::AlignCenter, label.second); } Sidebar::Sidebar(QWidget *parent) : QFrame(parent) { @@ -57,26 +64,26 @@ void Sidebar::updateState(const UIState &s) { ItemStatus connectStatus; auto last_ping = deviceState.getLastAthenaPingTime(); if (last_ping == 0) { - connectStatus = ItemStatus{"CONNECT\nOFFLINE", warning_color}; + connectStatus = ItemStatus{{"CONNECT", "OFFLINE"}, warning_color}; } else { - connectStatus = nanos_since_boot() - last_ping < 80e9 ? ItemStatus{"CONNECT\nONLINE", good_color} : ItemStatus{"CONNECT\nERROR", danger_color}; + connectStatus = nanos_since_boot() - last_ping < 80e9 ? ItemStatus{{"CONNECT", "ONLINE"}, good_color} : ItemStatus{{"CONNECT", "ERROR"}, danger_color}; } setProperty("connectStatus", QVariant::fromValue(connectStatus)); - ItemStatus tempStatus = {"TEMP\nHIGH", danger_color}; + ItemStatus tempStatus = {{"TEMP", "HIGH"}, danger_color}; auto ts = deviceState.getThermalStatus(); if (ts == cereal::DeviceState::ThermalStatus::GREEN) { - tempStatus = {"TEMP\nGOOD", good_color}; + tempStatus = {{"TEMP", "GOOD"}, good_color}; } else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { - tempStatus = {"TEMP\nOK", warning_color}; + tempStatus = {{"TEMP", "OK"}, warning_color}; } setProperty("tempStatus", QVariant::fromValue(tempStatus)); - ItemStatus pandaStatus = {"VEHICLE\nONLINE", good_color}; + ItemStatus pandaStatus = {{"VEHICLE", "ONLINE"}, good_color}; if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { - pandaStatus = {"NO\nPANDA", danger_color}; + pandaStatus = {{"NO", "PANDA"}, danger_color}; } else if (s.scene.started && !sm["liveLocationKalman"].getLiveLocationKalman().getGpsOK()) { - pandaStatus = {"GPS\nSEARCH", warning_color}; + pandaStatus = {{"GPS", "SEARCH"}, warning_color}; } setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); } @@ -103,7 +110,7 @@ void Sidebar::paintEvent(QPaintEvent *event) { x += 37; } - configFont(p, "Open Sans", 35, "Regular"); + configFont(p, "Inter", 35, "Regular"); p.setPen(QColor(0xff, 0xff, 0xff)); const QRect r = QRect(50, 247, 100, 50); p.drawText(r, Qt::AlignCenter, net_type); diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index ab3e990e61c73e..98ae6564d6ba5b 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -6,7 +6,7 @@ #include "common/params.h" #include "selfdrive/ui/ui.h" -typedef QPair ItemStatus; +typedef QPair, QColor> ItemStatus; Q_DECLARE_METATYPE(ItemStatus); class Sidebar : public QFrame { @@ -30,7 +30,7 @@ public slots: protected: void paintEvent(QPaintEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; - void drawMetric(QPainter &p, const QString &label, QColor c, int y); + void drawMetric(QPainter &p, const QPair &label, QColor c, int y); QPixmap home_img, settings_img; const QMap network_type = { From 05e7ce731f4259a569f5d28392a0d576e90dec1b Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 22 Jun 2022 11:46:16 +0200 Subject: [PATCH 153/436] ui: change set speed, speed limit and current speed to Inter (#24932) * ui: change set speed/speed limit to Inter * switch current speed to Inter --- selfdrive/ui/qt/onroad.cc | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 98b70c6b6ebdfa..27594162369d05 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -259,10 +259,10 @@ void NvgWindow::drawHud(QPainter &p) { } else { p.setPen(QColor(0xa6, 0xa6, 0xa6, 0xff)); } - configFont(p, "Open Sans", 40, "SemiBold"); + configFont(p, "Inter", 40, "SemiBold"); QRect max_rect = getTextRect(p, Qt::AlignCenter, "MAX"); max_rect.moveCenter({set_speed_rect.center().x(), 0}); - max_rect.moveTop(set_speed_rect.top() + 23); + max_rect.moveTop(set_speed_rect.top() + 27); p.drawText(max_rect, Qt::AlignCenter, "MAX"); // Draw set speed @@ -279,10 +279,10 @@ void NvgWindow::drawHud(QPainter &p) { } else { p.setPen(QColor(0x72, 0x72, 0x72, 0xff)); } - configFont(p, "Open Sans", 90, "Bold"); + configFont(p, "Inter", 90, "Bold"); QRect speed_rect = getTextRect(p, Qt::AlignCenter, setSpeedStr); speed_rect.moveCenter({set_speed_rect.center().x(), 0}); - speed_rect.moveTop(set_speed_rect.top() + 67); + speed_rect.moveTop(set_speed_rect.top() + 77); p.drawText(speed_rect, Qt::AlignCenter, setSpeedStr); @@ -306,23 +306,23 @@ void NvgWindow::drawHud(QPainter &p) { p.drawRoundedRect(sign_rect, 16, 16); // "SPEED" - configFont(p, "Open Sans", 28, "SemiBold"); + configFont(p, "Inter", 28, "SemiBold"); QRect text_speed_rect = getTextRect(p, Qt::AlignCenter, "SPEED"); text_speed_rect.moveCenter({sign_rect.center().x(), 0}); - text_speed_rect.moveTop(sign_rect_outer.top() + 20); + text_speed_rect.moveTop(sign_rect_outer.top() + 22); p.drawText(text_speed_rect, Qt::AlignCenter, "SPEED"); // "LIMIT" QRect text_limit_rect = getTextRect(p, Qt::AlignCenter, "LIMIT"); text_limit_rect.moveCenter({sign_rect.center().x(), 0}); - text_limit_rect.moveTop(sign_rect_outer.top() + 48); + text_limit_rect.moveTop(sign_rect_outer.top() + 51); p.drawText(text_limit_rect, Qt::AlignCenter, "LIMIT"); // Speed limit value - configFont(p, "Open Sans", 70, "Bold"); + configFont(p, "Inter", 70, "Bold"); QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); speed_limit_rect.moveCenter({sign_rect.center().x(), 0}); - speed_limit_rect.moveTop(sign_rect.top() + 70); + speed_limit_rect.moveTop(sign_rect_outer.top() + 85); p.drawText(speed_limit_rect, Qt::AlignCenter, speedLimitStr); } @@ -343,10 +343,8 @@ void NvgWindow::drawHud(QPainter &p) { p.drawEllipse(center, inner_radius_2, inner_radius_2); // Speed limit value - if (speedLimit < 1 ) center -= {0, 2}; // Make sure dash is centered if no speed limit available - - int font_size = (speedLimitStr.size() >= 3) ? 62 : 70; - configFont(p, "Open Sans", font_size, "Bold"); + int font_size = (speedLimitStr.size() >= 3) ? 60 : 70; + configFont(p, "Inter", font_size, "Bold"); QRect speed_limit_rect = getTextRect(p, Qt::AlignCenter, speedLimitStr); speed_limit_rect.moveCenter(center); p.setPen(blackColor()); @@ -354,9 +352,9 @@ void NvgWindow::drawHud(QPainter &p) { } // current speed - configFont(p, "Open Sans", 176, "Bold"); + configFont(p, "Inter", 176, "Bold"); drawText(p, rect().center().x(), 210, speedStr); - configFont(p, "Open Sans", 66, "Regular"); + configFont(p, "Inter", 66, "Regular"); drawText(p, rect().center().x(), 290, speedUnit, 200); // engage-ability icon From 1908b89e29a968a3f1b8dd8cdd8213570e02c186 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 22 Jun 2022 20:41:24 +0200 Subject: [PATCH 154/436] remove Open Sans fonts from assets (#24946) --- selfdrive/assets/fonts/opensans_bold.ttf | Bin 224452 -> 0 bytes selfdrive/assets/fonts/opensans_regular.ttf | Bin 217276 -> 0 bytes selfdrive/assets/fonts/opensans_semibold.ttf | Bin 221164 -> 0 bytes selfdrive/ui/qt/window.cc | 3 --- 4 files changed, 3 deletions(-) delete mode 100644 selfdrive/assets/fonts/opensans_bold.ttf delete mode 100644 selfdrive/assets/fonts/opensans_regular.ttf delete mode 100644 selfdrive/assets/fonts/opensans_semibold.ttf diff --git a/selfdrive/assets/fonts/opensans_bold.ttf b/selfdrive/assets/fonts/opensans_bold.ttf deleted file mode 100644 index 7b529456032abf9132194ddaac62d2d163247c38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224452 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%0AFPX_$oWVSJ?r+$`0^Vc7U(41ALVo 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 NJ^r4-t1mN*{|BJyceVfk diff --git a/selfdrive/assets/fonts/opensans_regular.ttf b/selfdrive/assets/fonts/opensans_regular.ttf deleted file mode 100644 index 2e31d02424ed50b9e05c19b5d82500699a6edbb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217276 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%0DHhV?N1s&&lC=`j?-^cJA^!2jed0`4 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 }c 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

dg4umb* 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 diff --git a/selfdrive/assets/fonts/opensans_semibold.ttf b/selfdrive/assets/fonts/opensans_semibold.ttf deleted file mode 100644 index 99db86aa0282e425450c86508d42da77a9f4102b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221164 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

YE=h9c6s`zZi@B7|3|JKJY7qo+jyw@ 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 Date: Wed, 22 Jun 2022 13:51:37 -0700 Subject: [PATCH 155/436] DM: track RHD predictions (#24947) * driverview * auto choose * useless * remove * modeld not use toggle * remove from params * Revert "remove from params" This reverts commit a08df0b4921e03deac24f4da2c0f1e9e9255a717. * Revert "modeld not use toggle" This reverts commit 2730bf8f57c8b057db2e4a76541e92880506cedd. * Revert "remove" This reverts commit 21f7cfaaee5452e53ee719762078cb153b3cc766. * Revert "driverview" This reverts commit 222d129711e6aa34c0468294b94f60ebbd1bb126. * semi revert --- selfdrive/modeld/models/dmonitoring.cc | 1 - selfdrive/modeld/models/dmonitoring.h | 1 - selfdrive/monitoring/driver_monitor.py | 21 +++++++++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/selfdrive/modeld/models/dmonitoring.cc b/selfdrive/modeld/models/dmonitoring.cc index 8c7e14edb2dc56..e7e6d466128987 100644 --- a/selfdrive/modeld/models/dmonitoring.cc +++ b/selfdrive/modeld/models/dmonitoring.cc @@ -20,7 +20,6 @@ static inline T *get_buffer(std::vector &buf, const size_t size) { } void dmonitoring_init(DMonitoringModelState* s) { - s->is_rhd = Params().getBool("IsRHD"); #ifdef USE_ONNX_MODEL s->m = new ONNXModel("models/dmonitoring_model.onnx", &s->output[0], OUTPUT_SIZE, USE_DSP_RUNTIME, false, true); diff --git a/selfdrive/modeld/models/dmonitoring.h b/selfdrive/modeld/models/dmonitoring.h index 874722cd93fb88..ae2bf053946861 100644 --- a/selfdrive/modeld/models/dmonitoring.h +++ b/selfdrive/modeld/models/dmonitoring.h @@ -38,7 +38,6 @@ typedef struct DMonitoringModelResult { typedef struct DMonitoringModelState { RunModel *m; - bool is_rhd; float output[OUTPUT_SIZE]; std::vector net_input_buf; float calib[CALIB_LEN]; diff --git a/selfdrive/monitoring/driver_monitor.py b/selfdrive/monitoring/driver_monitor.py index f2d0524422d123..48d4303aa54894 100644 --- a/selfdrive/monitoring/driver_monitor.py +++ b/selfdrive/monitoring/driver_monitor.py @@ -120,7 +120,7 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.settings = settings # init driver status - # self.wheelpos_learner = RunningStatFilter() + self.wheelpos_learner = RunningStatFilter() self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) self.pose_calibrated = False self.blink = DriverBlink() @@ -137,7 +137,8 @@ def __init__(self, rhd=False, settings=DRIVER_MONITOR_SETTINGS()): self.distracted_types = [] self.driver_distracted = False self.driver_distraction_filter = FirstOrderFilter(0., self.settings._DISTRACTED_FILTER_TS, self.settings._DT_DMON) - self.wheel_on_right = rhd + self.wheel_on_right = False + self.rhd_toggled = rhd self.face_detected = False self.terminal_alert_cnt = 0 self.terminal_time = 0 @@ -225,14 +226,14 @@ def set_policy(self, model_data, car_speed): self.settings._POSE_YAW_THRESHOLD_STRICT]) / self.settings._POSE_YAW_THRESHOLD def update_states(self, driver_state, cal_rpy, car_speed, op_engaged): - # rhd_pred = driver_state.wheelOnRightProb - # if car_speed > 0.01: - # self.wheelpos_learner.push_and_update(rhd_pred) - # if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT: - # self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD - # else: - # self.wheel_on_right = rhd_pred > self.settings._WHEELPOS_THRESHOLD - driver_data = driver_state.rightDriverData if self.wheel_on_right else driver_state.leftDriverData + rhd_pred = driver_state.wheelOnRightProb + if car_speed > 0.01: + self.wheelpos_learner.push_and_update(rhd_pred) + if self.wheelpos_learner.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT: + self.wheel_on_right = self.wheelpos_learner.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD + else: + self.wheel_on_right = rhd_pred > self.settings._WHEELPOS_THRESHOLD + driver_data = driver_state.rightDriverData if self.rhd_toggled else driver_state.leftDriverData if not all(len(x) > 0 for x in (driver_data.faceOrientation, driver_data.facePosition, driver_data.faceOrientationStd, driver_data.facePositionStd, driver_data.readyProb, driver_data.notReadyProb)): From fb949779ae151d9597cd70b5435f87edbffb1f53 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 22 Jun 2022 15:13:09 -0700 Subject: [PATCH 156/436] Chrysler: fill cruiseState.available (#24907) * Update some signals to unified names and definitions Co-authored-by: Jonathan * steering looks good * Fix cp signals * Do steering signal changes separately * bump opendbc to master * fix fix * check available is true if enabled is true * fix * already added * bump opendbc, better cruise status names * bump opendbc * bump opendbc to master * bump panda Co-authored-by: Jonathan --- opendbc | 2 +- panda | 2 +- selfdrive/car/chrysler/carstate.py | 31 +++++++++++++++--------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/opendbc b/opendbc index 5e2a82026842a7..e7cd3ebc893047 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 5e2a82026842a7082e5e81e5823dab6b6616dbf4 +Subproject commit e7cd3ebc893047bf6eb947c60d8e3196b506e8d3 diff --git a/panda b/panda index e1b2f1253cb7f0..4bc85ad40ad032 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit e1b2f1253cb7f05f39d4afa21500565bb8b955d2 +Subproject commit 4bc85ad40ad032672008eb75567892ba45e0b932 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index e3ee4753cc3e3d..f71a3002632679 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -18,10 +18,10 @@ def update(self, cp, cp_cam): self.frame = int(cp.vl["EPS_STATUS"]["COUNTER"]) - ret.doorOpen = any([cp.vl["DOORS"]["DOOR_OPEN_FL"], - cp.vl["DOORS"]["DOOR_OPEN_FR"], - cp.vl["DOORS"]["DOOR_OPEN_RL"], - cp.vl["DOORS"]["DOOR_OPEN_RR"]]) + ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"], + cp.vl["BCM_1"]["DOOR_OPEN_FR"], + cp.vl["BCM_1"]["DOOR_OPEN_RL"], + cp.vl["BCM_1"]["DOOR_OPEN_RR"]]) ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_UNLATCHED"] == 1 # brake pedal @@ -51,12 +51,12 @@ def update(self, cp, cp_cam): ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) - ret.cruiseState.enabled = cp.vl["ACC_2"]["ACC_STATUS_2"] == 7 # ACC is green. - ret.cruiseState.available = ret.cruiseState.enabled # FIXME: for now same as enabled + ret.cruiseState.available = cp.vl["DAS_3"]["ACC_AVAILABLE"] == 1 # ACC is white + ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_ACTIVE"] == 1 # ACC is green ret.cruiseState.speed = cp.vl["DASHBOARD"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too ret.cruiseState.nonAdaptive = cp.vl["DASHBOARD"]["CRUISE_STATE"] in (1, 2) - ret.accFaulted = cp.vl["ACC_2"]["ACC_FAULTED"] != 0 + ret.accFaulted = cp.vl["DAS_3"]["ACC_FAULTED"] != 0 ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] @@ -82,10 +82,10 @@ def get_can_parser(CP): signals = [ # sig_name, sig_address ("PRNDL", "GEAR"), - ("DOOR_OPEN_FL", "DOORS"), - ("DOOR_OPEN_FR", "DOORS"), - ("DOOR_OPEN_RL", "DOORS"), - ("DOOR_OPEN_RR", "DOORS"), + ("DOOR_OPEN_FL", "BCM_1"), + ("DOOR_OPEN_FR", "BCM_1"), + ("DOOR_OPEN_RL", "BCM_1"), + ("DOOR_OPEN_RR", "BCM_1"), ("Brake_Pedal_State", "ESP_1"), ("Accelerator_Position", "ECM_5"), ("SPEED_LEFT", "SPEED_1"), @@ -97,8 +97,9 @@ def get_can_parser(CP): ("STEER_ANGLE", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), - ("ACC_STATUS_2", "ACC_2"), - ("ACC_FAULTED", "ACC_2"), + ("ACC_AVAILABLE", "DAS_3"), + ("ACC_ACTIVE", "DAS_3"), + ("ACC_FAULTED", "DAS_3"), ("HIGH_BEAM_FLASH", "STEERING_LEVERS"), ("ACC_SPEED_CONFIG_KPH", "DASHBOARD"), ("CRUISE_STATE", "DASHBOARD"), @@ -118,14 +119,14 @@ def get_can_parser(CP): ("SPEED_1", 100), ("WHEEL_SPEEDS", 50), ("STEERING", 100), - ("ACC_2", 50), + ("DAS_3", 50), ("GEAR", 50), ("ECM_5", 50), ("WHEEL_BUTTONS", 50), ("DASHBOARD", 15), ("STEERING_LEVERS", 10), ("SEATBELT_STATUS", 2), - ("DOORS", 1), + ("BCM_1", 1), ("TRACTION_BUTTON", 1), ] From 569a39ff768ff1704bbf478719253b365070c98f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 22 Jun 2022 15:38:38 -0700 Subject: [PATCH 157/436] CANParser: invalid until valid (#24945) * CANParser: invalid until valid * bump opendbc * bump opendbc * fix counter in sim --- opendbc | 2 +- tools/sim/lib/can.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc b/opendbc index e7cd3ebc893047..82be71072c52fc 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit e7cd3ebc893047bf6eb947c60d8e3196b506e8d3 +Subproject commit 82be71072c52fc78cf0e1eabc396af26c18ddc11 diff --git a/tools/sim/lib/can.py b/tools/sim/lib/can.py index 2b1048fef297e0..939d081a809183 100755 --- a/tools/sim/lib/can.py +++ b/tools/sim/lib/can.py @@ -63,7 +63,7 @@ def can_function(pm, speed, angle, idx, cruise_button, is_engaged): msg.append(packer.make_can_msg("SCM_FEEDBACK", 0, {"MAIN_ON": 1}, idx)) msg.append(packer.make_can_msg("POWERTRAIN_DATA", 0, {"ACC_STATUS": int(is_engaged)}, idx)) msg.append(packer.make_can_msg("HUD_SETTING", 0, {})) - msg.append(packer.make_can_msg("CAR_SPEED", 0, {})) + msg.append(packer.make_can_msg("CAR_SPEED", 0, {}, idx)) # *** cam bus *** msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {}, idx)) From 2deaf69789fee9a048256a832ffddbda95242815 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Wed, 22 Jun 2022 15:58:06 -0700 Subject: [PATCH 158/436] Refactor torque stuff (#24921) * Refactor torque stuff * Add to release * Add substitute and override * Maxlataccel is required * Add to asserts * add ideal car * Need china too * yamls already linted * Fixed some bugs * Fixup * Unreliable data * Add cehck * Better comment * ref commit update --- .pre-commit-config.yaml | 2 +- docs/CARS.md | 70 ++++++++-------- release/files_common | 4 +- selfdrive/car/interfaces.py | 26 +++++- selfdrive/car/tests/test_car_interfaces.py | 1 + selfdrive/car/torque_data.json | 1 - selfdrive/car/torque_data/override.yaml | 29 +++++++ selfdrive/car/torque_data/params.yaml | 96 ++++++++++++++++++++++ selfdrive/car/torque_data/substitute.yaml | 75 +++++++++++++++++ selfdrive/test/process_replay/ref_commit | 2 +- 10 files changed, 263 insertions(+), 43 deletions(-) delete mode 100644 selfdrive/car/torque_data.json create mode 100644 selfdrive/car/torque_data/override.yaml create mode 100644 selfdrive/car/torque_data/params.yaml create mode 100644 selfdrive/car/torque_data/substitute.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81ce927f655b89..b901e07721ae2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: hooks: - id: mypy exclude: '^(pyextra/)|(cereal/)|(rednose/)|(panda/)|(laika/)|(opendbc/)|(laika_repo/)|(rednose_repo/)/' - additional_dependencies: ['lxml', 'numpy', 'types-atomicwrites', 'types-pycurl', 'types-requests', 'types-certifi'] + additional_dependencies: ['types-PyYAML', 'lxml', 'numpy', 'types-atomicwrites', 'types-pycurl', 'types-requests', 'types-certifi'] args: - --warn-redundant-casts - --warn-return-any diff --git a/docs/CARS.md b/docs/CARS.md index de7dbacf281585..fb163b44e0f79a 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -35,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -# Gold - 33 cars +# Gold - 31 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -55,8 +55,6 @@ How We Rate The Cars |Lexus|NX Hybrid 2020|All|||||| |Lexus|RX 2020-22|All|||||| |Lexus|UX Hybrid 2019-21|All|||||| -|Toyota|Alphard 2019-20|All|||||| -|Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Avalon 2022|All|||||| |Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| @@ -73,13 +71,12 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 76 cars +# Silver - 67 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| |Audi|A3 2014-19|ACC + Lane Assist|||||| |Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|||||| -|Audi|Q2 2018|ACC + Lane Assist|||||| |Audi|RS3 2018|ACC + Lane Assist|||||| |Audi|S3 2015-17|ACC + Lane Assist|||||| |Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| @@ -113,22 +110,18 @@ How We Rate The Cars |Lexus|NX 2018-19|All|[3](#footnotes)||||| |Lexus|NX Hybrid 2018-19|All|[3](#footnotes)||||| |Lexus|RX Hybrid 2020-21|All|||||| -|Mazda|CX-5 2022|All|||||| +|Nissan|Altima 2019-20|ProPILOT|||||| +|Nissan|Leaf 2018-22|ProPILOT|||||| +|Nissan|Rogue 2018-20|ProPILOT|||||| +|Nissan|X-Trail 2017|ProPILOT|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| |Subaru|Ascent 2019-20|All|||||| |Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Forester 2019-21|All|||||| |Subaru|Impreza 2020-21|EyeSight|||||| -|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| -|Škoda|Karoq 2019|Driver Assistance|||||| -|Škoda|Kodiaq 2018-19|Driver Assistance|||||| -|Škoda|Octavia 2015, 2018-19|Driver Assistance|||||| -|Škoda|Octavia RS 2016|Driver Assistance|||||| -|Škoda|Scala 2020|Driver Assistance|||||| -|Škoda|Superb 2015-18|Driver Assistance|||||| -|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| +|Toyota|Alphard 2019-20|All|||||| +|Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| |Toyota|Highlander 2017-19|All|[3](#footnotes)||||| @@ -138,7 +131,6 @@ How We Rate The Cars |Toyota|RAV4 2022|All|||||| |Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 Hybrid 2022|All|||||| -|Toyota|Sienna 2018-20|All|[3](#footnotes)||||| |Volkswagen|Atlas 2018-19, 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|e-Golf 2014, 2018-20|Driver Assistance|||||| |Volkswagen|Golf 2015-20|Driver Assistance|||||| @@ -148,23 +140,21 @@ How We Rate The Cars |Volkswagen|Golf R 2016-19|Driver Assistance|||||| |Volkswagen|Golf SportsVan 2016|Driver Assistance|||||| |Volkswagen|Golf SportWagen 2015|Driver Assistance|||||| +|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Polo 2020|Driver Assistance|||||| -|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Touran 2017|Driver Assistance|||||| -# Bronze - 67 cars +# Bronze - 78 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-19|AcuraWatch Plus|||||| |Acura|RDX 2016-18|AcuraWatch Plus|||||| |Acura|RDX 2019-21|All|||||| +|Audi|Q2 2018|ACC + Lane Assist|||||| |Audi|Q3 2020-21|ACC + Lane Assist|||||| -|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| +|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| |Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica 2020|Adaptive Cruise|||||| +|Chrysler|Pacifica 2020|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| |Genesis|G90 2018|All|||||| @@ -189,14 +179,14 @@ How We Rate The Cars |Honda|Passport 2019-21|All|||||| |Honda|Pilot 2016-21|Honda Sensing|||||| |Honda|Ridgeline 2017-22|Honda Sensing|||||| -|Hyundai|Elantra 2017-19|SCC + LKAS|||||| +|Hyundai|Elantra 2017-19|SCC + LKAS|||||| |Hyundai|Genesis 2015-16|SCC + LKAS|||||| |Hyundai|Ioniq Electric 2019|SCC + LKAS|||||| |Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|||||| |Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|||||| |Hyundai|Sonata 2018-19|SCC + LKAS|||||| |Hyundai|Tucson 2021|SCC + LKAS|||||| -|Hyundai|Veloster 2019-20|SCC + LKAS|||||| +|Hyundai|Veloster 2019-20|SCC + LKAS|||||| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| |Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| |Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| @@ -205,26 +195,36 @@ How We Rate The Cars |Lexus|RC 2020|All|||||| |Lexus|RX 2016-18|All|[3](#footnotes)||||| |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| -|Mazda|CX-9 2021|All|||||| -|Nissan|Altima 2019-20|ProPILOT|||||| -|Nissan|Leaf 2018-22|ProPILOT|||||| -|Nissan|Rogue 2018-20|ProPILOT|||||| -|Nissan|X-Trail 2017|ProPILOT|||||| -|Subaru|Crosstrek 2018-19|EyeSight|||||| -|Subaru|Impreza 2017-19|EyeSight|||||| +|Mazda|CX-5 2022|All|||||| +|Mazda|CX-9 2021|All|||||| +|Subaru|Crosstrek 2018-19|EyeSight|||||| +|Subaru|Impreza 2017-19|EyeSight|||||| +|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| +|Škoda|Karoq 2019|Driver Assistance|||||| +|Škoda|Kodiaq 2018-19|Driver Assistance|||||| +|Škoda|Octavia 2015, 2018-19|Driver Assistance|||||| +|Škoda|Octavia RS 2016|Driver Assistance|||||| +|Škoda|Scala 2020|Driver Assistance|||||| +|Škoda|Superb 2015-18|Driver Assistance|||||| |Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| +|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| |Toyota|C-HR 2017-21|All|||||| |Toyota|C-HR Hybrid 2017-19|All|||||| |Toyota|Corolla 2017-19|All|[3](#footnotes)||||| |Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| +|Toyota|Sienna 2018-20|All|[3](#footnotes)||||| |Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Jetta 2018-21|Driver Assistance|||||| |Volkswagen|Jetta GLI 2021|Driver Assistance|||||| -|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| +|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Touran 2017|Driver Assistance|||||| diff --git a/release/files_common b/release/files_common index 96649b9f161721..0ace73623f2ecd 100644 --- a/release/files_common +++ b/release/files_common @@ -105,7 +105,9 @@ selfdrive/car/ecu_addrs.py selfdrive/car/isotp_parallel_query.py selfdrive/car/tests/__init__.py selfdrive/car/tests/test_car_interfaces.py -selfdrive/car/torque_data.json +selfdrive/car/torque_data/params.yaml +selfdrive/car/torque_data/substitute.yaml +selfdrive/car/torque_data/override.yaml selfdrive/car/body/*.py selfdrive/car/chrysler/*.py diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 9220aee522f7e2..c6a64fe919efe2 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -1,4 +1,4 @@ -import json +import yaml import os import time from abc import abstractmethod, ABC @@ -20,7 +20,9 @@ MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS ACCEL_MAX = 2.0 ACCEL_MIN = -3.5 -TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data.json') +TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/params.yaml') +TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/override.yaml') +TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/substitute.yaml') # generic car and radar interfaces @@ -109,9 +111,25 @@ def get_std_params(candidate, fingerprint): @staticmethod def get_torque_params(candidate, default=float('NaN')): + with open(TORQUE_SUBSTITUTE_PATH) as f: + sub = yaml.load(f, Loader=yaml.FullLoader) + if candidate in sub: + candidate = sub[candidate] + with open(TORQUE_PARAMS_PATH) as f: - data = json.load(f) - return {key: data[key].get(candidate, default) for key in data} + params = yaml.load(f, Loader=yaml.FullLoader) + with open(TORQUE_OVERRIDE_PATH) as f: + params_override = yaml.load(f, Loader=yaml.FullLoader) + + assert len(set(sub.keys()) & set(params.keys()) & set(params_override.keys())) == 0 + + if candidate in params_override: + out = params_override[candidate] + elif candidate in params: + out = params[candidate] + else: + raise NotImplementedError(f"Did not find torque params for {candidate}") + return {key:out[i] for i, key in enumerate(params['legend'])} @abstractmethod def _update(self, c: car.CarControl) -> car.CarState: diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 15df1aafef918c..412874c813e661 100755 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -33,6 +33,7 @@ def test_car_interfaces(self, car_name): assert car_interface self.assertGreater(car_params.mass, 1) + self.assertGreater(car_params.maxLateralAccel, 0) if car_params.steerControlType != car.CarParams.SteerControlType.angle: tuning = car_params.lateralTuning.which() diff --git a/selfdrive/car/torque_data.json b/selfdrive/car/torque_data.json deleted file mode 100644 index 4917cba9bc38ca..00000000000000 --- a/selfdrive/car/torque_data.json +++ /dev/null @@ -1 +0,0 @@ -{"LAT_ACCEL_FACTOR": {"HONDA PILOT 2017": 1.682289482065265, "HONDA CIVIC 2016": 1.5248128495527884, "TOYOTA CAMRY 2018": 2.1115709806216447, "TOYOTA COROLLA HYBRID TSS2 2019": 2.3250600977240077, "TOYOTA RAV4 2019": 2.625504029066767, "HYUNDAI PALISADE 2020": 2.5250855675875634, "TOYOTA SIENNA 2018": 1.8254254785341577, "ACURA RDX 2020": 1.3998101622214894, "TOYOTA RAV4 2017": 1.948190869577896, "HONDA RIDGELINE 2017": 1.4158181862793415, "TOYOTA PRIUS 2017": 1.9142926195557595, "TOYOTA HIGHLANDER HYBRID 2020": 2.1097056247344392, "HYUNDAI SONATA 2020": 3.2488989629905944, "KIA STINGER GT2 2018": 2.7592622336517834, "TOYOTA HIGHLANDER 2020": 2.0408544157877055, "HONDA ACCORD 2018": 1.6374118241564064, "TOYOTA PRIUS TSS2 2021": 2.3207270770298365, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.46050785084946, "LEXUS NX 2020": 2.29533657249232, "TOYOTA RAV4 HYBRID 2019": 2.4003012079562085, "HONDA CIVIC (BOSCH) 2019": 1.6523031416671652, "KIA NIRO HYBRID 2021": 2.743464625803003, "HONDA ACCORD HYBRID 2018": 1.5904016830979033, "LEXUS NX HYBRID 2018": 2.398678119681945, "TOYOTA COROLLA TSS2 2019": 2.3859244449846466, "VOLKSWAGEN ARTEON 1ST GEN": 1.4249208219414902, "TOYOTA CAMRY HYBRID 2021": 2.5434553806317055, "VOLKSWAGEN JETTA 7TH GEN": 1.2228130240634283, "HONDA INSIGHT 2019": 1.468352089969897, "SUBARU FORESTER 2019": 3.6185035528523546, "HYUNDAI ELANTRA 2021": 3.5294999663335185, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.2179616966432905, "HYUNDAI KONA HYBRID 2020": 4.493208192966529, "HONDA ODYSSEY 2018": 1.8838175399087222, "LEXUS RX 2016": 1.3912132245094184, "TOYOTA COROLLA 2017": 3.0143547548384735, "LEXUS ES 2019": 2.012201253045193, "HYUNDAI SANTA FE 2019": 3.039728566484244, "TOYOTA AVALON 2022": 2.4619858654670885, "JEEP GRAND CHEROKEE V6 2018": 1.8411674990629987, "CHEVROLET VOLT PREMIER 2017": 1.5943438675127841, "TOYOTA RAV4 HYBRID 2017": 1.9803053616868995, "LEXUS RX 2020": 1.664616846377383, "TOYOTA HIGHLANDER HYBRID 2018": 1.8866764457400844, "TOYOTA CAMRY HYBRID 2018": 2.014213351947917, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.4428896585442685, "TOYOTA MIRAI 2021": 2.7217623852898853, "LEXUS IS 2018": 3.5624668608596837, "TOYOTA HIGHLANDER 2017": 1.9199133105823853, "HYUNDAI SONATA HYBRID 2021": 2.7313907441569554, "VOLKSWAGEN ATLAS 1ST GEN": 1.4483948408160645, "LEXUS ES HYBRID 2019": 2.4138026617523547, "HYUNDAI GENESIS 2015-2016": 1.7636839808044658, "JEEP GRAND CHEROKEE 2019": 1.787264083939164, "SUBARU ASCENT LIMITED 2019": 3.0494069339774565, "HONDA CR-V 2017": 1.9828470679233807, "HONDA FIT 2018": 1.594940026552055, "TOYOTA CAMRY 2021": 2.5057990840460342, "AUDI Q3 2ND GEN": 1.4558300885316715, "AUDI A3 3RD GEN": 1.5304173783542625, "LEXUS RX HYBRID 2017": 1.577216425446677, "HONDA CIVIC 2022": 2.69252285552613, "GENESIS G70 2018": 3.866842361627636, "CHRYSLER PACIFICA HYBRID 2018": 1.5771851419640903, "VOLKSWAGEN PASSAT 8TH GEN": 1.2985597059739313, "HONDA CR-V 2016": 0.7745984062630755, "HYUNDAI IONIQ PHEV 2020": 2.5696218908589383, "GMC ACADIA DENALI 2018": 1.3310088601868082, "HYUNDAI SONATA 2019": 1.9736552675022665, "TOYOTA AVALON 2019": 1.7245149905226294, "TOYOTA C-HR 2018": 1.5895016960662856, "HONDA CR-V HYBRID 2019": 2.0687746810729193, "CHRYSLER PACIFICA 2020": 1.40536880000744, "HYUNDAI IONIQ ELECTRIC 2020": 3.3220838625838667, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 1.7753192756242595, "KIA OPTIMA SX 2019 & 2016": 3.12625562280304, "TOYOTA AVALON HYBRID 2019": 1.7681286449373381, "TOYOTA RAV4 HYBRID 2022": 2.5518187542231816, "HONDA PASSPORT 2021": 1.5174924139130355, "KIA K5 2021": 2.482916204106975, "ACURA ILX 2016": 1.5237423964720282, "HYUNDAI IONIQ HYBRID 2017-2019": 2.3723887901632645, "KIA NIRO EV 2020": 2.924651969180446, "SUBARU IMPREZA SPORT 2020": 2.5317689549587694, "CHRYSLER PACIFICA HYBRID 2017": 1.167126725149114, "HYUNDAI KONA ELECTRIC 2019": 4.201092987427836, "HYUNDAI ELANTRA HYBRID 2021": 3.7153193626001926, "HYUNDAI SANTA FE HYBRID 2022": 3.3049230586030545, "CHRYSLER PACIFICA 2018": 1.524867383058782, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 2.5970979517766213, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.5460982690267173, "MAZDA CX-9 2021": 1.9514800984278198, "HYUNDAI SANTA FE 2022": 3.5354982200434524, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.8902492836532216, "HONDA HRV 2019": 2.1262957371020352, "TOYOTA AVALON HYBRID 2022": 2.4142150048378683, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": 2.4086794443413906, "VOLKSWAGEN TAOS 1ST GEN": 2.0031666974545947, "KIA FORTE E 2018 & GT 2021": 2.022553820222557, "CADILLAC ESCALADE ESV 2016": 1.5522339636408988, "TOYOTA C-HR 2021": 1.6519334844316687, "TOYOTA C-HR HYBRID 2018": 1.3193315010905482}, "MAX_LAT_ACCEL_MEASURED": {"HONDA PILOT 2017": 0.9069354290994807, "HONDA CIVIC 2016": 0.4030275472529351, "TOYOTA CAMRY 2018": 1.686123168195758, "TOYOTA COROLLA HYBRID TSS2 2019": 1.9139332621491167, "TOYOTA RAV4 2019": 2.234047196286479, "HYUNDAI PALISADE 2020": 1.8303582523301922, "TOYOTA SIENNA 2018": 1.4752503435300715, "ACURA RDX 2020": 0.40911581320000334, "TOYOTA RAV4 2017": 1.6622227720995595, "HONDA RIDGELINE 2017": 0.8224685813281227, "TOYOTA PRIUS 2017": 1.4548827870876067, "TOYOTA HIGHLANDER HYBRID 2020": 2.0649784271823037, "HYUNDAI SONATA 2020": 2.243989856570093, "KIA STINGER GT2 2018": 1.9531287107084392, "TOYOTA HIGHLANDER 2020": 1.659381392090836, "HONDA ACCORD 2018": 0.40486739531686267, "TOYOTA PRIUS TSS2 2021": 1.861541601048098, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 1.1930739374812243, "LEXUS NX 2020": 1.565268724838564, "TOYOTA RAV4 HYBRID 2019": 2.0915384047218426, "HONDA CIVIC (BOSCH) 2019": 0.4062886118517984, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 0.35128914564548286, "LEXUS NX HYBRID 2018": 1.81821359787186, "TOYOTA COROLLA TSS2 2019": 1.911280958056631, "VOLKSWAGEN ARTEON 1ST GEN": 1.2587939472578302, "TOYOTA CAMRY HYBRID 2021": 2.312510643730013, "VOLKSWAGEN JETTA 7TH GEN": 1.232161945396623, "HONDA INSIGHT 2019": 0.5174836462945298, "SUBARU FORESTER 2019": 2.29255993930968, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 2.133978602602408, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 0.8254773781363679, "LEXUS RX 2016": 1.0954776820595344, "TOYOTA COROLLA 2017": 2.2012870528168964, "LEXUS ES 2019": 2.069508805495439, "HYUNDAI SANTA FE 2019": 2.3763720477660253, "TOYOTA AVALON 2022": 2.531962323786023, "JEEP GRAND CHEROKEE V6 2018": 1.4193323242487865, "CHEVROLET VOLT PREMIER 2017": 1.8576430337666092, "TOYOTA RAV4 HYBRID 2017": 1.7425797219020926, "LEXUS RX 2020": 1.5118835180227874, "TOYOTA HIGHLANDER HYBRID 2018": 1.6872527654528833, "TOYOTA CAMRY HYBRID 2018": 1.6793468378089895, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 1.5614447712441282, "TOYOTA MIRAI 2021": 2.271146483563897, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 1.6573774863189379, "HYUNDAI SONATA HYBRID 2021": 1.9464120717803253, "VOLKSWAGEN ATLAS 1ST GEN": 1.6867005451451638, "LEXUS ES HYBRID 2019": 1.956450687999482, "HYUNDAI GENESIS 2015-2016": 1.5359761378898085, "JEEP GRAND CHEROKEE 2019": 1.2418961305308847, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 0.2642062271814174, "HONDA FIT 2018": 0.5896345937094754, "TOYOTA CAMRY 2021": 2.1783533980215166, "AUDI Q3 2ND GEN": 1.1582239457022647, "AUDI A3 3RD GEN": 1.598699116126939, "LEXUS RX HYBRID 2017": 1.319771127672888, "HONDA CIVIC 2022": 1.1806949852580793, "GENESIS G70 2018": 2.2496820850331134, "CHRYSLER PACIFICA HYBRID 2018": 1.294798200968084, "VOLKSWAGEN PASSAT 8TH GEN": 1.247540921731637, "HONDA CR-V 2016": 0.6991119250342539, "HYUNDAI IONIQ PHEV 2020": 1.9062392690595655, "GMC ACADIA DENALI 2018": 1.2986994230652662, "HYUNDAI SONATA 2019": 1.257445187146704, "TOYOTA AVALON 2019": 1.664577368475227, "TOYOTA C-HR 2018": 1.308490445144888, "HONDA CR-V HYBRID 2019": 0.4693072746041504, "CHRYSLER PACIFICA 2020": 1.1712413003138664, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": 1.1573057001955744, "LEXUS NX 2018": 1.9457312007432144, "KIA OPTIMA SX 2019 & 2016": 2.0928228595938845, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 1.7647290773049569, "HONDA PASSPORT 2021": 0.8248357750132685, "KIA K5 2021": 1.4628018983720577, "ACURA ILX 2016": 0.6330753921140401, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 2.020186575503497, "SUBARU IMPREZA SPORT 2020": 2.136786720514988, "CHRYSLER PACIFICA HYBRID 2017": 1.0642918033307907, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 1.3654603720349934, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 1.255230465866663, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 3.3823387508235827, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 1.544104124172169, "HONDA HRV 2019": 0.7492792210307291, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 1.2203463907025918, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 1.6590543949912684, "KIA FORTE E 2018 & GT 2021": 2.3970573789339786, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 1.3559230155096402, "TOYOTA C-HR HYBRID 2018": 1.271271459066948}, "FRICTION": {"HONDA PILOT 2017": 0.2168217463499328, "HONDA CIVIC 2016": 0.28406761310944795, "TOYOTA CAMRY 2018": 0.1327947477896041, "TOYOTA COROLLA HYBRID TSS2 2019": 0.21792021497538405, "TOYOTA RAV4 2019": 0.12757022360707945, "HYUNDAI PALISADE 2020": 0.13391574986922777, "TOYOTA SIENNA 2018": 0.1853443239485906, "ACURA RDX 2020": 0.18058553315570297, "TOYOTA RAV4 2017": 0.14319170324556796, "HONDA RIDGELINE 2017": 0.2380553573913589, "TOYOTA PRIUS 2017": 0.2079869382946584, "TOYOTA HIGHLANDER HYBRID 2020": 0.14038812589302646, "HYUNDAI SONATA 2020": 0.08266051305053168, "KIA STINGER GT2 2018": 0.11909534626930472, "TOYOTA HIGHLANDER 2020": 0.14658637853444048, "HONDA ACCORD 2018": 0.21616610462729247, "TOYOTA PRIUS TSS2 2021": 0.20613763260512002, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.16250373743651828, "LEXUS NX 2020": 0.14404022591302845, "TOYOTA RAV4 HYBRID 2019": 0.1319247989758836, "HONDA CIVIC (BOSCH) 2019": 0.2575217845562353, "KIA NIRO HYBRID 2021": 0.14468633728800306, "HONDA ACCORD HYBRID 2018": 0.21150723931119184, "LEXUS NX HYBRID 2018": 0.16117151597250162, "TOYOTA COROLLA TSS2 2019": 0.21045927995242877, "VOLKSWAGEN ARTEON 1ST GEN": 0.17828895368353925, "TOYOTA CAMRY HYBRID 2021": 0.16283734136957057, "VOLKSWAGEN JETTA 7TH GEN": 0.19508489725001105, "HONDA INSIGHT 2019": 0.25750800088299297, "SUBARU FORESTER 2019": 0.11783702069698135, "HYUNDAI ELANTRA 2021": 0.09377564130711125, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.14740189509875762, "HYUNDAI KONA HYBRID 2020": 0.0863709736632968, "HONDA ODYSSEY 2018": 0.2125595696498247, "LEXUS RX 2016": 0.21475140622981923, "TOYOTA COROLLA 2017": 0.12325064090161544, "LEXUS ES 2019": 0.12757526660498053, "HYUNDAI SANTA FE 2019": 0.12230125806479573, "TOYOTA AVALON 2022": 0.11030226705639488, "JEEP GRAND CHEROKEE V6 2018": 0.12871972792344108, "CHEVROLET VOLT PREMIER 2017": 0.16697256960295873, "TOYOTA RAV4 HYBRID 2017": 0.14074453855329072, "LEXUS RX 2020": 0.2249895411716623, "TOYOTA HIGHLANDER HYBRID 2018": 0.16692807938039034, "TOYOTA CAMRY HYBRID 2018": 0.13418904852016877, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.19324413131475543, "TOYOTA MIRAI 2021": 0.20035237756713503, "LEXUS IS 2018": 0.073103111226694, "TOYOTA HIGHLANDER 2017": 0.17502689439420385, "HYUNDAI SONATA HYBRID 2021": 0.09518615688045734, "VOLKSWAGEN ATLAS 1ST GEN": 0.12761803335799474, "LEXUS ES HYBRID 2019": 0.1682771025433274, "HYUNDAI GENESIS 2015-2016": 0.10254237048034251, "JEEP GRAND CHEROKEE 2019": 0.15702739382013717, "SUBARU ASCENT LIMITED 2019": 0.12936982863095342, "HONDA CR-V 2017": 0.22518506713451308, "HONDA FIT 2018": 0.10803295063463647, "TOYOTA CAMRY 2021": 0.15512845523424743, "AUDI Q3 2ND GEN": 0.14083949977629878, "AUDI A3 3RD GEN": 0.1611945965384188, "LEXUS RX HYBRID 2017": 0.19322020114452776, "HONDA CIVIC 2022": 0.24279247053469405, "GENESIS G70 2018": 0.06869638264150804, "CHRYSLER PACIFICA HYBRID 2018": 0.13887505891474383, "VOLKSWAGEN PASSAT 8TH GEN": 0.21714039653367842, "HONDA CR-V 2016": 0.41726236462791455, "HYUNDAI IONIQ PHEV 2020": 0.13800461817330806, "GMC ACADIA DENALI 2018": 0.3447163106452739, "HYUNDAI SONATA 2019": 0.15371520337813344, "TOYOTA AVALON 2019": 0.10392921606262978, "TOYOTA C-HR 2018": 0.2015190716953846, "HONDA CR-V HYBRID 2019": 0.19595630321202379, "CHRYSLER PACIFICA 2020": 0.14337114313208268, "HYUNDAI IONIQ ELECTRIC 2020": 0.08104502306679212, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.1471001686544422, "KIA OPTIMA SX 2019 & 2016": 0.11703652166984638, "TOYOTA AVALON HYBRID 2019": 0.10863628402866225, "TOYOTA RAV4 HYBRID 2022": 0.14334213238415072, "HONDA PASSPORT 2021": 0.19826160782809032, "KIA K5 2021": 0.1027179720106188, "ACURA ILX 2016": 0.35663988815912573, "HYUNDAI IONIQ HYBRID 2017-2019": 0.12332151728479951, "KIA NIRO EV 2020": 0.0892074288578785, "SUBARU IMPREZA SPORT 2020": 0.15841234487251604, "CHRYSLER PACIFICA HYBRID 2017": 0.1345638758810282, "HYUNDAI KONA ELECTRIC 2019": 0.08503096350356723, "HYUNDAI ELANTRA HYBRID 2021": 0.09887804390243872, "HYUNDAI SANTA FE HYBRID 2022": 0.11171499761140577, "CHRYSLER PACIFICA 2018": 0.13611561752951415, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": 0.10502695501512567, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.21818156330777305, "MAZDA CX-9 2021": 0.1793735649504697, "HYUNDAI SANTA FE 2022": 0.09184808719698756, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.14050744688135813, "HONDA HRV 2019": 0.17840321248608593, "TOYOTA AVALON HYBRID 2022": 0.16159049452515487, "SUBARU IMPREZA LIMITED 2019": 0.20322553080306893, "GENESIS G80 2017": 0.07934444681782107, "VOLKSWAGEN TAOS 1ST GEN": 0.18276122764341485, "KIA FORTE E 2018 & GT 2021": 0.11406160665068436, "CADILLAC ESCALADE ESV 2016": 0.15063766975884627, "TOYOTA C-HR 2021": 0.22798633346500694, "TOYOTA C-HR HYBRID 2018": 0.2036375866375624}, "ERROR_RATIO": {"HONDA PILOT 2017": 0.6158457247286419, "HONDA CIVIC 2016": 2.0785618623350928, "TOYOTA CAMRY 2018": 0.17356565057429169, "TOYOTA COROLLA HYBRID TSS2 2019": 0.10094741777075293, "TOYOTA RAV4 2019": 0.11812042718338775, "HYUNDAI PALISADE 2020": 0.30639442561268304, "TOYOTA SIENNA 2018": 0.1117307389748361, "ACURA RDX 2020": 1.9801454495960717, "TOYOTA RAV4 2017": 0.08589486116378196, "HONDA RIDGELINE 2017": 0.4319851914417577, "TOYOTA PRIUS 2017": 0.17281316158588575, "TOYOTA HIGHLANDER HYBRID 2020": 0.046325388721577, "HYUNDAI SONATA 2020": 0.4109860794021653, "KIA STINGER GT2 2018": 0.3517628781488943, "TOYOTA HIGHLANDER 2020": 0.14155072865224166, "HONDA ACCORD 2018": 2.510398061115294, "TOYOTA PRIUS TSS2 2021": 0.13593456264106363, "NISSAN LEAF 2018": NaN, "CHRYSLER PACIFICA HYBRID 2019": 0.08794943266738546, "LEXUS NX 2020": 0.3743942573190866, "TOYOTA RAV4 HYBRID 2019": 0.0845492503791727, "HONDA CIVIC (BOSCH) 2019": 2.4329816697390063, "KIA NIRO HYBRID 2021": NaN, "HONDA ACCORD HYBRID 2018": 2.9252406767451804, "LEXUS NX HYBRID 2018": 0.23060712246809048, "TOYOTA COROLLA TSS2 2019": 0.13822363784977285, "VOLKSWAGEN ARTEON 1ST GEN": 0.009661691674299285, "TOYOTA CAMRY HYBRID 2021": 0.029451711159377333, "VOLKSWAGEN JETTA 7TH GEN": 0.16591473170144055, "HONDA INSIGHT 2019": 1.3398692842898896, "SUBARU FORESTER 2019": 0.5269683780697442, "HYUNDAI ELANTRA 2021": NaN, "HYUNDAI IONIQ ELECTRIC LIMITED 2019": 0.02971857401969039, "HYUNDAI KONA HYBRID 2020": NaN, "HONDA ODYSSEY 2018": 1.0245957242729038, "LEXUS RX 2016": 0.07392586589971588, "TOYOTA COROLLA 2017": 0.31336988069649124, "LEXUS ES 2019": 0.08933657038050916, "HYUNDAI SANTA FE 2019": 0.2276812089092099, "TOYOTA AVALON 2022": 0.07120118798045925, "JEEP GRAND CHEROKEE V6 2018": 0.2065164316228118, "CHEVROLET VOLT PREMIER 2017": 0.2316223989408518, "TOYOTA RAV4 HYBRID 2017": 0.055653752888652736, "LEXUS RX 2020": 0.047792182371008345, "TOYOTA HIGHLANDER HYBRID 2018": 0.019259474082467202, "TOYOTA CAMRY HYBRID 2018": 0.11949733140330816, "TESLA AP2 MODEL S": NaN, "VOLKSWAGEN GOLF 7TH GEN": 0.1996863736436734, "TOYOTA MIRAI 2021": 0.11019259478417197, "LEXUS IS 2018": NaN, "TOYOTA HIGHLANDER 2017": 0.05279963713251727, "HYUNDAI SONATA HYBRID 2021": 0.3543918194389536, "VOLKSWAGEN ATLAS 1ST GEN": 0.21694647502209782, "LEXUS ES HYBRID 2019": 0.14775474433507507, "HYUNDAI GENESIS 2015-2016": 0.0814892037361157, "JEEP GRAND CHEROKEE 2019": 0.3126997097753535, "SUBARU ASCENT LIMITED 2019": NaN, "HONDA CR-V 2017": 5.652613829506629, "HONDA FIT 2018": 1.5217432826711779, "TOYOTA CAMRY 2021": 0.07910435053686729, "AUDI Q3 2ND GEN": 0.13535089102138698, "AUDI A3 3RD GEN": 0.14353941401245793, "LEXUS RX HYBRID 2017": 0.048663813961824696, "HONDA CIVIC 2022": 1.0748206908458815, "GENESIS G70 2018": 0.688303429295532, "CHRYSLER PACIFICA HYBRID 2018": 0.11083725786301112, "VOLKSWAGEN PASSAT 8TH GEN": 0.13315924904555493, "HONDA CR-V 2016": 0.488871482749128, "HYUNDAI IONIQ PHEV 2020": 0.2756096845519595, "GMC ACADIA DENALI 2018": 0.24055364003040136, "HYUNDAI SONATA 2019": 0.4473315280277132, "TOYOTA AVALON 2019": 0.026428086100632363, "TOYOTA C-HR 2018": 0.06075105822970755, "HONDA CR-V HYBRID 2019": 2.9906016360828276, "CHRYSLER PACIFICA 2020": 0.07748732608487266, "HYUNDAI IONIQ ELECTRIC 2020": NaN, "VOLKSWAGEN TIGUAN 2ND GEN": NaN, "LEXUS NX 2018": 0.16318394527060903, "KIA OPTIMA SX 2019 & 2016": 0.4378756841929454, "TOYOTA AVALON HYBRID 2019": NaN, "TOYOTA RAV4 HYBRID 2022": 0.36478548056633514, "HONDA PASSPORT 2021": 0.5993860184637646, "KIA K5 2021": 0.6271500841947655, "ACURA ILX 2016": 0.8435442647921855, "HYUNDAI IONIQ HYBRID 2017-2019": NaN, "KIA NIRO EV 2020": 0.40355577782011604, "SUBARU IMPREZA SPORT 2020": 0.11071291640854522, "CHRYSLER PACIFICA HYBRID 2017": 0.029812269495458284, "HYUNDAI KONA ELECTRIC 2019": NaN, "HYUNDAI ELANTRA HYBRID 2021": NaN, "HYUNDAI SANTA FE HYBRID 2022": NaN, "CHRYSLER PACIFICA 2018": 0.01705753895996445, "NISSAN ROGUE 2019": NaN, "KIA SORENTO GT LINE 2018": NaN, "COMMA BODY": NaN, "NISSAN LEAF 2018 Instrument Cluster": NaN, "LEXUS RX HYBRID 2020": 0.05790668871480552, "MAZDA CX-9 2021": NaN, "HYUNDAI SANTA FE 2022": 0.018126919430513307, "HYUNDAI SANTA FE PlUG-IN HYBRID 2022": 0.1331760659016062, "HONDA HRV 2019": 1.599688433820939, "TOYOTA AVALON HYBRID 2022": NaN, "SUBARU IMPREZA LIMITED 2019": 0.2514545160390271, "GENESIS G80 2017": NaN, "VOLKSWAGEN TAOS 1ST GEN": 0.09725484306423876, "KIA FORTE E 2018 & GT 2021": 0.20381871942480628, "CADILLAC ESCALADE ESV 2016": NaN, "TOYOTA C-HR 2021": 0.05016813984196128, "TOYOTA C-HR HYBRID 2018": 0.2521485862766935}} \ No newline at end of file diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml new file mode 100644 index 00000000000000..fb086fd13f2b44 --- /dev/null +++ b/selfdrive/car/torque_data/override.yaml @@ -0,0 +1,29 @@ +legend: [LAT_ACCEL_FACTOR, MAX_LAT_ACCEL_MEASURED, FRICTION] +### angle control +# Nissan appears to have torque +NISSAN X-TRAIL 2017: [.nan, 1.5, .nan] +NISSAN ALTIMA 2020: [.nan, 1.5, .nan] +NISSAN LEAF 2018 Instrument Cluster: [.nan, 1.5, .nan] +NISSAN LEAF 2018: [.nan, 1.5, .nan] +NISSAN ROGUE 2019: [.nan, 1.5, .nan] + +# Tesla has high torque +TESLA AP1 MODEL S: [.nan, 2.5, .nan] +TESLA AP2 MODEL S: [.nan, 2.5, .nan] + +# Guess +FORD ESCAPE 4TH GEN: [.nan, 1.5, .nan] +FORD FOCUS 4TH GEN: [.nan, 1.5, .nan] +### + +# No steering wheel +COMMA BODY: [.nan, 1000, .nan] + +# Totally new car +KIA EV6 2022: [3.0, 2.5, 0.05] + +# Dashcam or fallback configured as ideal car +mock: [10.0, 10, 0.0] + +# Manually checked +HONDA CIVIC 2022: [2.5, 1.2, 0.15] diff --git a/selfdrive/car/torque_data/params.yaml b/selfdrive/car/torque_data/params.yaml new file mode 100644 index 00000000000000..160f605488b7f7 --- /dev/null +++ b/selfdrive/car/torque_data/params.yaml @@ -0,0 +1,96 @@ +ACURA ILX 2016: [1.524988973896102, 0.519011053086259, 0.34236219253028] +ACURA RDX 2018: [0.9987728568686902, 0.5323765166196301, 0.303218805715844] +ACURA RDX 2020: [1.4314459806646749, 0.33874701282109954, 0.18048847083897598] +AUDI A3 3RD GEN: [1.5122414863077502, 1.7443517531719404, 0.15194151892450905] +AUDI Q3 2ND GEN: [1.4439223359448605, 1.2254955789112076, 0.1413798895978097] +CHEVROLET VOLT PREMIER 2017: [1.5961527626411784, 1.8422651988094612, 0.1572393918005158] +CHRYSLER PACIFICA 2018: [1.593387270257916, 1.3366521181047952, 0.13776367250652022] +CHRYSLER PACIFICA 2020: [1.4323553627965695, 1.509076559398423, 0.14328246159386085] +CHRYSLER PACIFICA HYBRID 2017: [1.3032470208409048, 1.06831764583744, 0.13287170990024627] +CHRYSLER PACIFICA HYBRID 2018: [1.6068280248761635, 1.2943025830995154, 0.1358557824293823] +CHRYSLER PACIFICA HYBRID 2019: [1.4624643614072217, 1.1958788168371808, 0.15748488008472716] +GENESIS G70 2018: [3.8520195946707947, 2.354697063349854, 0.06830285485626221] +GMC ACADIA DENALI 2018: [1.3181430320331884, 1.1853735340610179, 0.3450592280031644] +HONDA ACCORD 2018: [1.7135052593468778, 0.3461280068322071, 0.21579936052863807] +HONDA ACCORD HYBRID 2018: [1.6651615004829625, 0.30322180951193245, 0.2083000440586149] +HONDA CIVIC (BOSCH) 2019: [1.691708637466905, 0.40132900729454185, 0.25460295304024094] +HONDA CIVIC 2016: [1.6528895627785531, 0.4018518740819229, 0.25458812851328544] +HONDA CR-V 2016: [0.7667141440182675, 0.5927571534745969, 0.40909087636157127] +HONDA CR-V 2017: [2.01323205142022, 0.2700612209345081, 0.2238412881331528] +HONDA CR-V HYBRID 2019: [2.072034634644233, 0.7152085160516978, 0.20237105008376083] +HONDA FIT 2018: [1.5719981427109775, 0.5712761407108976, 0.110773383324281] +HONDA HRV 2019: [2.0661212805710205, 0.7521343418694775, 0.17760375789242094] +HONDA INSIGHT 2019: [1.5201671214069354, 0.5660229120683284, 0.25808042580281876] +HONDA ODYSSEY 2018: [1.8774809275211801, 0.8394431662987996, 0.2096978613792822] +HONDA PASSPORT 2021: [1.5305538930036766, 0.7956063674638759, 0.19599407381531284] +HONDA PILOT 2017: [1.7262026201812795, 0.9470005614967523, 0.21351430733218763] +HONDA RIDGELINE 2017: [1.4146525028237624, 0.7356572861629564, 0.23307177552211328] +HYUNDAI GENESIS 2015-2016: [1.8466226943929824, 1.5552063647830634, 0.0984484465421171] +HYUNDAI IONIQ ELECTRIC LIMITED 2019: [1.7662975472852054, 1.613755614526594, 0.17087579756306276] +HYUNDAI IONIQ PHEV 2020: [3.2928700076638537, 2.1193482926455656, 0.12463700961468778] +HYUNDAI IONIQ PLUG-IN HYBRID 2019: [2.970807902012267, 1.6312321830002083, 0.1088964990357482] +HYUNDAI KONA ELECTRIC 2019: [4.398306735170212, 3.2961956260770484, 0.08651833437845884] +HYUNDAI PALISADE 2020: [2.544642494803999, 1.8721703683337008, 0.1301424599248651] +HYUNDAI SANTA FE 2019: [3.0787027729757632, 2.6173437483495565, 0.1207019341823945] +HYUNDAI SANTA FE HYBRID 2022: [3.501877602644835, 2.729064118456137, 0.10384068104538963] +HYUNDAI SANTA FE PlUG-IN HYBRID 2022: [1.6953050513611045, 1.5837614296206861, 0.12672855941458458] +HYUNDAI SONATA 2019: [2.2200457811703953, 1.2967330275895228, 0.14039920986586393] +HYUNDAI SONATA 2020: [3.284505627881726, 2.1259108157250735, 0.08452048323586728] +HYUNDAI SONATA HYBRID 2021: [2.8990264092395734, 2.061410192222139, 0.0899805488717382] +JEEP GRAND CHEROKEE 2019: [1.7321233388827006, 1.289689569171081, 0.15046331002097185] +JEEP GRAND CHEROKEE V6 2018: [1.8776598027756923, 1.4057367824262523, 0.11725947414922003] +KIA K5 2021: [2.405339728085138, 1.460032270828705, 0.11650989850813716] +KIA NIRO EV 2020: [2.9215954981365337, 2.1500583840260044, 0.09236802474810267] +KIA SORENTO GT LINE 2018: [2.464854685101844, 1.5335274218367956, 0.12056170567599558] +KIA STINGER GT2 2018: [2.7499043387418967, 1.849652021986449, 0.12048334239559202] +LEXUS ES 2019: [2.0203086922726112, 2.134803912579666, 0.12757526789308554] +LEXUS ES HYBRID 2019: [2.392442298703042, 1.863360677810788, 0.17690002108856212] +LEXUS NX 2018: [2.302625600642627, 2.1382378491466625, 0.14986840878892838] +LEXUS NX 2020: [2.4331999786982936, 2.1045680431705414, 0.14099899317761067] +LEXUS NX HYBRID 2018: [2.4025593501080955, 1.8080446063815507, 0.15349361249519017] +LEXUS RX 2016: [1.5876816543130423, 1.0427699298523752, 0.21334066732397142] +LEXUS RX 2020: [1.5228812994274734, 1.431102486563665, 0.2093316728710659] +LEXUS RX HYBRID 2017: [1.6984261557042386, 1.3211501880159107, 0.1820354534928893] +LEXUS RX HYBRID 2020: [1.5522309889823778, 1.255230465866663, 0.2220954003055114] +MAZDA CX-9 2021: [1.7601682915983443, 1.0889677335154337, 0.17713792194297195] +SKODA SUPERB 3RD GEN: [1.166437404652981, 1.1686163012668165, 0.12194533036948708] +SUBARU FORESTER 2019: [3.6617001649776793, 2.342197172531713, 0.11075960785398745] +SUBARU IMPREZA LIMITED 2019: [1.0670704910352047, 0.8234374840709592, 0.20986563268614938] +SUBARU IMPREZA SPORT 2020: [2.6068223389108303, 2.134872342760203, 0.15261513193561627] +TOYOTA AVALON 2016: [2.5185770183845646, 1.7153346784214922, 0.10603968787111022] +TOYOTA AVALON 2019: [1.7036141952825095, 1.239619084240008, 0.08459830394899492] +TOYOTA AVALON 2022: [2.3154403649717357, 2.7777922854327124, 0.11453999639164605] +TOYOTA C-HR 2018: [1.5591084333664578, 1.271271459066948, 0.20259087058453193] +TOYOTA C-HR 2021: [1.7678810166088303, 1.3742176337919942, 0.2319674583741509] +TOYOTA CAMRY 2018: [2.1172995371905015, 1.7156177222420887, 0.13519250664782062] +TOYOTA CAMRY 2021: [2.6922769557433055, 2.3476510120007434, 0.1450430192989234] +TOYOTA CAMRY HYBRID 2018: [2.0974120828287774, 1.7996193116697359, 0.13823613467632756] +TOYOTA CAMRY HYBRID 2021: [2.6426668350384457, 2.3901492458927986, 0.16103875108816076] +TOYOTA COROLLA 2017: [3.117154369115421, 1.8438132575043773, 0.12289685869250652] +TOYOTA COROLLA HYBRID TSS2 2019: [2.3287672277252005, 1.8118712531729109, 0.2215868445753317] +TOYOTA COROLLA TSS2 2019: [2.4204464833010175, 1.9258612322678952, 0.20670411068012526] +TOYOTA HIGHLANDER 2017: [1.8696367437248915, 1.626293990451463, 0.17485372210240796] +TOYOTA HIGHLANDER 2020: [2.022340166827233, 1.6183134804881791, 0.14592306380054457] +TOYOTA HIGHLANDER HYBRID 2018: [1.9421825202382728, 1.6433903296845025, 0.16928956792275918] +TOYOTA HIGHLANDER HYBRID 2020: [2.103373061114133, 2.104015182965606, 0.14447040132184993] +TOYOTA MIRAI 2021: [2.506899832157829, 1.7417213930750164, 0.20182618449440565] +TOYOTA PRIUS 2017: [2.0183401513314294, 1.5023147650693636, 0.20856908464957724] +TOYOTA PRIUS TSS2 2021: [2.327639738920072, 1.9104337425537743, 0.2030762265549664] +TOYOTA RAV4 2017: [2.085695074355425, 2.2142832316984733, 0.13339165270103975] +TOYOTA RAV4 2019: [2.5038362866776835, 2.0993589721530252, 0.1552425356342368] +TOYOTA RAV4 2019 8965: [2.5084506298290377, 2.4216520504763475, 0.11992835265067918] +TOYOTA RAV4 2019 x02: [2.7209621987605024, 2.2148637653781593, 0.10862567142268198] +TOYOTA RAV4 HYBRID 2017: [1.9796257271652042, 1.7503987331707576, 0.14628860048885406] +TOYOTA RAV4 HYBRID 2019: [2.2271858492309153, 2.074844961405639, 0.14382216826893632] +TOYOTA RAV4 HYBRID 2019 8965: [2.1077397198131336, 1.8162215166877735, 0.13891369391200137] +TOYOTA RAV4 HYBRID 2019 x02: [2.803624333289342, 2.272367966572498, 0.11364569214387774] +TOYOTA RAV4 HYBRID 2022: [2.241883248393209, 1.9304407208090029, 0.1565442715453653] +TOYOTA RAV4 HYBRID 2022 x02: [3.044930631831037, 2.3979189796380918, 0.14023209146703736] +TOYOTA SIENNA 2018: [1.8660896232147548, 1.3208264576110418, 0.18799149615227198] +VOLKSWAGEN ARTEON 1ST GEN: [1.45136518053819, 1.3639364049316804, 0.23806361745695032] +VOLKSWAGEN ATLAS 1ST GEN: [1.4677006726964945, 1.6733266634075656, 0.12959584092073367] +VOLKSWAGEN GOLF 7TH GEN: [1.3750394140491293, 1.5814743077200641, 0.2018321939386586] +VOLKSWAGEN JETTA 7TH GEN: [1.2271623034089392, 1.216955117387, 0.19437384688370712] +VOLKSWAGEN PASSAT 8TH GEN: [1.3432120736752917, 1.7087275587362314, 0.19444383787326647] +VOLKSWAGEN TIGUAN 2ND GEN: [0.9711965500094828, 1.0001565939459098, 0.1465626137072916] +legend: [LAT_ACCEL_FACTOR, MAX_LAT_ACCEL_MEASURED, FRICTION] diff --git a/selfdrive/car/torque_data/substitute.yaml b/selfdrive/car/torque_data/substitute.yaml new file mode 100644 index 00000000000000..d368b2c6727a5d --- /dev/null +++ b/selfdrive/car/torque_data/substitute.yaml @@ -0,0 +1,75 @@ +MAZDA 3: MAZDA CX-9 2021 +MAZDA 6: MAZDA CX-9 2021 +MAZDA CX-5: MAZDA CX-9 2021 +MAZDA CX-5 2022: MAZDA CX-9 2021 +MAZDA CX-9: MAZDA CX-9 2021 + +TOYOTA ALPHARD HYBRID 2021 : TOYOTA SIENNA 2018 +TOYOTA ALPHARD 2020: TOYOTA SIENNA 2018 +TOYOTA PRIUS v 2017 : TOYOTA PRIUS 2017 +TOYOTA RAV4 2022: TOYOTA RAV4 HYBRID 2022 +TOYOTA C-HR HYBRID 2018: TOYOTA C-HR 2018 +LEXUS IS 2018: LEXUS NX 2018 +LEXUS CT HYBRID 2018 : LEXUS NX 2018 +LEXUS ES HYBRID 2018: TOYOTA CAMRY HYBRID 2018 +LEXUS NX HYBRID 2020: LEXUS NX 2020 +LEXUS RC 2020: LEXUS NX 2020 +TOYOTA AVALON HYBRID 2019: TOYOTA AVALON 2019 +TOYOTA AVALON HYBRID 2022: TOYOTA AVALON 2022 + +KIA OPTIMA SX 2019 & 2016: HYUNDAI SONATA 2020 +KIA OPTIMA HYBRID 2017 & SPORTS 2019: HYUNDAI SONATA 2020 +KIA FORTE E 2018 & GT 2021: HYUNDAI SONATA 2020 +KIA CEED INTRO ED 2019: HYUNDAI SONATA 2020 +KIA SELTOS 2021: HYUNDAI SONATA 2020 +KIA NIRO HYBRID 2019: KIA NIRO EV 2020 +KIA NIRO HYBRID 2021: KIA NIRO EV 2020 +HYUNDAI VELOSTER 2019: HYUNDAI SONATA 2019 +HYUNDAI I30 N LINE 2019 & GT 2018 DCT: HYUNDAI SONATA 2019 +HYUNDAI KONA 2020: HYUNDAI KONA ELECTRIC 2019 +HYUNDAI KONA HYBRID 2020: HYUNDAI KONA ELECTRIC 2019 +HYUNDAI IONIQ HYBRID 2017-2019: HYUNDAI IONIQ PLUG-IN HYBRID 2019 +HYUNDAI IONIQ HYBRID 2020-2022: HYUNDAI IONIQ PLUG-IN HYBRID 2019 +HYUNDAI IONIQ ELECTRIC 2020: HYUNDAI IONIQ PLUG-IN HYBRID 2019 +HYUNDAI ELANTRA 2017: HYUNDAI SONATA 2019 +HYUNDAI ELANTRA HYBRID 2021: HYUNDAI SONATA 2020 +HYUNDAI ELANTRA 2021: HYUNDAI SONATA 2020 +HYUNDAI TUCSON 2019: HYUNDAI SANTA FE 2019 +HYUNDAI SANTA FE 2022: HYUNDAI SANTA FE HYBRID 2022 +GENESIS G90 2017: GENESIS G70 2018 +GENESIS G80 2017: GENESIS G70 2018 +GENESIS G70 2020: HYUNDAI SONATA 2020 + +HONDA FREED 2020: HONDA ODYSSEY 2018 +HONDA CR-V EU 2016: HONDA CR-V 2016 +HONDA CIVIC SEDAN 1.6 DIESEL 2019: HONDA CIVIC (BOSCH) 2019 +HONDA E 2020: HONDA CIVIC (BOSCH) 2019 +HONDA ODYSSEY CHN 2019: HONDA ODYSSEY 2018 + +BUICK REGAL ESSENCE 2018: CHEVROLET VOLT PREMIER 2017 +CADILLAC ESCALADE ESV 2016: CHEVROLET VOLT PREMIER 2017 +CADILLAC ATS Premium Performance 2018: CHEVROLET VOLT PREMIER 2017 +CHEVROLET MALIBU PREMIER 2017: CHEVROLET VOLT PREMIER 2017 +HOLDEN ASTRA RS-V BK 2017: CHEVROLET VOLT PREMIER 2017 + +SKODA OCTAVIA 3RD GEN: SKODA SUPERB 3RD GEN +SKODA SCALA 1ST GEN: SKODA SUPERB 3RD GEN +SKODA KODIAQ 1ST GEN: SKODA SUPERB 3RD GEN +SKODA KAROQ 1ST GEN: SKODA SUPERB 3RD GEN +SKODA KAMIQ 1ST GEN: SKODA SUPERB 3RD GEN +VOLKSWAGEN T-ROC 1ST GEN: VOLKSWAGEN TIGUAN 2ND GEN +VOLKSWAGEN T-CROSS 1ST GEN: VOLKSWAGEN TIGUAN 2ND GEN +VOLKSWAGEN TOURAN 2ND GEN: VOLKSWAGEN TIGUAN 2ND GEN +VOLKSWAGEN TRANSPORTER T6.1: VOLKSWAGEN TIGUAN 2ND GEN +AUDI Q2 1ST GEN: VOLKSWAGEN TIGUAN 2ND GEN +VOLKSWAGEN TAOS 1ST GEN: VOLKSWAGEN TIGUAN 2ND GEN +VOLKSWAGEN POLO 6TH GEN: VOLKSWAGEN GOLF 7TH GEN +SEAT LEON 3RD GEN: VOLKSWAGEN GOLF 7TH GEN +SEAT ATECA 1ST GEN: VOLKSWAGEN GOLF 7TH GEN + +# Old subarus don't have much data guessing it's like low torque impreza +SUBARU OUTBACK 2018 - 2019: SUBARU IMPREZA LIMITED 2019 +SUBARU OUTBACK 2015 - 2017: SUBARU IMPREZA LIMITED 2019 +SUBARU FORESTER 2017 - 2018: SUBARU IMPREZA LIMITED 2019 +SUBARU LEGACY 2015 - 2018: SUBARU IMPREZA LIMITED 2019 +SUBARU ASCENT LIMITED 2019: SUBARU FORESTER 2019 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 05d016911b0276..662f8cc5b85247 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2d736ca51acc1ac06216631b0529b50d9a6d2170 \ No newline at end of file +41161c8d151b0c2017214cad0aad3156533ab868 From d8bfe2f0052390b8864910b00e43f61c0eeb9ff7 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Wed, 22 Jun 2022 19:20:07 -0700 Subject: [PATCH 159/436] Cleanup car interfaces (#24948) * remove interface overrides * Fix test * set torque tune for ev6 --- selfdrive/car/hyundai/interface.py | 6 +----- selfdrive/car/interfaces.py | 10 ++++++---- selfdrive/car/toyota/interface.py | 7 ------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 2739ebd8cc47f7..7cfce551005bfe 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -204,7 +204,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.80 ret.steerRatio = 13.75 tire_stiffness_factor = 0.5 - torque_params = CarInterfaceBase.get_torque_params(CAR.KIA_OPTIMA) set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) elif candidate == CAR.KIA_STINGER: ret.lateralTuning.pid.kf = 0.00005 @@ -245,10 +244,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.noOutput), get_safety_config(car.CarParams.SafetyModel.hyundaiHDA2)] tire_stiffness_factor = 0.65 - - ret.maxLateralAccel = 2. - # TODO override until there is more data - set_torque_tune(ret.lateralTuning, 2.0, 0.05) + set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) # Genesis elif candidate == CAR.GENESIS_G70: diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index c6a64fe919efe2..ced2ce8e61a430 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -119,12 +119,14 @@ def get_torque_params(candidate, default=float('NaN')): with open(TORQUE_PARAMS_PATH) as f: params = yaml.load(f, Loader=yaml.FullLoader) with open(TORQUE_OVERRIDE_PATH) as f: - params_override = yaml.load(f, Loader=yaml.FullLoader) + override = yaml.load(f, Loader=yaml.FullLoader) - assert len(set(sub.keys()) & set(params.keys()) & set(params_override.keys())) == 0 + # Ensure no overlap + if sum([candidate in x for x in [sub, params, override]]) > 1: + raise RuntimeError(f'{candidate} is defined twice in torque config') - if candidate in params_override: - out = params_override[candidate] + if candidate in override: + out = override[candidate] elif candidate in params: out = params[candidate] else: diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index d0f47897758467..75f2ab303393fc 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -54,9 +54,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 17.4 tire_stiffness_factor = 0.5533 ret.mass = 3340. * CV.LB_TO_KG + STD_CARGO_KG - # TODO override until there is enough data - ret.maxLateralAccel = 1.8 - torque_params = CarInterfaceBase.get_torque_params(CAR.PRIUS) set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) elif candidate in (CAR.RAV4, CAR.RAV4H): @@ -132,10 +129,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.mass = 3585. * CV.LB_TO_KG + STD_CARGO_KG # Average between ICE and Hybrid set_lat_tune(ret.lateralTuning, LatTunes.PID_D) - # TODO: remove once there's data - if candidate == CAR.RAV4_TSS2_2022: - ret.maxLateralAccel = CarInterfaceBase.get_torque_params(CAR.RAV4H_TSS2_2022)['MAX_LAT_ACCEL_MEASURED'] - # 2019+ RAV4 TSS2 uses two different steering racks and specific tuning seems to be necessary. # See https://github.com/commaai/openpilot/pull/21429#issuecomment-873652891 for fw in car_fw: From 221086857aac770f5d22273119e6175eeb06c729 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jun 2022 13:58:01 -0700 Subject: [PATCH 160/436] EV6: adjust steering thresholds (#24901) * EV6: adjust steering thresholds * Is there any friction * bump panda * no friction Co-authored-by: Harald Schafer --- panda | 2 +- selfdrive/car/hyundai/carstate.py | 8 +++++--- selfdrive/car/hyundai/values.py | 26 ++++++++++++++----------- selfdrive/car/torque_data/override.yaml | 4 ++-- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/panda b/panda index 4bc85ad40ad032..265245389208e1 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 4bc85ad40ad032672008eb75567892ba45e0b932 +Subproject commit 265245389208e1e6ada86b169e879c0a2e30426c diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 9d864faac4a220..6c82c3385686c4 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -5,7 +5,7 @@ from common.conversions import Conversions as CV from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine -from selfdrive.car.hyundai.values import DBC, STEER_THRESHOLD, FEATURES, HDA2_CAR, EV_CAR, HYBRID_CAR, Buttons +from selfdrive.car.hyundai.values import DBC, FEATURES, HDA2_CAR, EV_CAR, HYBRID_CAR, Buttons, CarControllerParams from selfdrive.car.interfaces import CarStateBase PREV_BUTTON_SAMPLES = 4 @@ -32,6 +32,8 @@ def __init__(self, CP): self.park_brake = False self.buttons_counter = 0 + self.params = CarControllerParams(CP) + def update(self, cp, cp_cam): if self.CP.carFingerprint in HDA2_CAR: return self.update_hda2(cp, cp_cam) @@ -61,7 +63,7 @@ def update(self, cp, cp_cam): 50, cp.vl["CGW1"]["CF_Gway_TurnSigLh"], cp.vl["CGW1"]["CF_Gway_TurnSigRh"]) ret.steeringTorque = cp.vl["MDPS12"]["CR_Mdps_StrColTq"] ret.steeringTorqueEps = cp.vl["MDPS12"]["CR_Mdps_OutTq"] - ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD + ret.steeringPressed = abs(ret.steeringTorque) > self.params.STEER_THRESHOLD ret.steerFaultTemporary = cp.vl["MDPS12"]["CF_Mdps_ToiUnavail"] != 0 or cp.vl["MDPS12"]["CF_Mdps_ToiFlt"] != 0 # cruise state @@ -157,7 +159,7 @@ def update_hda2(self, cp, cp_cam): ret.steeringAngleDeg = cp.vl["STEERING_SENSORS"]["STEERING_ANGLE"] * -1 ret.steeringTorque = cp.vl["MDPS"]["STEERING_COL_TORQUE"] ret.steeringTorqueEps = cp.vl["MDPS"]["STEERING_OUT_TORQUE"] - ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD + ret.steeringPressed = abs(ret.steeringTorque) > self.params.STEER_THRESHOLD ret.leftBlinker, ret.rightBlinker = self.update_blinker_from_lamp(50, cp.vl["BLINKERS"]["LEFT_LAMP"], cp.vl["BLINKERS"]["RIGHT_LAMP"]) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index cf47501588ee67..219e1619c0cfd0 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -13,21 +13,27 @@ class CarControllerParams: ACCEL_MAX = 2.0 # m/s def __init__(self, CP): + self.STEER_DELTA_UP = 3 + self.STEER_DELTA_DOWN = 7 + self.STEER_DRIVER_ALLOWANCE = 50 + self.STEER_DRIVER_MULTIPLIER = 2 + self.STEER_DRIVER_FACTOR = 1 + self.STEER_THRESHOLD = 150 + + if CP.carFingerprint in HDA2_CAR: + self.STEER_MAX = 270 + self.STEER_DRIVER_ALLOWANCE = 250 + self.STEER_DRIVER_MULTIPLIER = 2 + self.STEER_THRESHOLD = 250 + # To determine the limit for your car, find the maximum value that the stock LKAS will request. # If the max stock LKAS request is <384, add your car to this list. - if CP.carFingerprint in HDA2_CAR: - self.STEER_MAX = 150 elif CP.carFingerprint in (CAR.GENESIS_G80, CAR.GENESIS_G90, CAR.ELANTRA, CAR.HYUNDAI_GENESIS, CAR.ELANTRA_GT_I30, CAR.IONIQ, - CAR.IONIQ_EV_LTD, CAR.SANTA_FE_PHEV_2022, CAR.SONATA_LF, CAR.KIA_FORTE, CAR.KIA_NIRO_HEV, - CAR.KIA_OPTIMA_H, CAR.KIA_SORENTO, CAR.KIA_STINGER): + CAR.IONIQ_EV_LTD, CAR.SANTA_FE_PHEV_2022, CAR.SONATA_LF, CAR.KIA_FORTE, CAR.KIA_NIRO_HEV, + CAR.KIA_OPTIMA_H, CAR.KIA_SORENTO, CAR.KIA_STINGER): self.STEER_MAX = 255 else: self.STEER_MAX = 384 - self.STEER_DELTA_UP = 3 - self.STEER_DELTA_DOWN = 7 - self.STEER_DRIVER_ALLOWANCE = 50 - self.STEER_DRIVER_MULTIPLIER = 2 - self.STEER_DRIVER_FACTOR = 1 class CAR: @@ -1268,5 +1274,3 @@ class Buttons: CAR.KIA_EV6: dbc_dict('kia_ev6', None), CAR.SONATA_HYBRID: dbc_dict('hyundai_kia_generic', 'hyundai_kia_mando_front_radar'), } - -STEER_THRESHOLD = 150 diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index fb086fd13f2b44..a2200926c031aa 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -1,7 +1,7 @@ legend: [LAT_ACCEL_FACTOR, MAX_LAT_ACCEL_MEASURED, FRICTION] ### angle control # Nissan appears to have torque -NISSAN X-TRAIL 2017: [.nan, 1.5, .nan] +NISSAN X-TRAIL 2017: [.nan, 1.5, .nan] NISSAN ALTIMA 2020: [.nan, 1.5, .nan] NISSAN LEAF 2018 Instrument Cluster: [.nan, 1.5, .nan] NISSAN LEAF 2018: [.nan, 1.5, .nan] @@ -20,7 +20,7 @@ FORD FOCUS 4TH GEN: [.nan, 1.5, .nan] COMMA BODY: [.nan, 1000, .nan] # Totally new car -KIA EV6 2022: [3.0, 2.5, 0.05] +KIA EV6 2022: [3.0, 2.5, 0.0] # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] From 1dffd48a2bab07512af71cfa73e572d602977340 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 23 Jun 2022 15:07:34 -0700 Subject: [PATCH 161/436] count_events improvements --- selfdrive/debug/count_events.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/selfdrive/debug/count_events.py b/selfdrive/debug/count_events.py index c3870e0f9ef662..93dd5bdc470f25 100755 --- a/selfdrive/debug/count_events.py +++ b/selfdrive/debug/count_events.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 import sys +import math +import datetime from collections import Counter from pprint import pprint from tqdm import tqdm +from typing import cast from cereal.services import service_list from tools.lib.route import Route @@ -17,6 +20,8 @@ cams = [s for s in service_list if s.endswith('CameraState')] cnt_cameras = dict.fromkeys(cams, 0) + start_time = math.inf + end_time = -math.inf for q in tqdm(r.qlog_paths()): if q is None: continue @@ -31,6 +36,10 @@ if not msg.valid: cnt_valid[msg.which()] += 1 + end_time = max(end_time, msg.logMonoTime) + start_time = min(start_time, msg.logMonoTime) + + duration = (end_time - start_time) / 1e9 print("Events") pprint(cnt_events) @@ -42,4 +51,9 @@ print("\n") print("Cameras") for k, v in cnt_cameras.items(): - print(" ", k.ljust(20), v) + s = service_list[k] + expected_frames = int(s.frequency * duration / cast(float, s.decimation)) + print(" ", k.ljust(20), f"{v}, {v/expected_frames:.1%} of expected") + + print("\n") + print("Route duration", datetime.timedelta(seconds=duration)) From 3b495fcb0b004fd00d12bf059473d9d0b7f7c0b9 Mon Sep 17 00:00:00 2001 From: Ayushman Kumar <41910134+ayushmankumar7@users.noreply.github.com> Date: Fri, 24 Jun 2022 14:32:48 +0530 Subject: [PATCH 162/436] Navd added to README (#24953) (#24954) * Navd added to README * Update README.md Co-authored-by: Willem Melching --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c426b839d5e459..34b17625f90166 100755 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ Directory Structure ├── modeld # Driving and monitoring model runners ├── proclogd # Logs information from proc ├── sensord # IMU interface code + ├── navd # Turn-by-turn navigation ├── test # Unit tests, system tests, and a car simulator └── ui # The UI From 5cd8bef921c886e17ea6afc9fcdad44be72a998c Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Fri, 24 Jun 2022 12:37:30 +0200 Subject: [PATCH 163/436] use correct tty device for serial --- tools/serial/connect.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/serial/connect.sh b/tools/serial/connect.sh index 037152019fb6cf..a073310b0a8a43 100755 --- a/tools/serial/connect.sh +++ b/tools/serial/connect.sh @@ -1,8 +1,8 @@ #!/bin/bash while true; do - if ls /dev/ttyUSB* 2> /dev/null; then - sudo screen /dev/ttyUSB* 115200 + if ls /dev/serial/by-id/usb-FTDI_FT230X* 2> /dev/null; then + sudo screen /dev/serial/by-id/usb-FTDI_FT230X* 115200 fi sleep 0.005 done From b854e67e91105da7301936aab8fd55f94dd46fbb Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 24 Jun 2022 17:56:33 +0200 Subject: [PATCH 164/436] Laikad: minor refactor (#24956) extract code to get_est_pos func --- selfdrive/locationd/laikad.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index f1997bceb0634b..ba665b75309847 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -62,6 +62,16 @@ def cache_ephemeris(self, t: GPSTime): cls=CacheSerializer)) self.last_cached_t = t + def get_est_pos(self, t, processed_measurements): + if self.last_pos_fix_t is None or abs(self.last_pos_fix_t - t) >= 2: + min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 + pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) + if len(pos_fix) > 0: + self.last_pos_fix = pos_fix[:3] + self.last_pos_residual = pos_fix_residual + self.last_pos_fix_t = t + return self.last_pos_fix + def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': t = ublox_mono_time * 1e-9 @@ -73,17 +83,11 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): new_meas = read_raw_ublox(report) processed_measurements = process_measurements(new_meas, self.astro_dog) - if self.last_pos_fix_t is None or abs(self.last_pos_fix_t - t) >= 2: - min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 - pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) - if len(pos_fix) > 0: - self.last_pos_fix = pos_fix[:3] - self.last_pos_residual = pos_fix_residual - self.last_pos_fix_t = t + est_pos = self.get_est_pos(t, processed_measurements) - corrected_measurements = correct_measurements(processed_measurements, self.last_pos_fix, self.astro_dog) if self.last_pos_fix_t is not None else [] + corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) if len(est_pos) > 0 else [] - self.update_localizer(self.last_pos_fix, t, corrected_measurements) + self.update_localizer(est_pos, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() @@ -116,7 +120,7 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement valid = self.kf_valid(t) if not all(valid): if not valid[0]: - cloudlog.info("Init gnss kalman filter") + cloudlog.info("Kalman filter uninitialized") elif not valid[1]: cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: @@ -133,7 +137,7 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement # Ensure gnss filter is updated even with no new measurements self.gnss_kf.predict(t) - def kf_valid(self, t: float): + def kf_valid(self, t: float) -> List[bool]: filter_time = self.gnss_kf.filter.filter_time return [filter_time is not None, filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, From e26db5dc91e2dd6348cb1dd79ce1549b74f39309 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Fri, 24 Jun 2022 15:32:01 -0400 Subject: [PATCH 165/436] VW MQB: Add FW for 2016 Volkswagen Passat (#24957) * VW MQB: Add FW for 2016 Passat B8 Passat B8 TDi 2.0 240HP DSG 7 Europe * mechanical sort Co-authored-by: Pierre Christen --- selfdrive/car/volkswagen/values.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 9b6f6a16c31723..0148a138a29df9 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -438,6 +438,7 @@ class VWCarInfo(CarInfo): }, CAR.PASSAT_MK8: { (Ecu.engine, 0x7e0, None): [ + b'\xf1\x8703N906026E \xf1\x892114', b'\xf1\x8704E906023AH\xf1\x893379', b'\xf1\x8704L906026ET\xf1\x891990', b'\xf1\x8704L906026GA\xf1\x892013', @@ -450,17 +451,20 @@ class VWCarInfo(CarInfo): b'\xf1\x870D9300014L \xf1\x895002', b'\xf1\x870D9300041A \xf1\x894801', b'\xf1\x870DD300045T \xf1\x891601', + b'\xf1\x870DL300011H \xf1\x895201', b'\xf1\x870GC300042H \xf1\x891404', ], (Ecu.srs, 0x715, None): [ b'\xf1\x873Q0959655AE\xf1\x890195\xf1\x82\r56140056130012416612124111', b'\xf1\x873Q0959655AN\xf1\x890306\xf1\x82\r58160058140013036914110311', + b'\xf1\x873Q0959655BA\xf1\x890195\xf1\x82\r56140056130012516612125111', b'\xf1\x873Q0959655BB\xf1\x890195\xf1\x82\r56140056130012026612120211', b'\xf1\x873Q0959655BK\xf1\x890703\xf1\x82\0165915005914001344701311442900', b'\xf1\x873Q0959655CN\xf1\x890720\xf1\x82\x0e5915005914001305701311052900', b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\02315120011111200631145171716121691132111', ], (Ecu.eps, 0x712, None): [ + b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566B00611A1', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0060803', b'\xf1\x875Q0909143M \xf1\x892041\xf1\x820522B0080803', b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521B00606A1', From 062a8bcdbd78826cbb035c1dabc6ebd18a9367dd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 24 Jun 2022 13:01:49 -0700 Subject: [PATCH 166/436] cleanup torque tuning config (#24951) --- selfdrive/car/hyundai/interface.py | 12 ++--- selfdrive/car/interfaces.py | 59 +++++++++++++-------- selfdrive/car/toyota/interface.py | 8 ++- selfdrive/car/toyota/tunes.py | 6 +-- selfdrive/controls/lib/latcontrol_torque.py | 10 ---- 5 files changed, 45 insertions(+), 50 deletions(-) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 7cfce551005bfe..58a67f58cef7fb 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -7,7 +7,6 @@ from selfdrive.car import STD_CARGO_KG, create_button_enable_events, create_button_event, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase from selfdrive.car.disable_ecu import disable_ecu -from selfdrive.controls.lib.latcontrol_torque import set_torque_tune ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName @@ -51,7 +50,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.stopAccel = 0.0 ret.longitudinalActuatorDelayUpperBound = 1.0 # s - torque_params = CarInterfaceBase.get_torque_params(candidate) if candidate in (CAR.SANTA_FE, CAR.SANTA_FE_2022, CAR.SANTA_FE_HEV_2022, CAR.SANTA_FE_PHEV_2022): ret.lateralTuning.pid.kf = 0.00005 ret.mass = 3982. * CV.LB_TO_KG + STD_CARGO_KG @@ -66,7 +64,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.84 ret.steerRatio = 13.27 * 1.15 # 15% higher at the center seems reasonable tire_stiffness_factor = 0.65 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.SONATA_LF: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 4497. * CV.LB_TO_KG @@ -96,13 +94,13 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.72 ret.steerRatio = 12.9 tire_stiffness_factor = 0.65 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.ELANTRA_HEV_2021: ret.mass = (3017. * CV.LB_TO_KG) + STD_CARGO_KG ret.wheelbase = 2.72 ret.steerRatio = 12.9 tire_stiffness_factor = 0.65 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.HYUNDAI_GENESIS: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 2060. + STD_CARGO_KG @@ -204,7 +202,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.80 ret.steerRatio = 13.75 tire_stiffness_factor = 0.5 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate == CAR.KIA_STINGER: ret.lateralTuning.pid.kf = 0.00005 ret.mass = 1825. + STD_CARGO_KG @@ -244,7 +242,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.noOutput), get_safety_config(car.CarParams.SafetyModel.hyundaiHDA2)] tire_stiffness_factor = 0.65 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION']) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) # Genesis elif candidate == CAR.GENESIS_G70: diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index ced2ce8e61a430..e0b38d9e20818c 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -20,11 +20,36 @@ MAX_CTRL_SPEED = (V_CRUISE_MAX + 4) * CV.KPH_TO_MS ACCEL_MAX = 2.0 ACCEL_MIN = -3.5 + TORQUE_PARAMS_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/params.yaml') TORQUE_OVERRIDE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/override.yaml') TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/substitute.yaml') +def get_torque_params(candidate, default=float('NaN')): + with open(TORQUE_SUBSTITUTE_PATH) as f: + sub = yaml.load(f, Loader=yaml.FullLoader) + if candidate in sub: + candidate = sub[candidate] + + with open(TORQUE_PARAMS_PATH) as f: + params = yaml.load(f, Loader=yaml.FullLoader) + with open(TORQUE_OVERRIDE_PATH) as f: + override = yaml.load(f, Loader=yaml.FullLoader) + + # Ensure no overlap + if sum([candidate in x for x in [sub, params, override]]) > 1: + raise RuntimeError(f'{candidate} is defined twice in torque config') + + if candidate in override: + out = override[candidate] + elif candidate in params: + out = params[candidate] + else: + raise NotImplementedError(f"Did not find torque params for {candidate}") + return {key:out[i] for i, key in enumerate(params['legend'])} + + # generic car and radar interfaces class CarInterfaceBase(ABC): @@ -85,7 +110,7 @@ def get_std_params(candidate, fingerprint): ret.steerControlType = car.CarParams.SteerControlType.torque ret.minSteerSpeed = 0. ret.wheelSpeedFactor = 1.0 - ret.maxLateralAccel = CarInterfaceBase.get_torque_params(candidate)['MAX_LAT_ACCEL_MEASURED'] + ret.maxLateralAccel = get_torque_params(candidate)['MAX_LAT_ACCEL_MEASURED'] ret.pcmCruise = True # openpilot's state is tied to the PCM's cruise state on most cars ret.minEnableSpeed = -1. # enable is done by stock ACC, so ignore this @@ -110,28 +135,16 @@ def get_std_params(candidate, fingerprint): return ret @staticmethod - def get_torque_params(candidate, default=float('NaN')): - with open(TORQUE_SUBSTITUTE_PATH) as f: - sub = yaml.load(f, Loader=yaml.FullLoader) - if candidate in sub: - candidate = sub[candidate] - - with open(TORQUE_PARAMS_PATH) as f: - params = yaml.load(f, Loader=yaml.FullLoader) - with open(TORQUE_OVERRIDE_PATH) as f: - override = yaml.load(f, Loader=yaml.FullLoader) - - # Ensure no overlap - if sum([candidate in x for x in [sub, params, override]]) > 1: - raise RuntimeError(f'{candidate} is defined twice in torque config') - - if candidate in override: - out = override[candidate] - elif candidate in params: - out = params[candidate] - else: - raise NotImplementedError(f"Did not find torque params for {candidate}") - return {key:out[i] for i, key in enumerate(params['legend'])} + def configure_torque_tune(candidate, tune, steering_angle_deadzone_deg=0.0): + params = get_torque_params(candidate) + + tune.init('torque') + tune.torque.useSteeringAngle = True + tune.torque.kp = 1.0 / params['LAT_ACCEL_FACTOR'] + tune.torque.kf = 1.0 / params['LAT_ACCEL_FACTOR'] + tune.torque.ki = 0.1 / params['LAT_ACCEL_FACTOR'] + tune.torque.friction = params['FRICTION'] + tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg @abstractmethod def _update(self, c: car.CarControl) -> car.CarState: diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 75f2ab303393fc..fdb923f693cc46 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -2,7 +2,6 @@ from cereal import car from common.conversions import Conversions as CV from panda import Panda -from selfdrive.controls.lib.latcontrol_torque import set_torque_tune from selfdrive.car.toyota.tunes import LatTunes, LongTunes, set_long_tune, set_lat_tune from selfdrive.car.toyota.values import Ecu, CAR, ToyotaFlags, TSS2_CAR, RADAR_ACC_CAR, NO_DSU_CAR, MIN_ACC_SPEED, EPS_SCALE, EV_HYBRID_CAR, CarControllerParams from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config @@ -32,9 +31,8 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.stoppingControl = False # Toyota starts braking more when it thinks you want to stop stop_and_go = False - torque_params = CarInterfaceBase.get_torque_params(candidate) steering_angle_deadzone_deg = 0.0 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg) if candidate == CAR.PRIUS: stop_and_go = True @@ -46,7 +44,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl for fw in car_fw: if fw.ecu == "eps" and not fw.fwVersion == b'8965B47060\x00\x00\x00\x00\x00\x00': steering_angle_deadzone_deg = 1.0 - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg) elif candidate == CAR.PRIUS_V: stop_and_go = True @@ -54,7 +52,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.steerRatio = 17.4 tire_stiffness_factor = 0.5533 ret.mass = 3340. * CV.LB_TO_KG + STD_CARGO_KG - set_torque_tune(ret.lateralTuning, torque_params['LAT_ACCEL_FACTOR'], torque_params['FRICTION'], steering_angle_deadzone_deg) + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning, steering_angle_deadzone_deg) elif candidate in (CAR.RAV4, CAR.RAV4H): stop_and_go = True if (candidate in CAR.RAV4H) else False diff --git a/selfdrive/car/toyota/tunes.py b/selfdrive/car/toyota/tunes.py index 3de6daae21b468..a8b8758d893344 100644 --- a/selfdrive/car/toyota/tunes.py +++ b/selfdrive/car/toyota/tunes.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 from enum import Enum -from selfdrive.controls.lib.latcontrol_torque import set_torque_tune class LongTunes(Enum): PEDAL = 0 @@ -24,7 +23,6 @@ class LatTunes(Enum): PID_L = 13 PID_M = 14 PID_N = 15 - TORQUE = 16 ###### LONG ###### @@ -51,9 +49,7 @@ def set_long_tune(tune, name): ###### LAT ###### def set_lat_tune(tune, name, MAX_LAT_ACCEL=2.5, FRICTION=0.01, steering_angle_deadzone_deg=0.0, use_steering_angle=True): - if name == LatTunes.TORQUE: - set_torque_tune(tune, MAX_LAT_ACCEL, FRICTION, steering_angle_deadzone_deg) - elif 'PID' in str(name): + if 'PID' in str(name): tune.init('pid') tune.pid.kiBP = [0.0] tune.pid.kpBP = [0.0] diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index f72ffc4b88ea7b..014c3480b80254 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -22,16 +22,6 @@ FRICTION_THRESHOLD = 0.2 -def set_torque_tune(tune, MAX_LAT_ACCEL=2.5, FRICTION=0.01, steering_angle_deadzone_deg=0.0): - tune.init('torque') - tune.torque.useSteeringAngle = True - tune.torque.kp = 1.0 / MAX_LAT_ACCEL - tune.torque.kf = 1.0 / MAX_LAT_ACCEL - tune.torque.ki = 0.1 / MAX_LAT_ACCEL - tune.torque.friction = FRICTION - tune.torque.steeringAngleDeadzoneDeg = steering_angle_deadzone_deg - - class LatControlTorque(LatControl): def __init__(self, CP, CI): super().__init__(CP, CI) From 6721f0ef57fd5bda5045e80edac815adcfdef380 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 24 Jun 2022 14:29:46 -0700 Subject: [PATCH 167/436] fix carla test fake driverState (#24959) use driverstatev2 --- tools/sim/bridge.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/sim/bridge.py b/tools/sim/bridge.py index faf67c3ef52ce1..17dc2c7a62f4d5 100755 --- a/tools/sim/bridge.py +++ b/tools/sim/bridge.py @@ -198,12 +198,12 @@ def gps_callback(gps, vehicle_state): def fake_driver_monitoring(exit_event: threading.Event): - pm = messaging.PubMaster(['driverState', 'driverMonitoringState']) + pm = messaging.PubMaster(['driverStateV2', 'driverMonitoringState']) while not exit_event.is_set(): # dmonitoringmodeld output - dat = messaging.new_message('driverState') - dat.driverState.faceProb = 1.0 - pm.send('driverState', dat) + dat = messaging.new_message('driverStateV2') + dat.driverStateV2.leftDriverData.faceProb = 1.0 + pm.send('driverStateV2', dat) # dmonitoringd output dat = messaging.new_message('driverMonitoringState') From 10fb2b9456b9d0d065b411ac64fd97831ee2fa8c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 24 Jun 2022 15:16:46 -0700 Subject: [PATCH 168/436] Speed up YAML parsing with CSafeLoader (#24958) Use CSafeLoader --- selfdrive/car/interfaces.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index e0b38d9e20818c..136337c5a48f01 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -26,16 +26,16 @@ TORQUE_SUBSTITUTE_PATH = os.path.join(BASEDIR, 'selfdrive/car/torque_data/substitute.yaml') -def get_torque_params(candidate, default=float('NaN')): +def get_torque_params(candidate): with open(TORQUE_SUBSTITUTE_PATH) as f: - sub = yaml.load(f, Loader=yaml.FullLoader) + sub = yaml.load(f, Loader=yaml.CSafeLoader) if candidate in sub: candidate = sub[candidate] with open(TORQUE_PARAMS_PATH) as f: - params = yaml.load(f, Loader=yaml.FullLoader) + params = yaml.load(f, Loader=yaml.CSafeLoader) with open(TORQUE_OVERRIDE_PATH) as f: - override = yaml.load(f, Loader=yaml.FullLoader) + override = yaml.load(f, Loader=yaml.CSafeLoader) # Ensure no overlap if sum([candidate in x for x in [sub, params, override]]) > 1: @@ -47,7 +47,7 @@ def get_torque_params(candidate, default=float('NaN')): out = params[candidate] else: raise NotImplementedError(f"Did not find torque params for {candidate}") - return {key:out[i] for i, key in enumerate(params['legend'])} + return {key: out[i] for i, key in enumerate(params['legend'])} # generic car and radar interfaces From 379dc24ecad7f1bdc9c11fbb2416cc2d68b3a297 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 24 Jun 2022 16:49:56 -0700 Subject: [PATCH 169/436] can replay: get logs in parallel (#24960) * get can replay segs in parallel * total not needed --- tools/replay/can_replay.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/replay/can_replay.py b/tools/replay/can_replay.py index b212abc4312002..0b8b4fe0a19a53 100755 --- a/tools/replay/can_replay.py +++ b/tools/replay/can_replay.py @@ -2,6 +2,7 @@ import os import time import threading +import multiprocessing from tqdm import tqdm os.environ['FILEREADER_CACHE'] = '1' @@ -9,7 +10,7 @@ from common.basedir import BASEDIR from common.realtime import config_realtime_process, Ratekeeper, DT_CTRL from selfdrive.boardd.boardd import can_capnp_to_can_list -from tools.lib.logreader import LogReader +from tools.plotjuggler.juggle import load_segment from panda import Panda try: @@ -94,11 +95,10 @@ def connect(): ROUTE = "77611a1fac303767/2020-03-24--09-50-38" REPLAY_SEGS = list(range(10, 16)) # route has 82 segments available CAN_MSGS = [] - for i in tqdm(REPLAY_SEGS): - log_url = f"https://commadataci.blob.core.windows.net/openpilotci/{ROUTE}/{i}/rlog.bz2" - lr = LogReader(log_url) - CAN_MSGS += [can_capnp_to_can_list(m.can) for m in lr if m.which() == 'can'] - + logs = [f"https://commadataci.blob.core.windows.net/openpilotci/{ROUTE}/{i}/rlog.bz2" for i in REPLAY_SEGS] + with multiprocessing.Pool(24) as pool: + for lr in tqdm(pool.map(load_segment, logs)): + CAN_MSGS += [can_capnp_to_can_list(m.can) for m in lr if m.which() == 'can'] # set both to cycle ignition IGN_ON = int(os.getenv("ON", "0")) From 72edc309327e8f6a1630e06d27cb21d16ba1e3ae Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 25 Jun 2022 03:07:29 -0700 Subject: [PATCH 170/436] Hyundai: remove bad esp fingerprint (#24952) Remove unknown "esp" fp --- selfdrive/car/hyundai/values.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 219e1619c0cfd0..645d2f523ba621 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -1146,9 +1146,6 @@ class Buttons: b'\xf1\000DNhe SCC F-CUP 1.00 1.02 99110-L5000 ', b'\xf1\x8799110L5000\xf1\000DNhe SCC F-CUP 1.00 1.02 99110-L5000 ', ], - (Ecu.esp, 0x7b0, None): [ - b'\xf1\x87\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x81\x00\x00\x00\x00\x00\x00\x00\x00', - ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x8756310-L5500\xf1\x00DN8 MDPS C 1.00 1.02 56310-L5500 4DNHC102', b'\xf1\x8756310-L5450\xf1\x00DN8 MDPS C 1.00 1.02 56310-L5450 4DNHC102', From 461f747247add7d4c50344042997427cdb2a90fa Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 27 Jun 2022 17:18:56 +0800 Subject: [PATCH 171/436] logger.cc: remove unused function append_property (#24966) remove append_property --- selfdrive/loggerd/logger.cc | 9 --------- 1 file changed, 9 deletions(-) diff --git a/selfdrive/loggerd/logger.cc b/selfdrive/loggerd/logger.cc index 5aed47e291b75b..8038f1926c8d47 100644 --- a/selfdrive/loggerd/logger.cc +++ b/selfdrive/loggerd/logger.cc @@ -19,15 +19,6 @@ #include "common/swaglog.h" #include "common/version.h" -// ***** logging helpers ***** - -void append_property(const char* key, const char* value, void *cookie) { - std::vector > *properties = - (std::vector > *)cookie; - - properties->push_back(std::make_pair(std::string(key), std::string(value))); -} - // ***** log metadata ***** kj::Array logger_build_init_data() { MessageBuilder msg; From 748bbac3444cc30e6dafc2f0bda60b46f3fc8d5a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 27 Jun 2022 17:19:46 +0800 Subject: [PATCH 172/436] loggerd: remove rotate_lock (#24969) remove lock --- selfdrive/loggerd/loggerd.cc | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/selfdrive/loggerd/loggerd.cc b/selfdrive/loggerd/loggerd.cc index 4086f4991ef87a..a75ab2c92b2b5c 100644 --- a/selfdrive/loggerd/loggerd.cc +++ b/selfdrive/loggerd/loggerd.cc @@ -6,7 +6,6 @@ ExitHandler do_exit; struct LoggerdState { LoggerState logger = {}; char segment_path[4096]; - std::mutex rotate_lock; std::atomic rotate_segment; std::atomic last_camera_seen_tms; std::atomic ready_to_rotate; // count of encoders ready to rotate @@ -15,15 +14,12 @@ struct LoggerdState { }; void logger_rotate(LoggerdState *s) { - { - std::unique_lock lk(s->rotate_lock); - int segment = -1; - int err = logger_next(&s->logger, LOG_ROOT.c_str(), s->segment_path, sizeof(s->segment_path), &segment); - assert(err == 0); - s->rotate_segment = segment; - s->ready_to_rotate = 0; - s->last_rotate_tms = millis_since_boot(); - } + int segment = -1; + int err = logger_next(&s->logger, LOG_ROOT.c_str(), s->segment_path, sizeof(s->segment_path), &segment); + assert(err == 0); + s->rotate_segment = segment; + s->ready_to_rotate = 0; + s->last_rotate_tms = millis_since_boot(); LOGW((s->logger.part == 0) ? "logging to %s" : "rotated to %s", s->segment_path); } From 0aa9ae21d4068d1e8099ff4256d86a204ec31c1c Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 27 Jun 2022 17:20:13 +0800 Subject: [PATCH 173/436] FfmpegEncoder: free codec_ctx in encoder_close (#24967) free context --- selfdrive/loggerd/encoder/ffmpeg_encoder.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/loggerd/encoder/ffmpeg_encoder.cc b/selfdrive/loggerd/encoder/ffmpeg_encoder.cc index 22587819a45d2b..5f8d140e8b395b 100644 --- a/selfdrive/loggerd/encoder/ffmpeg_encoder.cc +++ b/selfdrive/loggerd/encoder/ffmpeg_encoder.cc @@ -68,7 +68,9 @@ void FfmpegEncoder::encoder_open(const char* path) { void FfmpegEncoder::encoder_close() { if (!is_open) return; + writer_close(); + avcodec_free_context(&codec_ctx); is_open = false; } From 684d4b75a1c2f349da1e00dae9a40c39e3bb3607 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Mon, 27 Jun 2022 15:33:46 +0200 Subject: [PATCH 174/436] Log SOM power draw (#24975) * log SOM power draw * bump cereal Co-authored-by: Comma Device Co-authored-by: Willem Melching --- cereal | 2 +- selfdrive/thermald/thermald.py | 13 +++++++------ system/hardware/base.py | 4 ++++ system/hardware/pc/hardware.py | 3 +++ system/hardware/tici/hardware.py | 3 +++ 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cereal b/cereal index c6acc0698a604e..3fef4f7861e041 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit c6acc0698a604e715e960250359b6bf97e4987e3 +Subproject commit 3fef4f7861e04193fdb11dbd5b0eaf6d2b102ee1 diff --git a/selfdrive/thermald/thermald.py b/selfdrive/thermald/thermald.py index 6cf5c428a88d1d..f20c649b4e906d 100755 --- a/selfdrive/thermald/thermald.py +++ b/selfdrive/thermald/thermald.py @@ -348,12 +348,13 @@ def thermald_thread(end_event, hw_queue): power_monitor.calculate(peripheralState, onroad_conditions["ignition"]) msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used() msg.deviceState.carBatteryCapacityUwh = max(0, power_monitor.get_car_battery_capacity()) - current_power_draw = HARDWARE.get_current_power_draw() # pylint: disable=assignment-from-none - if current_power_draw is not None: - statlog.sample("power_draw", current_power_draw) - msg.deviceState.powerDrawW = current_power_draw - else: - msg.deviceState.powerDrawW = 0 + current_power_draw = HARDWARE.get_current_power_draw() + statlog.sample("power_draw", current_power_draw) + msg.deviceState.powerDrawW = current_power_draw + + som_power_draw = HARDWARE.get_som_power_draw() + statlog.sample("som_power_draw", som_power_draw) + msg.deviceState.somPowerDrawW = som_power_draw # Check if we need to disable charging (handled by boardd) msg.deviceState.chargingDisabled = power_monitor.should_disable_charging(onroad_conditions["ignition"], in_car, off_ts) diff --git a/system/hardware/base.py b/system/hardware/base.py index 8684386f6204ce..b052a48c5b3579 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -86,6 +86,10 @@ def get_usb_present(self): def get_current_power_draw(self): pass + @abstractmethod + def get_som_power_draw(self): + pass + @abstractmethod def shutdown(self): pass diff --git a/system/hardware/pc/hardware.py b/system/hardware/pc/hardware.py index b2c4a4343b2b43..60d14e4a6b1568 100644 --- a/system/hardware/pc/hardware.py +++ b/system/hardware/pc/hardware.py @@ -55,6 +55,9 @@ def get_usb_present(self): def get_current_power_draw(self): return 0 + + def get_som_power_draw(self): + return 0 def shutdown(self): print("SHUTDOWN!") diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index a69d3eb74350fd..66cb98c606a64e 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -362,6 +362,9 @@ def get_usb_present(self): def get_current_power_draw(self): return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6) + def get_som_power_draw(self): + return (self.read_param_file("/sys/class/power_supply/bms/voltage_now", int) * self.read_param_file("/sys/class/power_supply/bms/current_now", int) / 1e12) + def shutdown(self): # Note that for this to work and have the device stay powered off, the panda needs to be in UsbPowerMode::CLIENT! os.system("sudo poweroff") From de0c12e5af0e550c194510a19253dd9100b1a9f7 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 27 Jun 2022 15:34:36 +0200 Subject: [PATCH 175/436] calibrationd: start faster by not waiting for carParams (#24976) * calibrationd: start faster by not waiting for carParams * fix process replay * update ref --- selfdrive/locationd/calibrationd.py | 10 ++++++---- selfdrive/test/process_replay/process_replay.py | 3 ++- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index e092c939ae3126..81bcc6ce1c9b8d 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -12,7 +12,7 @@ import numpy as np from typing import List, NoReturn, Optional -from cereal import car, log +from cereal import log import cereal.messaging as messaging from common.conversions import Conversions as CV from common.params import Params, put_nonblocking @@ -62,7 +62,7 @@ class Calibrator: def __init__(self, param_put: bool = False): self.param_put = param_put - self.CP = car.CarParams.from_bytes(Params().get("CarParams", block=True)) + self.not_car = False # Read saved calibration params = Params() @@ -192,7 +192,7 @@ def get_msg(self) -> capnp.lib.capnp._DynamicStructBuilder: liveCalibration.rpyCalib = smooth_rpy.tolist() liveCalibration.rpyCalibSpread = self.calib_spread.tolist() - if self.CP.notCar: + if self.not_car: extrinsic_matrix = get_view_frame_from_road_frame(0, 0, 0, model_height) liveCalibration.validBlocks = INPUTS_NEEDED liveCalibration.calStatus = Calibration.CALIBRATED @@ -212,7 +212,7 @@ def calibrationd_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[m set_realtime_priority(1) if sm is None: - sm = messaging.SubMaster(['cameraOdometry', 'carState'], poll=['cameraOdometry']) + sm = messaging.SubMaster(['cameraOdometry', 'carState', 'carParams'], poll=['cameraOdometry']) if pm is None: pm = messaging.PubMaster(['liveCalibration']) @@ -223,6 +223,8 @@ def calibrationd_thread(sm: Optional[messaging.SubMaster] = None, pm: Optional[m timeout = 0 if sm.frame == -1 else 100 sm.update(timeout) + calibrator.not_car = sm['carParams'].notCar + if sm.updated['cameraOdometry']: calibrator.handle_v_ego(sm['carState'].vEgo) new_rpy = calibrator.handle_cam_odom(sm['cameraOdometry'].trans, diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 62c5eb4826ac53..228f07c4c246f2 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -282,7 +282,8 @@ def ublox_rcv_callback(msg): proc_name="calibrationd", pub_sub={ "carState": ["liveCalibration"], - "cameraOdometry": [] + "cameraOdometry": [], + "carParams": [], }, ignore=["logMonoTime", "valid"], init_callback=get_car_params, diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 662f8cc5b85247..292c41b9789abb 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -41161c8d151b0c2017214cad0aad3156533ab868 +b55de9fdb2416e63ab554c95233a78219d8d3db9 \ No newline at end of file From 240c44e50c4dd44c2237f4f37d716fd898160917 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Mon, 27 Jun 2022 06:36:27 -0700 Subject: [PATCH 176/436] snapshot: fix rgb overflow (#24963) clamp rgb --- system/camerad/snapshot/snapshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/camerad/snapshot/snapshot.py b/system/camerad/snapshot/snapshot.py index 9a81937eec3ceb..946c220a770310 100755 --- a/system/camerad/snapshot/snapshot.py +++ b/system/camerad/snapshot/snapshot.py @@ -39,7 +39,7 @@ def yuv_to_rgb(y, u, v): [0.00000, -0.39465, 2.03211], [1.13983, -0.58060, 0.00000], ]) - rgb = np.dot(yuv, m) + rgb = np.dot(yuv, m).clip(0, 255) return rgb.astype(np.uint8) From 915b4928ff044302bba0e8bdf15f154646f528e6 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 27 Jun 2022 16:03:26 +0200 Subject: [PATCH 177/436] ui: use current calibration to center vanishing point (#24955) * compute x and y offsets using calibration * fix default calibration * clamp to max values * only use when valid * not while calibrating * less diff * cleanup zoom --- selfdrive/ui/qt/onroad.cc | 21 +++++----- selfdrive/ui/qt/onroad.h | 2 +- selfdrive/ui/qt/widgets/cameraview.cc | 55 +++++++++++++++++---------- selfdrive/ui/qt/widgets/cameraview.h | 12 +++++- selfdrive/ui/ui.cc | 1 + selfdrive/ui/ui.h | 8 ++-- 6 files changed, 62 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 27594162369d05..ecd6d61471cf8b 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -208,6 +208,12 @@ void NvgWindow::updateState(const UIState &s) { setProperty("engageable", cs.getEngageable() || cs.getEnabled()); setProperty("dmActive", sm["driverMonitoringState"].getDriverMonitoringState().getIsActiveMode()); } + + if (s.scene.calibration_valid) { + CameraViewWidget::updateCalibration(s.scene.view_from_calib); + } else { + CameraViewWidget::updateCalibration(DEFAULT_CALIBRATION); + } } void NvgWindow::drawHud(QPainter &p) { @@ -399,23 +405,20 @@ void NvgWindow::initializeGL() { setBackgroundColor(bg_colors[STATUS_DISENGAGED]); } -void NvgWindow::updateFrameMat(int w, int h) { - CameraViewWidget::updateFrameMat(w, h); - +void NvgWindow::updateFrameMat() { + CameraViewWidget::updateFrameMat(); UIState *s = uiState(); + int w = width(), h = height(); + s->fb_w = w; s->fb_h = h; - auto intrinsic_matrix = s->wide_camera ? ecam_intrinsic_matrix : fcam_intrinsic_matrix; - float zoom = ZOOM / intrinsic_matrix.v[0]; - if (s->wide_camera) { - zoom *= 0.5; - } + // Apply transformation such that video pixel coordinates match video // 1) Put (0, 0) in the middle of the video // 2) Apply same scaling as video // 3) Put (0, 0) in top left corner of video s->car_space_transform.reset(); - s->car_space_transform.translate(w / 2, h / 2 + y_offset) + s->car_space_transform.translate(w / 2 - x_offset, h / 2 - y_offset) .scale(zoom, zoom) .translate(-intrinsic_matrix.v[2], -intrinsic_matrix.v[5]); } diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 4cd88fc9e3f642..dc1e69da2a1d8b 100644 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -70,7 +70,7 @@ class NvgWindow : public CameraViewWidget { void paintGL() override; void initializeGL() override; void showEvent(QShowEvent *event) override; - void updateFrameMat(int w, int h) override; + void updateFrameMat() override; void drawLaneLines(QPainter &painter, const UIState *s); void drawLead(QPainter &painter, const cereal::ModelDataV2::LeadDataV3::Reader &lead_data, const QPointF &vd); void drawHud(QPainter &p); diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index 8666eb1d4e450e..a80672f2c36992 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -6,6 +6,8 @@ #include #endif +#include + #include #include @@ -59,13 +61,6 @@ const char frame_fragment_shader[] = "}\n"; #endif -const mat4 device_transform = {{ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -}}; - mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) { const float driver_view_ratio = 2.0; const float yscale = stream_height * driver_view_ratio / stream_width; @@ -185,35 +180,55 @@ void CameraViewWidget::hideEvent(QHideEvent *event) { } } -void CameraViewWidget::updateFrameMat(int w, int h) { +void CameraViewWidget::updateFrameMat() { + int w = width(), h = height(); + if (zoomed_view) { if (stream_type == VISION_STREAM_DRIVER) { - frame_mat = matmul(device_transform, get_driver_view_transform(w, h, stream_width, stream_height)); + frame_mat = get_driver_view_transform(w, h, stream_width, stream_height); } else { - auto intrinsic_matrix = stream_type == VISION_STREAM_WIDE_ROAD ? ecam_intrinsic_matrix : fcam_intrinsic_matrix; - float zoom = ZOOM / intrinsic_matrix.v[0]; - if (stream_type == VISION_STREAM_WIDE_ROAD) { - zoom *= 0.5; - } + intrinsic_matrix = (stream_type == VISION_STREAM_WIDE_ROAD) ? ecam_intrinsic_matrix : fcam_intrinsic_matrix; + zoom = (stream_type == VISION_STREAM_WIDE_ROAD) ? 2.5 : 1.1; + + // Project point at "infinity" to compute x and y offsets + // to ensure this ends up in the middle of the screen + // TODO: use proper perspective transform? + const vec3 inf = {{1000., 0., 0.}}; + const vec3 Ep = matvecmul3(calibration, inf); + const vec3 Kep = matvecmul3(intrinsic_matrix, Ep); + + float x_offset_ = (Kep.v[0] / Kep.v[2] - intrinsic_matrix.v[2]) * zoom; + float y_offset_ = (Kep.v[1] / Kep.v[2] - intrinsic_matrix.v[5]) * zoom; + + float max_x_offset = intrinsic_matrix.v[2] * zoom - w / 2 - 5; + float max_y_offset = intrinsic_matrix.v[5] * zoom - h / 2 - 5; + + x_offset = std::clamp(x_offset_, -max_x_offset, max_x_offset); + y_offset = std::clamp(y_offset_, -max_y_offset, max_y_offset); + float zx = zoom * 2 * intrinsic_matrix.v[2] / width(); float zy = zoom * 2 * intrinsic_matrix.v[5] / height(); - const mat4 frame_transform = {{ - zx, 0.0, 0.0, 0.0, - 0.0, zy, 0.0, -y_offset / height() * 2, + zx, 0.0, 0.0, -x_offset / width() * 2, + 0.0, zy, 0.0, y_offset / height() * 2, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, }}; - frame_mat = matmul(device_transform, frame_transform); + frame_mat = frame_transform; } } else if (stream_width > 0 && stream_height > 0) { // fit frame to widget size float widget_aspect_ratio = (float)width() / height(); float frame_aspect_ratio = (float)stream_width / stream_height; - frame_mat = matmul(device_transform, get_fit_view_transform(widget_aspect_ratio, frame_aspect_ratio)); + frame_mat = get_fit_view_transform(widget_aspect_ratio, frame_aspect_ratio); } } +void CameraViewWidget::updateCalibration(const mat3 &calib) { + calibration = calib; + updateFrameMat(); +} + void CameraViewWidget::paintGL() { glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); @@ -311,7 +326,7 @@ void CameraViewWidget::vipcConnected(VisionIpcClient *vipc_client) { assert(glGetError() == GL_NO_ERROR); #endif - updateFrameMat(width(), height()); + updateFrameMat(); } void CameraViewWidget::vipcFrameReceived(VisionBuf *buf, uint32_t frame_id) { diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index 42e9043602000f..cc11ec2c277d0e 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -42,11 +42,12 @@ class CameraViewWidget : public QOpenGLWidget, protected QOpenGLFunctions { protected: void paintGL() override; void initializeGL() override; - void resizeGL(int w, int h) override { updateFrameMat(w, h); } + void resizeGL(int w, int h) override { updateFrameMat(); } void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } - virtual void updateFrameMat(int w, int h); + virtual void updateFrameMat(); + void updateCalibration(const mat3 &calib); void vipcThread(); bool zoomed_view; @@ -68,6 +69,13 @@ class CameraViewWidget : public QOpenGLWidget, protected QOpenGLFunctions { std::atomic stream_type; QThread *vipc_thread = nullptr; + // Calibration + float x_offset = 0; + float y_offset = 0; + float zoom = 1.0; + mat3 calibration = DEFAULT_CALIBRATION; + mat3 intrinsic_matrix = fcam_intrinsic_matrix; + std::deque> frames; uint32_t draw_frame_id = 0; diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 4d1e1ab7462593..c8fc645cf2031d 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -140,6 +140,7 @@ static void update_state(UIState *s) { scene.view_from_calib.v[i*3 + j] = view_from_calib(i,j); } } + scene.calibration_valid = sm["liveCalibration"].getLiveCalibration().getCalStatus() == 1; } if (s->worldObjectsVisible()) { if (sm.updated("modelV2")) { diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index cbf3921e4ebefa..7364b81a40833b 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -22,10 +22,7 @@ const int footer_h = 280; const int UI_FREQ = 20; // Hz typedef cereal::CarControl::HUDControl::AudibleAlert AudibleAlert; -// TODO: this is also hardcoded in common/transformations/camera.py -// TODO: choose based on frame input size -const float y_offset = 150.0; -const float ZOOM = 2912.8; +const mat3 DEFAULT_CALIBRATION = {{ 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0 }}; struct Alert { QString text1; @@ -93,7 +90,8 @@ typedef struct { } line_vertices_data; typedef struct UIScene { - mat3 view_from_calib; + bool calibration_valid = false; + mat3 view_from_calib = DEFAULT_CALIBRATION; cereal::PandaState::PandaType pandaType; // modelV2 From b95e68778271cccf558452686e022ae57f25a701 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 27 Jun 2022 21:31:54 +0200 Subject: [PATCH 178/436] split locationd and liblocationd tests (#24977) * laikad: use cython version of gnss kf * fix import error * test liblocationd separate * Revert "laikad: use cython version of gnss kf" This reverts commit bdd769b9554e7e45e976dabd6595403943e864bb. --- .github/workflows/selfdrive_tests.yaml | 3 +- .../locationd/test/_test_locationd_lib.py | 106 ++++++++++++++++++ selfdrive/locationd/test/test_locationd.py | 94 ---------------- 3 files changed, 108 insertions(+), 95 deletions(-) create mode 100755 selfdrive/locationd/test/_test_locationd_lib.py diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index fedeaa734bb65d..52dd012baaade8 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -299,6 +299,7 @@ jobs: $UNIT_TEST selfdrive/loggerd && \ $UNIT_TEST selfdrive/car && \ $UNIT_TEST selfdrive/locationd && \ + selfdrive/locationd/test/_test_locationd_lib.py && \ $UNIT_TEST selfdrive/athena && \ $UNIT_TEST selfdrive/thermald && \ $UNIT_TEST selfdrive/hardware/tici && \ @@ -364,7 +365,7 @@ jobs: CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v2 - + model_replay_onnx: name: model replay onnx runs-on: ubuntu-20.04 diff --git a/selfdrive/locationd/test/_test_locationd_lib.py b/selfdrive/locationd/test/_test_locationd_lib.py new file mode 100755 index 00000000000000..8a0ed3ef05923e --- /dev/null +++ b/selfdrive/locationd/test/_test_locationd_lib.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""This test can't be run together with other locationd tests. +cffi.dlopen breaks the list of registered filters.""" +import os +import random +import unittest + +from cffi import FFI + +import cereal.messaging as messaging +from cereal import log + +SENSOR_DECIMATION = 1 +VISION_DECIMATION = 1 + +LIBLOCATIOND_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../liblocationd.so')) + + +class TestLocationdLib(unittest.TestCase): + def setUp(self): + header = '''typedef ...* Localizer_t; +Localizer_t localizer_init(); +void localizer_get_message_bytes(Localizer_t localizer, bool inputsOK, bool sensorsOK, bool gpsOK, bool msgValid, char *buff, size_t buff_size); +void localizer_handle_msg_bytes(Localizer_t localizer, const char *data, size_t size);''' + + self.ffi = FFI() + self.ffi.cdef(header) + self.lib = self.ffi.dlopen(LIBLOCATIOND_PATH) + + self.localizer = self.lib.localizer_init() + + self.buff_size = 2048 + self.msg_buff = self.ffi.new(f'char[{self.buff_size}]') + + def localizer_handle_msg(self, msg_builder): + bytstr = msg_builder.to_bytes() + self.lib.localizer_handle_msg_bytes(self.localizer, self.ffi.from_buffer(bytstr), len(bytstr)) + + def localizer_get_msg(self, t=0, inputsOK=True, sensorsOK=True, gpsOK=True, msgValid=True): + self.lib.localizer_get_message_bytes(self.localizer, inputsOK, sensorsOK, gpsOK, msgValid, self.ffi.addressof(self.msg_buff, 0), self.buff_size) + return log.Event.from_bytes(self.ffi.buffer(self.msg_buff), nesting_limit=self.buff_size // 8) + + def test_liblocalizer(self): + msg = messaging.new_message('liveCalibration') + msg.liveCalibration.validBlocks = random.randint(1, 10) + msg.liveCalibration.rpyCalib = [random.random() / 10 for _ in range(3)] + + self.localizer_handle_msg(msg) + liveloc = self.localizer_get_msg() + self.assertTrue(liveloc is not None) + + @unittest.skip("temporarily disabled due to false positives") + def test_device_fell(self): + msg = messaging.new_message('sensorEvents', 1) + msg.sensorEvents[0].sensor = 1 + msg.sensorEvents[0].timestamp = msg.logMonoTime + msg.sensorEvents[0].type = 1 + msg.sensorEvents[0].init('acceleration') + msg.sensorEvents[0].acceleration.v = [10.0, 0.0, 0.0] # zero with gravity + self.localizer_handle_msg(msg) + + ret = self.localizer_get_msg() + self.assertTrue(ret.liveLocationKalman.deviceStable) + + msg = messaging.new_message('sensorEvents', 1) + msg.sensorEvents[0].sensor = 1 + msg.sensorEvents[0].timestamp = msg.logMonoTime + msg.sensorEvents[0].type = 1 + msg.sensorEvents[0].init('acceleration') + msg.sensorEvents[0].acceleration.v = [50.1, 0.0, 0.0] # more than 40 m/s**2 + self.localizer_handle_msg(msg) + + ret = self.localizer_get_msg() + self.assertFalse(ret.liveLocationKalman.deviceStable) + + def test_posenet_spike(self): + for _ in range(SENSOR_DECIMATION): + msg = messaging.new_message('carState') + msg.carState.vEgo = 6.0 # more than 5 m/s + self.localizer_handle_msg(msg) + + ret = self.localizer_get_msg() + self.assertTrue(ret.liveLocationKalman.posenetOK) + + for _ in range(20 * VISION_DECIMATION): # size of hist_old + msg = messaging.new_message('cameraOdometry') + msg.cameraOdometry.rot = [0.0, 0.0, 0.0] + msg.cameraOdometry.rotStd = [0.1, 0.1, 0.1] + msg.cameraOdometry.trans = [0.0, 0.0, 0.0] + msg.cameraOdometry.transStd = [2.0, 0.1, 0.1] + self.localizer_handle_msg(msg) + + for _ in range(20 * VISION_DECIMATION): # size of hist_new + msg = messaging.new_message('cameraOdometry') + msg.cameraOdometry.rot = [0.0, 0.0, 0.0] + msg.cameraOdometry.rotStd = [1.0, 1.0, 1.0] + msg.cameraOdometry.trans = [0.0, 0.0, 0.0] + msg.cameraOdometry.transStd = [10.1, 0.1, 0.1] # more than 4 times larger + self.localizer_handle_msg(msg) + + ret = self.localizer_get_msg() + self.assertFalse(ret.liveLocationKalman.posenetOK) + +if __name__ == "__main__": + unittest.main() + diff --git a/selfdrive/locationd/test/test_locationd.py b/selfdrive/locationd/test/test_locationd.py index 7f5d7521005502..29036b8387cc9f 100755 --- a/selfdrive/locationd/test/test_locationd.py +++ b/selfdrive/locationd/test/test_locationd.py @@ -1,110 +1,16 @@ #!/usr/bin/env python3 -import os import json import random import unittest import time import capnp -from cffi import FFI -from cereal import log import cereal.messaging as messaging from cereal.services import service_list from common.params import Params from selfdrive.manager.process_config import managed_processes -SENSOR_DECIMATION = 1 -VISION_DECIMATION = 1 - -LIBLOCATIOND_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../liblocationd.so')) - - -class TestLocationdLib(unittest.TestCase): - def setUp(self): - header = '''typedef ...* Localizer_t; -Localizer_t localizer_init(); -void localizer_get_message_bytes(Localizer_t localizer, bool inputsOK, bool sensorsOK, bool gpsOK, bool msgValid, char *buff, size_t buff_size); -void localizer_handle_msg_bytes(Localizer_t localizer, const char *data, size_t size);''' - - self.ffi = FFI() - self.ffi.cdef(header) - self.lib = self.ffi.dlopen(LIBLOCATIOND_PATH) - - self.localizer = self.lib.localizer_init() - - self.buff_size = 2048 - self.msg_buff = self.ffi.new(f'char[{self.buff_size}]') - - def localizer_handle_msg(self, msg_builder): - bytstr = msg_builder.to_bytes() - self.lib.localizer_handle_msg_bytes(self.localizer, self.ffi.from_buffer(bytstr), len(bytstr)) - - def localizer_get_msg(self, t=0, inputsOK=True, sensorsOK=True, gpsOK=True, msgValid=True): - self.lib.localizer_get_message_bytes(self.localizer, inputsOK, sensorsOK, gpsOK, msgValid, self.ffi.addressof(self.msg_buff, 0), self.buff_size) - return log.Event.from_bytes(self.ffi.buffer(self.msg_buff), nesting_limit=self.buff_size // 8) - - def test_liblocalizer(self): - msg = messaging.new_message('liveCalibration') - msg.liveCalibration.validBlocks = random.randint(1, 10) - msg.liveCalibration.rpyCalib = [random.random() / 10 for _ in range(3)] - - self.localizer_handle_msg(msg) - liveloc = self.localizer_get_msg() - self.assertTrue(liveloc is not None) - - @unittest.skip("temporarily disabled due to false positives") - def test_device_fell(self): - msg = messaging.new_message('sensorEvents', 1) - msg.sensorEvents[0].sensor = 1 - msg.sensorEvents[0].timestamp = msg.logMonoTime - msg.sensorEvents[0].type = 1 - msg.sensorEvents[0].init('acceleration') - msg.sensorEvents[0].acceleration.v = [10.0, 0.0, 0.0] # zero with gravity - self.localizer_handle_msg(msg) - - ret = self.localizer_get_msg() - self.assertTrue(ret.liveLocationKalman.deviceStable) - - msg = messaging.new_message('sensorEvents', 1) - msg.sensorEvents[0].sensor = 1 - msg.sensorEvents[0].timestamp = msg.logMonoTime - msg.sensorEvents[0].type = 1 - msg.sensorEvents[0].init('acceleration') - msg.sensorEvents[0].acceleration.v = [50.1, 0.0, 0.0] # more than 40 m/s**2 - self.localizer_handle_msg(msg) - - ret = self.localizer_get_msg() - self.assertFalse(ret.liveLocationKalman.deviceStable) - - def test_posenet_spike(self): - for _ in range(SENSOR_DECIMATION): - msg = messaging.new_message('carState') - msg.carState.vEgo = 6.0 # more than 5 m/s - self.localizer_handle_msg(msg) - - ret = self.localizer_get_msg() - self.assertTrue(ret.liveLocationKalman.posenetOK) - - for _ in range(20 * VISION_DECIMATION): # size of hist_old - msg = messaging.new_message('cameraOdometry') - msg.cameraOdometry.rot = [0.0, 0.0, 0.0] - msg.cameraOdometry.rotStd = [0.1, 0.1, 0.1] - msg.cameraOdometry.trans = [0.0, 0.0, 0.0] - msg.cameraOdometry.transStd = [2.0, 0.1, 0.1] - self.localizer_handle_msg(msg) - - for _ in range(20 * VISION_DECIMATION): # size of hist_new - msg = messaging.new_message('cameraOdometry') - msg.cameraOdometry.rot = [0.0, 0.0, 0.0] - msg.cameraOdometry.rotStd = [1.0, 1.0, 1.0] - msg.cameraOdometry.trans = [0.0, 0.0, 0.0] - msg.cameraOdometry.transStd = [10.1, 0.1, 0.1] # more than 4 times larger - self.localizer_handle_msg(msg) - - ret = self.localizer_get_msg() - self.assertFalse(ret.liveLocationKalman.posenetOK) - class TestLocationdProc(unittest.TestCase): MAX_WAITS = 1000 From aaca31b73eb109a053e94b2153eb3068a0976c95 Mon Sep 17 00:00:00 2001 From: "Shafiqur R. Khan" <37314609+shfqrkhn@users.noreply.github.com> Date: Mon, 27 Jun 2022 14:10:57 -0600 Subject: [PATCH 179/436] 2022 RAV4 XLE engine FW (#24973) Update values.py Added ecu.engine address for 2022 RAV4 XLE (ICE) bought in Edmonton, Canada --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 32f66b6fa0cbe5..76f84302ce0da4 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1296,6 +1296,7 @@ class ToyotaCarInfo(CarInfo): b'\x028965B0R01500\x00\x00\x00\x008965B0R02500\x00\x00\x00\x00', ], (Ecu.engine, 0x700, None): [ + b'\x01896634AA0000\x00\x00\x00\x00', b'\x01896634AA1000\x00\x00\x00\x00', b'\x01896634A88000\x00\x00\x00\x00', ], From b3226d505b9260ec8565acfda7138601a76039a6 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Mon, 27 Jun 2022 15:25:47 -0700 Subject: [PATCH 180/436] Torque control: higher low speed gains and better steering angle deadzone logic (#24980) * Try no friction and no deadzone * Learn fromd ata * update refs --- selfdrive/controls/lib/latcontrol_torque.py | 6 +++--- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 014c3480b80254..46caa41a90fc86 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -56,15 +56,15 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 - low_speed_factor = interp(CS.vEgo, [0, 15], [500, 0]) + low_speed_factor = interp(CS.vEgo, [0, 10, 20], [500, 500, 200]) setpoint = desired_lateral_accel + low_speed_factor * desired_curvature measurement = actual_lateral_accel + low_speed_factor * actual_curvature - error = apply_deadzone(setpoint - measurement, lateral_accel_deadzone) + error = setpoint - measurement pid_log.error = error ff = desired_lateral_accel - params.roll * ACCELERATION_DUE_TO_GRAVITY # convert friction into lateral accel units for feedforward - friction_compensation = interp(error, [-FRICTION_THRESHOLD, FRICTION_THRESHOLD], [-self.friction, self.friction]) + friction_compensation = interp(apply_deadzone(error, lateral_accel_deadzone), [-FRICTION_THRESHOLD, FRICTION_THRESHOLD], [-self.friction, self.friction]) ff += friction_compensation / self.kf freeze_integrator = CS.steeringRateLimited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(error, diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 292c41b9789abb..1402284b967fea 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -b55de9fdb2416e63ab554c95233a78219d8d3db9 \ No newline at end of file +a16ca1082cd493f6cea5252eaaba9f8c6574334a From 3a2bcc092c74925edb161959cdcc66073a9cd8f3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 28 Jun 2022 01:08:11 -0700 Subject: [PATCH 181/436] onroad UI: fix onroad double tap (#24982) * The default implementation calls mousePressEvent(). * no sidebar when entering body * wrong one * you can't double tap with body anyway (fixes inconsistencies with prime vs not prime) * hide sidebar --- selfdrive/ui/qt/home.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index e522d6160d62cd..aec9bffbc0c3ee 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -85,6 +85,7 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) { } void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { + HomeWindow::mousePressEvent(e); const SubMaster &sm = *(uiState()->sm); if (sm["carParams"].getCarParams().getNotCar()) { if (onroad->isVisible()) { @@ -92,6 +93,7 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) { } else if (body->isVisible()) { slayout->setCurrentWidget(onroad); } + showSidebar(false); } } From d693285b02a0f005ce1d29793c8672ab2fe24a24 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 28 Jun 2022 01:09:52 -0700 Subject: [PATCH 182/436] Power Monitoring test: fix exceptions (#24981) * fix missing POWER_DRAW * think should be 0 --- selfdrive/thermald/tests/test_power_monitoring.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/thermald/tests/test_power_monitoring.py b/selfdrive/thermald/tests/test_power_monitoring.py index c0524add69ade1..efe607155aeeb3 100755 --- a/selfdrive/thermald/tests/test_power_monitoring.py +++ b/selfdrive/thermald/tests/test_power_monitoring.py @@ -119,7 +119,8 @@ def test_car_battery_integration_lower_limit(self, hw_type): @parameterized.expand(ALL_PANDA_TYPES) def test_max_time_offroad(self, hw_type): MOCKED_MAX_OFFROAD_TIME = 3600 - with pm_patch("MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True), pm_patch("HARDWARE.get_current_power_draw", None): + POWER_DRAW = 0 # To stop shutting down for other reasons + with pm_patch("MAX_TIME_OFFROAD_S", MOCKED_MAX_OFFROAD_TIME, constant=True), pm_patch("HARDWARE.get_current_power_draw", POWER_DRAW): pm = PowerMonitoring() pm.car_battery_capacity_uWh = CAR_BATTERY_CAPACITY_uWh start_time = ssb From fdc22554b8c2a2e4b9e0df91beddf469d735b399 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 28 Jun 2022 11:32:51 +0200 Subject: [PATCH 183/436] Update rednose: use EKF_sym_pyx (#24978) * Update rednose * Update rednose * cleanup --- rednose_repo | 2 +- selfdrive/locationd/models/car_kf.py | 4 ++-- selfdrive/locationd/models/gnss_kf.py | 16 +++++++++++----- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/rednose_repo b/rednose_repo index 7663289f1e6886..3a6f937ad1bb23 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 7663289f1e68860f53dc34337ef080dde69a2586 +Subproject commit 3a6f937ad1bb234dca7b0b2052faabdd58f3e085 diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index fe7d2e650bd267..3faf4f8d4e2563 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -15,7 +15,7 @@ import sympy as sp from rednose.helpers.ekf_sym import gen_code else: - from rednose.helpers.ekf_sym_pyx import EKF_sym # pylint: disable=no-name-in-module, import-error + from rednose.helpers.ekf_sym_pyx import EKF_sym_pyx # pylint: disable=no-name-in-module, import-error i = 0 @@ -171,7 +171,7 @@ def __init__(self, generated_dir, steer_ratio=15, stiffness_factor=1, angle_offs if P_initial is not None: self.P_initial = P_initial # init filter - self.filter = EKF_sym(generated_dir, self.name, self.Q, self.initial_x, self.P_initial, dim_state, dim_state_err, global_vars=self.global_vars, logger=cloudlog) + self.filter = EKF_sym_pyx(generated_dir, self.name, self.Q, self.initial_x, self.P_initial, dim_state, dim_state_err, global_vars=self.global_vars, logger=cloudlog) if __name__ == "__main__": diff --git a/selfdrive/locationd/models/gnss_kf.py b/selfdrive/locationd/models/gnss_kf.py index ce0ff84dc2705a..5c9cb4b3d13577 100755 --- a/selfdrive/locationd/models/gnss_kf.py +++ b/selfdrive/locationd/models/gnss_kf.py @@ -3,12 +3,17 @@ from typing import List import numpy as np -import sympy as sp -from rednose.helpers.ekf_sym import EKF_sym, gen_code from selfdrive.locationd.models.constants import ObservationKind from selfdrive.locationd.models.gnss_helpers import parse_pr, parse_prr +if __name__ == '__main__': # Generating sympy + import sympy as sp + from rednose.helpers.ekf_sym import gen_code +else: + from rednose.helpers.ekf_sym_pyx import EKF_sym_pyx # pylint: disable=no-name-in-module,import-error + from rednose.helpers.ekf_sym import EKF_sym # pylint: disable=no-name-in-module,import-error + class States(): ECEF_POS = slice(0, 3) # x, y and z in ECEF in meters @@ -115,12 +120,13 @@ def generate_code(generated_dir): gen_code(generated_dir, name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state, maha_test_kinds=maha_test_kinds) - def __init__(self, generated_dir): + def __init__(self, generated_dir, cython=False): self.dim_state = self.x_initial.shape[0] # init filter - self.filter = EKF_sym(generated_dir, self.name, self.Q, self.x_initial, self.P_initial, self.dim_state, - self.dim_state, maha_test_kinds=self.maha_test_kinds) + filter_cls = EKF_sym_pyx if cython else EKF_sym + self.filter = filter_cls(generated_dir, self.name, self.Q, self.x_initial, self.P_initial, self.dim_state, + self.dim_state, maha_test_kinds=self.maha_test_kinds) self.init_state(GNSSKalman.x_initial, covs=GNSSKalman.P_initial) @property From 4cf63f4758c937a169962f325517b442f6c9492f Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 11:59:15 +0200 Subject: [PATCH 184/436] bump rednose --- rednose_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rednose_repo b/rednose_repo index 3a6f937ad1bb23..a79a87300a6c50 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 3a6f937ad1bb234dca7b0b2052faabdd58f3e085 +Subproject commit a79a87300a6c5093d99f281307c65b1e99295b20 From 3823f55476b56fd3db0afe686b1d2f3d508817bc Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 13:43:15 +0200 Subject: [PATCH 185/436] add laikad to process replay (#24889) * merge * Fix closing process executor after fetching orbits * cleanup * Add ref commit and revert test_processes hack * Fix * Fix ref * Fix test * Temp * Temp * Trying * Trying * Cleanup and change test * add ref commit * remove print * fix test getting stuck * cleanup fetch_orbits Co-authored-by: Gijs Koning --- selfdrive/locationd/laikad.py | 50 ++++++++++++------- selfdrive/locationd/test/test_laikad.py | 27 ++++++++-- selfdrive/test/process_replay/README.md | 3 +- .../test/process_replay/process_replay.py | 18 +++++++ selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/profiling/profiler.py | 3 ++ 6 files changed, 81 insertions(+), 22 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index ba665b75309847..60028b637a61fa 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import os import time from collections import defaultdict from concurrent.futures import Future, ProcessPoolExecutor @@ -32,8 +33,10 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe save_ephemeris=False, last_known_position=None): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) self.gnss_kf = GNSSKalman(GENERATED_DIR) - self.orbit_fetch_executor = ProcessPoolExecutor() + + self.orbit_fetch_executor: Optional[ProcessPoolExecutor] = None self.orbit_fetch_future: Optional[Future] = None + self.last_fetch_orbits_t = None self.last_cached_t = None self.save_ephemeris = save_ephemeris @@ -44,9 +47,13 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.last_pos_fix_t = None def load_cache(self): + if not self.save_ephemeris: + return + cache = Params().get(EPHEMERIS_CACHE) if not cache: return + try: cache = json.loads(cache, object_hook=deserialize_hook) self.astro_dog.add_orbits(cache['orbits']) @@ -71,7 +78,7 @@ def get_est_pos(self, t, processed_measurements): self.last_pos_residual = pos_fix_residual self.last_pos_fix_t = t return self.last_pos_fix - + def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): if ublox_msg.which == 'measurementReport': t = ublox_mono_time * 1e-9 @@ -152,17 +159,22 @@ def init_gnss_localizer(self, est_pos): def fetch_orbits(self, t: GPSTime, block): if t not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or t - self.last_fetch_orbits_t > SECS_IN_HR): astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types - if self.orbit_fetch_future is None: + + ret = None + + if block: + ret = get_orbit_data(t, *astro_dog_vars) + elif self.orbit_fetch_future is None: + self.orbit_fetch_executor = ProcessPoolExecutor(max_workers=1) self.orbit_fetch_future = self.orbit_fetch_executor.submit(get_orbit_data, t, *astro_dog_vars) - if block: - self.orbit_fetch_future.result() - if self.orbit_fetch_future.done(): - ret = self.orbit_fetch_future.result() + elif self.orbit_fetch_future.done(): self.last_fetch_orbits_t = t - if ret: - self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret - self.cache_ephemeris(t=t) - self.orbit_fetch_future = None + ret = self.orbit_fetch_future.result() + self.orbit_fetch_executor = self.orbit_fetch_future = None + + if ret is not None: + self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret + self.cache_ephemeris(t=t) def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): @@ -174,7 +186,7 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): astro_dog.get_orbit_data(t, only_predictions=True) data = (astro_dog.orbits, astro_dog.orbit_fetched_times) except RuntimeError as e: - cloudlog.info(f"No orbit data found. {e}") + cloudlog.warning(f"No orbit data found. {e}") cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") return data @@ -255,17 +267,21 @@ class EphemerisSourceType(IntEnum): glonassIacUltraRapid = 2 -def main(): - sm = messaging.SubMaster(['ubloxGnss']) - pm = messaging.PubMaster(['gnssMeasurements']) +def main(sm=None, pm=None): + if sm is None: + sm = messaging.SubMaster(['ubloxGnss']) + if pm is None: + pm = messaging.PubMaster(['gnssMeasurements']) + + replay = "REPLAY" in os.environ # todo get last_known_position - laikad = Laikad(save_ephemeris=True) + laikad = Laikad(save_ephemeris=not replay) while True: sm.update() if sm.updated['ubloxGnss']: ublox_msg = sm['ubloxGnss'] - msg = laikad.process_ublox_msg(ublox_msg, sm.logMonoTime['ubloxGnss']) + msg = laikad.process_ublox_msg(ublox_msg, sm.logMonoTime['ubloxGnss'], block=replay) if msg is not None: pm.send('gnssMeasurements', msg) diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 3a72303b0c0533..c353da9624e7e2 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -7,6 +7,7 @@ from unittest.mock import Mock, patch from common.params import Params +from laika.constants import SECS_IN_DAY from laika.ephemeris import EphemerisType, GPSEphemeris from laika.gps_time import GPSTime from laika.helpers import ConstellationId, TimeRangeHolder @@ -62,6 +63,26 @@ def setUpClass(cls): def setUp(self): Params().delete(EPHEMERIS_CACHE) + def test_fetch_orbits_non_blocking(self): + gpstime = GPSTime.from_datetime(datetime(2021, month=3, day=1)) + laikad = Laikad() + laikad.fetch_orbits(gpstime, block=False) + laikad.orbit_fetch_future.result(5) + # Get results and save orbits to laikad: + laikad.fetch_orbits(gpstime, block=False) + + ephem = laikad.astro_dog.orbits['G01'][0] + self.assertIsNotNone(ephem) + + laikad.fetch_orbits(gpstime+2*SECS_IN_DAY, block=False) + laikad.orbit_fetch_future.result(5) + # Get results and save orbits to laikad: + laikad.fetch_orbits(gpstime + 2 * SECS_IN_DAY, block=False) + + ephem2 = laikad.astro_dog.orbits['G01'][0] + self.assertIsNotNone(ephem) + self.assertNotEqual(ephem, ephem2) + def test_ephemeris_source_in_msg(self): data_mock = defaultdict(str) data_mock['sv_id'] = 1 @@ -155,7 +176,7 @@ def wait_for_cache(): while Params().get(EPHEMERIS_CACHE) is None: time.sleep(0.1) max_time -= 0.1 - if max_time == 0: + if max_time < 0: self.fail("Cache has not been written after 2 seconds") # Test cache with no ephemeris @@ -170,7 +191,7 @@ def wait_for_cache(): wait_for_cache() # Check both nav and orbits separate - laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.NAV) + laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.NAV, save_ephemeris=True) # Verify orbits and nav are loaded from cache self.dict_has_values(laikad.astro_dog.orbits) self.dict_has_values(laikad.astro_dog.nav) @@ -185,7 +206,7 @@ def wait_for_cache(): mock_method.assert_not_called() # Verify cache is working for only orbits by running a segment - laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT) + laikad = Laikad(auto_update=False, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT, save_ephemeris=True) msg = verify_messages(self.logs, laikad, return_one_success=True) self.assertIsNotNone(msg) # Verify orbit data is not downloaded diff --git a/selfdrive/test/process_replay/README.md b/selfdrive/test/process_replay/README.md index 9fa4ca644a08bb..531ddb3a023c46 100644 --- a/selfdrive/test/process_replay/README.md +++ b/selfdrive/test/process_replay/README.md @@ -5,7 +5,7 @@ Process replay is a regression test designed to identify any changes in the outp If the test fails, make sure that you didn't unintentionally change anything. If there are intentional changes, the reference logs will be updated. Use `test_processes.py` to run the test locally. -Use `FILEREADER_CACHE='1' test_processes.py` to cache log files. +Use `FILEREADER_CACHE='1' test_processes.py` to cache log files. Currently the following processes are tested: @@ -15,6 +15,7 @@ Currently the following processes are tested: * calibrationd * dmonitoringd * locationd +* laikad * paramsd * ubloxd diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 228f07c4c246f2..def61a10a19951 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -236,6 +236,13 @@ def ublox_rcv_callback(msg): return [] +def laika_rcv_callback(msg, CP, cfg, fsm): + if msg.ubloxGnss.which() == "measurementReport": + return ["gnssMeasurements"], True + else: + return [], False + + CONFIGS = [ ProcessConfig( proc_name="controlsd", @@ -338,6 +345,17 @@ def ublox_rcv_callback(msg): tolerance=None, fake_pubsubmaster=False, ), + ProcessConfig( + proc_name="laikad", + pub_sub={ + "ubloxGnss": ["gnssMeasurements"], + }, + ignore=["logMonoTime"], + init_callback=get_car_params, + should_recv_callback=laika_rcv_callback, + tolerance=NUMPY_TOLERANCE, + fake_pubsubmaster=True, + ), ] diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 1402284b967fea..3e02830bf4e3f9 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a16ca1082cd493f6cea5252eaaba9f8c6574334a +2ee969b34585f8055bb3eabab2dcc4061cc4bef9 \ No newline at end of file diff --git a/selfdrive/test/profiling/profiler.py b/selfdrive/test/profiling/profiler.py index 5f73176fabeaf2..91226fc577620b 100755 --- a/selfdrive/test/profiling/profiler.py +++ b/selfdrive/test/profiling/profiler.py @@ -53,6 +53,7 @@ def profile(proc, func, car='toyota'): msgs = list(LogReader(rlog_url)) * int(os.getenv("LOOP", "1")) os.environ['FINGERPRINT'] = fingerprint + os.environ['REPLAY'] = "1" def run(sm, pm, can_sock): try: @@ -81,12 +82,14 @@ def run(sm, pm, can_sock): from selfdrive.controls.radard import radard_thread from selfdrive.locationd.paramsd import main as paramsd_thread from selfdrive.controls.plannerd import main as plannerd_thread + from selfdrive.locationd.laikad import main as laikad_thread procs = { 'radard': radard_thread, 'controlsd': controlsd_thread, 'paramsd': paramsd_thread, 'plannerd': plannerd_thread, + 'laikad': laikad_thread, } proc = sys.argv[1] From 005bc44df692acb3389924d362f5b761987c7c9d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 14:29:41 +0200 Subject: [PATCH 186/436] laikad: use cython filter (#24983) use cython filter --- selfdrive/locationd/laikad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 60028b637a61fa..5098b25d38f1b7 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -32,7 +32,7 @@ class Laikad: def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), save_ephemeris=False, last_known_position=None): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) - self.gnss_kf = GNSSKalman(GENERATED_DIR) + self.gnss_kf = GNSSKalman(GENERATED_DIR, cython=True) self.orbit_fetch_executor: Optional[ProcessPoolExecutor] = None self.orbit_fetch_future: Optional[Future] = None @@ -145,7 +145,7 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement self.gnss_kf.predict(t) def kf_valid(self, t: float) -> List[bool]: - filter_time = self.gnss_kf.filter.filter_time + filter_time = self.gnss_kf.filter.get_filter_time() return [filter_time is not None, filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS]))] From 338df150d5112d4c6772edb797cb45370fdc3599 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 15:48:57 +0200 Subject: [PATCH 187/436] ui: disable sync with model until more stable (#24984) --- selfdrive/ui/qt/widgets/cameraview.cc | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index a80672f2c36992..0bc90b530719c1 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -235,10 +235,21 @@ void CameraViewWidget::paintGL() { if (frames.empty()) return; - int frame_idx; - for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) { - if (frames[frame_idx].first == draw_frame_id) break; + int frame_idx = frames.size() - 1; + + // Always draw latest frame until sync logic is more stable + // for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) { + // if (frames[frame_idx].first == draw_frame_id) break; + // } + + // Log duplicate/dropped frames + static int prev_id = 0; + if (frames[frame_idx].first == prev_id) { + qInfo() << "Drawing same frame twice" << frames[frame_idx].first; + } else if (frames[frame_idx].first != prev_id + 1) { + qInfo() << "Skipped frame" << frames[frame_idx].first; } + prev_id = frames[frame_idx].first; glViewport(0, 0, width(), height()); glBindVertexArray(frame_vao); From fd5b3d76036b78864111790931a3abcb1d11ee0f Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Tue, 28 Jun 2022 22:12:42 +0800 Subject: [PATCH 188/436] move replay from selfdrive/ui/replay to tools/replay (#24971) * mv to tools/replay * change folder * add .gitignore * fix build doc * disable warning * enable warning after build * build qt/util.cc qt/api.cc to library * cleanup --- .github/workflows/selfdrive_tests.yaml | 2 +- SConstruct | 2 ++ docs/c_docs.rst | 2 +- release/files_common | 4 ++-- selfdrive/loggerd/tests/test_logger.cc | 2 +- selfdrive/ui/SConscript | 20 +++++-------------- tools/CTF.md | 2 +- tools/replay/.gitignore | 5 +++++ tools/replay/README.md | 14 ++++++------- tools/replay/SConscript | 19 ++++++++++++++++++ {selfdrive/ui => tools}/replay/camera.cc | 4 ++-- {selfdrive/ui => tools}/replay/camera.h | 4 ++-- {selfdrive/ui => tools}/replay/consoleui.cc | 2 +- {selfdrive/ui => tools}/replay/consoleui.h | 2 +- {selfdrive/ui => tools}/replay/filereader.cc | 4 ++-- {selfdrive/ui => tools}/replay/filereader.h | 0 {selfdrive/ui => tools}/replay/framereader.cc | 4 ++-- {selfdrive/ui => tools}/replay/framereader.h | 2 +- {selfdrive/ui => tools}/replay/logreader.cc | 4 ++-- {selfdrive/ui => tools}/replay/logreader.h | 2 +- {selfdrive/ui => tools}/replay/main.cc | 4 ++-- {selfdrive/ui => tools}/replay/replay.cc | 4 ++-- {selfdrive/ui => tools}/replay/replay.h | 4 ++-- {selfdrive/ui => tools}/replay/route.cc | 6 +++--- {selfdrive/ui => tools}/replay/route.h | 6 +++--- .../ui => tools}/replay/tests/test_replay.cc | 4 ++-- .../ui => tools}/replay/tests/test_runner.cc | 0 {selfdrive/ui => tools}/replay/util.cc | 2 +- {selfdrive/ui => tools}/replay/util.h | 0 29 files changed, 73 insertions(+), 57 deletions(-) create mode 100644 tools/replay/.gitignore create mode 100644 tools/replay/SConscript rename {selfdrive/ui => tools}/replay/camera.cc (96%) rename {selfdrive/ui => tools}/replay/camera.h (92%) rename {selfdrive/ui => tools}/replay/consoleui.cc (99%) rename {selfdrive/ui => tools}/replay/consoleui.h (96%) rename {selfdrive/ui => tools}/replay/filereader.cc (94%) rename {selfdrive/ui => tools}/replay/filereader.h (100%) rename {selfdrive/ui => tools}/replay/framereader.cc (98%) rename {selfdrive/ui => tools}/replay/framereader.h (97%) rename {selfdrive/ui => tools}/replay/logreader.cc (97%) rename {selfdrive/ui => tools}/replay/logreader.h (97%) rename {selfdrive/ui => tools}/replay/main.cc (96%) rename {selfdrive/ui => tools}/replay/replay.cc (99%) rename {selfdrive/ui => tools}/replay/replay.h (97%) rename {selfdrive/ui => tools}/replay/route.cc (97%) rename {selfdrive/ui => tools}/replay/route.h (92%) rename {selfdrive/ui => tools}/replay/tests/test_replay.cc (98%) rename {selfdrive/ui => tools}/replay/tests/test_runner.cc (100%) rename {selfdrive/ui => tools}/replay/util.cc (99%) rename {selfdrive/ui => tools}/replay/util.h (100%) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 52dd012baaade8..a40aa31341dc28 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -310,7 +310,7 @@ jobs: ./selfdrive/boardd/tests/test_boardd_usbprotocol && \ ./selfdrive/loggerd/tests/test_logger &&\ ./system/proclogd/tests/test_proclog && \ - ./selfdrive/ui/replay/tests/test_replay && \ + ./tools/replay/tests/test_replay && \ ./system/camerad/test/ae_gray_test && \ coverage xml" - name: "Upload coverage to Codecov" diff --git a/SConstruct b/SConstruct index b243a4dc45d609..425ce3d761aef3 100644 --- a/SConstruct +++ b/SConstruct @@ -411,6 +411,8 @@ SConscript(['selfdrive/locationd/SConscript']) SConscript(['selfdrive/sensord/SConscript']) SConscript(['selfdrive/ui/SConscript']) +SConscript(['tools/replay/SConscript']) + if GetOption('test'): SConscript('panda/tests/safety/SConscript') diff --git a/docs/c_docs.rst b/docs/c_docs.rst index 4e1e8a247a100d..5638b40bf04428 100644 --- a/docs/c_docs.rst +++ b/docs/c_docs.rst @@ -54,7 +54,7 @@ soundd replay """""" .. autodoxygenindex:: - :project: selfdrive_ui_replay + :project: tools_replay qt "" diff --git a/release/files_common b/release/files_common index 0ace73623f2ecd..7e8dbd37a63609 100644 --- a/release/files_common +++ b/release/files_common @@ -296,8 +296,8 @@ selfdrive/ui/qt/offroad/*.qml selfdrive/ui/qt/widgets/*.cc selfdrive/ui/qt/widgets/*.h -selfdrive/ui/replay/*.cc -selfdrive/ui/replay/*.h +tools/replay/*.cc +tools/replay/*.h selfdrive/ui/qt/maps/*.cc selfdrive/ui/qt/maps/*.h diff --git a/selfdrive/loggerd/tests/test_logger.cc b/selfdrive/loggerd/tests/test_logger.cc index 11a50fa2e75425..c8f6620924c9d6 100644 --- a/selfdrive/loggerd/tests/test_logger.cc +++ b/selfdrive/loggerd/tests/test_logger.cc @@ -9,7 +9,7 @@ #include "cereal/messaging/messaging.h" #include "common/util.h" #include "selfdrive/loggerd/logger.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/util.h" typedef cereal::Sentinel::SentinelType SentinelType; diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 28fcc5f56f95e5..970f715f54b087 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -18,10 +18,11 @@ if arch == "Darwin": del base_libs[base_libs.index('OpenCL')] qt_env['FRAMEWORKS'] += ['OpenCL'] -widgets_src = ["ui.cc", "qt/util.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", +qt_util = qt_env.Library("qt_util", ["#selfdrive/ui/qt/api.cc", "#selfdrive/ui/qt/util.cc"], LIBS=base_libs) +widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", "qt/widgets/ssh_keys.cc", "qt/widgets/toggle.cc", "qt/widgets/controls.cc", "qt/widgets/offroad_alerts.cc", "qt/widgets/prime.cc", "qt/widgets/keyboard.cc", - "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", "qt/api.cc", + "qt/widgets/scrollview.cc", "qt/widgets/cameraview.cc", "#third_party/qrcode/QrCode.cc", "qt/request_repeater.cc", "qt/qt_window.cc", "qt/offroad/networking.cc", "qt/offroad/wifiManager.cc"] qt_env['CPPDEFINES'] = [] @@ -31,7 +32,7 @@ if maps: qt_env['CPPDEFINES'] += ["ENABLE_MAPS"] widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs) -qt_libs = [widgets] + base_libs +qt_libs = [widgets, qt_util] + base_libs # build assets assets = "#selfdrive/assets/assets.cc" @@ -107,17 +108,6 @@ if GetOption('extras'): # keep installers small assert f[0].get_size() < 300*1e3 - -# build headless replay +# build watch3 if arch in ['x86_64', 'Darwin'] or GetOption('extras'): - qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] - - replay_lib_src = ["replay/replay.cc", "replay/consoleui.cc", "replay/camera.cc", "replay/filereader.cc", "replay/logreader.cc", "replay/framereader.cc", "replay/route.cc", "replay/util.cc"] - - replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=base_libs) - replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs - qt_env.Program("replay/replay", ["replay/main.cc"], LIBS=replay_libs) qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'json11', 'zmq', 'visionipc', 'messaging']) - - if GetOption('test'): - qt_env.Program('replay/tests/test_replay', ['replay/tests/test_runner.cc', 'replay/tests/test_replay.cc'], LIBS=[replay_libs]) diff --git a/tools/CTF.md b/tools/CTF.md index 396e7575a481fb..a14a41d55da106 100644 --- a/tools/CTF.md +++ b/tools/CTF.md @@ -12,7 +12,7 @@ Welcome to the first part of the comma CTF! getting started ```bash # start the route reply -cd selfdrive/ui/replay +cd tools/replay ./replay '0c7f0c7f0c7f0c7f|2021-10-13--13-00-00' --dcam --ecam # start the UI in another terminal diff --git a/tools/replay/.gitignore b/tools/replay/.gitignore new file mode 100644 index 00000000000000..83f0e99a8b9842 --- /dev/null +++ b/tools/replay/.gitignore @@ -0,0 +1,5 @@ +moc_* +*.moc + +replay +tests/test_replay diff --git a/tools/replay/README.md b/tools/replay/README.md index 759a448d731a39..b705fb60db5703 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -9,12 +9,12 @@ python lib/auth.py # Start a replay -selfdrive/ui/replay/replay +tools/replay/replay # Example: -# selfdrive/ui/replay/replay '4cf7a6ad03080c90|2021-09-29--13-46-36' +# tools/replay/replay '4cf7a6ad03080c90|2021-09-29--13-46-36' # or use --demo to replay the default demo route: -# selfdrive/ui/replay/replay --demo +# tools/replay/replay --demo # watch the replay with the normal openpilot UI cd selfdrive/ui && ./ui @@ -25,8 +25,8 @@ python replay/ui.py ## usage ``` bash -$ selfdrive/ui/replay/replay -h -Usage: selfdrive/ui/replay/replay [options] route +$ tools/replay/replay -h +Usage: tools/replay/replay [options] route Mock openpilot components by publishing logged messages. Options: @@ -51,7 +51,7 @@ simply replay a route using the `--dcam` and `--ecam` flags: ```bash # start a replay -cd selfdrive/ui/replay && ./replay --demo --dcam --ecam +cd tools/replay && ./replay --demo --dcam --ecam # then start watch3 cd selfdrive/ui && ./watch3 @@ -70,5 +70,5 @@ In order to replay specific route: MOCK=1 selfdrive/boardd/tests/boardd_old.py # In another terminal: -selfdrive/ui/replay/replay +tools/replay/replay ``` diff --git a/tools/replay/SConscript b/tools/replay/SConscript new file mode 100644 index 00000000000000..4a85f46d612b03 --- /dev/null +++ b/tools/replay/SConscript @@ -0,0 +1,19 @@ +import os +Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', + 'cereal', 'transformations') + +base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', + 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] + +qt_libs = ['qt_util'] + base_libs +if arch in ['x86_64', 'Darwin'] or GetOption('extras'): + qt_env['CXXFLAGS'] += ["-Wno-deprecated-declarations"] + + replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"] + + replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs) + replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs + qt_env.Program("replay", ["main.cc"], LIBS=replay_libs) + + if GetOption('test'): + qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs]) diff --git a/selfdrive/ui/replay/camera.cc b/tools/replay/camera.cc similarity index 96% rename from selfdrive/ui/replay/camera.cc rename to tools/replay/camera.cc index 2e8b68a41507cc..87afe63a2af64e 100644 --- a/selfdrive/ui/replay/camera.cc +++ b/tools/replay/camera.cc @@ -1,5 +1,5 @@ -#include "selfdrive/ui/replay/camera.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/camera.h" +#include "tools/replay/util.h" #include diff --git a/selfdrive/ui/replay/camera.h b/tools/replay/camera.h similarity index 92% rename from selfdrive/ui/replay/camera.h rename to tools/replay/camera.h index 7f078511cf65aa..66d33142fb8ada 100644 --- a/selfdrive/ui/replay/camera.h +++ b/tools/replay/camera.h @@ -3,8 +3,8 @@ #include #include "cereal/visionipc/visionipc_server.h" #include "common/queue.h" -#include "selfdrive/ui/replay/framereader.h" -#include "selfdrive/ui/replay/logreader.h" +#include "tools/replay/framereader.h" +#include "tools/replay/logreader.h" class CameraServer { public: diff --git a/selfdrive/ui/replay/consoleui.cc b/tools/replay/consoleui.cc similarity index 99% rename from selfdrive/ui/replay/consoleui.cc rename to tools/replay/consoleui.cc index 05eb09c9edb814..e4a3146a69c02c 100644 --- a/selfdrive/ui/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -1,4 +1,4 @@ -#include "selfdrive/ui/replay/consoleui.h" +#include "tools/replay/consoleui.h" #include #include diff --git a/selfdrive/ui/replay/consoleui.h b/tools/replay/consoleui.h similarity index 96% rename from selfdrive/ui/replay/consoleui.h rename to tools/replay/consoleui.h index bce1146d4615b9..20e07524ddad8c 100644 --- a/selfdrive/ui/replay/consoleui.h +++ b/tools/replay/consoleui.h @@ -7,7 +7,7 @@ #include #include -#include "selfdrive/ui/replay/replay.h" +#include "tools/replay/replay.h" #include class ConsoleUI : public QObject { diff --git a/selfdrive/ui/replay/filereader.cc b/tools/replay/filereader.cc similarity index 94% rename from selfdrive/ui/replay/filereader.cc rename to tools/replay/filereader.cc index 5f862bec3517cb..88879067c9f96a 100644 --- a/selfdrive/ui/replay/filereader.cc +++ b/tools/replay/filereader.cc @@ -1,9 +1,9 @@ -#include "selfdrive/ui/replay/filereader.h" +#include "tools/replay/filereader.h" #include #include "common/util.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/util.h" std::string cacheFilePath(const std::string &url) { static std::string cache_path = [] { diff --git a/selfdrive/ui/replay/filereader.h b/tools/replay/filereader.h similarity index 100% rename from selfdrive/ui/replay/filereader.h rename to tools/replay/filereader.h diff --git a/selfdrive/ui/replay/framereader.cc b/tools/replay/framereader.cc similarity index 98% rename from selfdrive/ui/replay/framereader.cc rename to tools/replay/framereader.cc index 38d98bddc56209..bfa85a0625cbe2 100644 --- a/selfdrive/ui/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -1,5 +1,5 @@ -#include "selfdrive/ui/replay/framereader.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/framereader.h" +#include "tools/replay/util.h" #include #include "libyuv.h" diff --git a/selfdrive/ui/replay/framereader.h b/tools/replay/framereader.h similarity index 97% rename from selfdrive/ui/replay/framereader.h rename to tools/replay/framereader.h index 443636e27ddcc8..10bf34a24e8ac6 100644 --- a/selfdrive/ui/replay/framereader.h +++ b/tools/replay/framereader.h @@ -4,7 +4,7 @@ #include #include -#include "selfdrive/ui/replay/filereader.h" +#include "tools/replay/filereader.h" extern "C" { #include diff --git a/selfdrive/ui/replay/logreader.cc b/tools/replay/logreader.cc similarity index 97% rename from selfdrive/ui/replay/logreader.cc rename to tools/replay/logreader.cc index 579fe50644dd3f..f27224ac53a09b 100644 --- a/selfdrive/ui/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -1,7 +1,7 @@ -#include "selfdrive/ui/replay/logreader.h" +#include "tools/replay/logreader.h" #include -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/util.h" Event::Event(const kj::ArrayPtr &amsg, bool frame) : reader(amsg), frame(frame) { words = kj::ArrayPtr(amsg.begin(), reader.getEnd()); diff --git a/selfdrive/ui/replay/logreader.h b/tools/replay/logreader.h similarity index 97% rename from selfdrive/ui/replay/logreader.h rename to tools/replay/logreader.h index b4a38a57210721..fb63bf39136e3b 100644 --- a/selfdrive/ui/replay/logreader.h +++ b/tools/replay/logreader.h @@ -7,7 +7,7 @@ #include "cereal/gen/cpp/log.capnp.h" #include "system/camerad/cameras/camera_common.h" -#include "selfdrive/ui/replay/filereader.h" +#include "tools/replay/filereader.h" const CameraType ALL_CAMERAS[] = {RoadCam, DriverCam, WideRoadCam}; const int MAX_CAMERAS = std::size(ALL_CAMERAS); diff --git a/selfdrive/ui/replay/main.cc b/tools/replay/main.cc similarity index 96% rename from selfdrive/ui/replay/main.cc rename to tools/replay/main.cc index e09587023cf8c0..d3d6894877ee21 100644 --- a/selfdrive/ui/replay/main.cc +++ b/tools/replay/main.cc @@ -1,8 +1,8 @@ #include #include -#include "selfdrive/ui/replay/consoleui.h" -#include "selfdrive/ui/replay/replay.h" +#include "tools/replay/consoleui.h" +#include "tools/replay/replay.h" const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; diff --git a/selfdrive/ui/replay/replay.cc b/tools/replay/replay.cc similarity index 99% rename from selfdrive/ui/replay/replay.cc rename to tools/replay/replay.cc index cf7520b361b236..c886a7e1862ae3 100644 --- a/selfdrive/ui/replay/replay.cc +++ b/tools/replay/replay.cc @@ -1,4 +1,4 @@ -#include "selfdrive/ui/replay/replay.h" +#include "tools/replay/replay.h" #include #include @@ -8,7 +8,7 @@ #include "common/params.h" #include "common/timing.h" #include "system/hardware/hw.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/util.h" Replay::Replay(QString route, QStringList allow, QStringList block, SubMaster *sm_, uint32_t flags, QString data_dir, QObject *parent) : sm(sm_), flags_(flags), QObject(parent) { diff --git a/selfdrive/ui/replay/replay.h b/tools/replay/replay.h similarity index 97% rename from selfdrive/ui/replay/replay.h rename to tools/replay/replay.h index c89a835f64160a..13269d3ec9a269 100644 --- a/selfdrive/ui/replay/replay.h +++ b/tools/replay/replay.h @@ -4,8 +4,8 @@ #include -#include "selfdrive/ui/replay/camera.h" -#include "selfdrive/ui/replay/route.h" +#include "tools/replay/camera.h" +#include "tools/replay/route.h" // one segment uses about 100M of memory constexpr int FORWARD_SEGS = 5; diff --git a/selfdrive/ui/replay/route.cc b/tools/replay/route.cc similarity index 97% rename from selfdrive/ui/replay/route.cc rename to tools/replay/route.cc index 3d595141cdbba3..5b47090229749d 100644 --- a/selfdrive/ui/replay/route.cc +++ b/tools/replay/route.cc @@ -1,4 +1,4 @@ -#include "selfdrive/ui/replay/route.h" +#include "tools/replay/route.h" #include #include @@ -11,8 +11,8 @@ #include "system/hardware/hw.h" #include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/replay/replay.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/replay.h" +#include "tools/replay/util.h" Route::Route(const QString &route, const QString &data_dir) : data_dir_(data_dir) { route_ = parseRoute(route); diff --git a/selfdrive/ui/replay/route.h b/tools/replay/route.h similarity index 92% rename from selfdrive/ui/replay/route.h rename to tools/replay/route.h index ac8fba75ca3288..6ca9c3b883af12 100644 --- a/selfdrive/ui/replay/route.h +++ b/tools/replay/route.h @@ -2,9 +2,9 @@ #include -#include "selfdrive/ui/replay/framereader.h" -#include "selfdrive/ui/replay/logreader.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/framereader.h" +#include "tools/replay/logreader.h" +#include "tools/replay/util.h" struct RouteIdentifier { QString dongle_id; diff --git a/selfdrive/ui/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc similarity index 98% rename from selfdrive/ui/replay/tests/test_replay.cc rename to tools/replay/tests/test_replay.cc index bf984f5f3d3c4a..bd5dee013c8af6 100644 --- a/selfdrive/ui/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -6,8 +6,8 @@ #include "catch2/catch.hpp" #include "common/util.h" -#include "selfdrive/ui/replay/replay.h" -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/replay.h" +#include "tools/replay/util.h" const QString DEMO_ROUTE = "4cf7a6ad03080c90|2021-09-29--13-46-36"; const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; diff --git a/selfdrive/ui/replay/tests/test_runner.cc b/tools/replay/tests/test_runner.cc similarity index 100% rename from selfdrive/ui/replay/tests/test_runner.cc rename to tools/replay/tests/test_runner.cc diff --git a/selfdrive/ui/replay/util.cc b/tools/replay/util.cc similarity index 99% rename from selfdrive/ui/replay/util.cc rename to tools/replay/util.cc index 099219ccdea98f..a6ebbec6158114 100644 --- a/selfdrive/ui/replay/util.cc +++ b/tools/replay/util.cc @@ -1,4 +1,4 @@ -#include "selfdrive/ui/replay/util.h" +#include "tools/replay/util.h" #include #include diff --git a/selfdrive/ui/replay/util.h b/tools/replay/util.h similarity index 100% rename from selfdrive/ui/replay/util.h rename to tools/replay/util.h From 42d51a1b959fbd00cd43b7fb36cd5dce00f22c55 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 17:20:37 +0200 Subject: [PATCH 189/436] Properly pass KF dependencies to rednose (#24985) * Fix rednose dependencies * bump rednose * bump rednose --- SConstruct | 21 +++++++++++++-------- rednose_repo | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/SConstruct b/SConstruct index 425ce3d761aef3..1209894ba46d31 100644 --- a/SConstruct +++ b/SConstruct @@ -356,22 +356,27 @@ Export('cereal', 'messaging', 'visionipc') # Build rednose library and ekf models +rednose_deps = [ + "#selfdrive/locationd/models/constants.py", + "#selfdrive/locationd/models/gnss_helpers.py", +] + rednose_config = { 'generated_folder': '#selfdrive/locationd/models/generated', 'to_build': { - 'gnss': ('#selfdrive/locationd/models/gnss_kf.py', True, []), - 'live': ('#selfdrive/locationd/models/live_kf.py', True, ['live_kf_constants.h']), - 'car': ('#selfdrive/locationd/models/car_kf.py', True, []), + 'gnss': ('#selfdrive/locationd/models/gnss_kf.py', True, [], rednose_deps), + 'live': ('#selfdrive/locationd/models/live_kf.py', True, ['live_kf_constants.h'], rednose_deps), + 'car': ('#selfdrive/locationd/models/car_kf.py', True, [], rednose_deps), }, } if arch != "larch64": rednose_config['to_build'].update({ - 'loc_4': ('#selfdrive/locationd/models/loc_kf.py', True, []), - 'pos_computer_4': ('#rednose/helpers/lst_sq_computer.py', False, []), - 'pos_computer_5': ('#rednose/helpers/lst_sq_computer.py', False, []), - 'feature_handler_5': ('#rednose/helpers/feature_handler.py', False, []), - 'lane': ('#xx/pipeline/lib/ekf/lane_kf.py', True, []), + 'loc_4': ('#selfdrive/locationd/models/loc_kf.py', True, [], rednose_deps), + 'pos_computer_4': ('#rednose/helpers/lst_sq_computer.py', False, [], []), + 'pos_computer_5': ('#rednose/helpers/lst_sq_computer.py', False, [], []), + 'feature_handler_5': ('#rednose/helpers/feature_handler.py', False, [], []), + 'lane': ('#xx/pipeline/lib/ekf/lane_kf.py', True, [], rednose_deps), }) Export('rednose_config') diff --git a/rednose_repo b/rednose_repo index a79a87300a6c50..225dbacbaac312 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit a79a87300a6c5093d99f281307c65b1e99295b20 +Subproject commit 225dbacbaac312f85eaaee0b97a3acc31f9c6b47 From 712445c5314d0634e1f6a78a95901dd14ac6bc57 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 28 Jun 2022 17:39:22 +0200 Subject: [PATCH 190/436] build.py: remove retry logic (#24986) --- selfdrive/manager/build.py | 101 ++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 58 deletions(-) diff --git a/selfdrive/manager/build.py b/selfdrive/manager/build.py index 10b4c0a4b8766b..12f894061a8061 100755 --- a/selfdrive/manager/build.py +++ b/selfdrive/manager/build.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 import os import subprocess -import sys -import time import textwrap from pathlib import Path @@ -28,62 +26,49 @@ def build(spinner: Spinner, dirty: bool = False) -> None: nproc = os.cpu_count() j_flag = "" if nproc is None else f"-j{nproc - 1}" - for retry in [True, False]: - scons: subprocess.Popen = subprocess.Popen(["scons", j_flag, "--cache-populate"], cwd=BASEDIR, env=env, stderr=subprocess.PIPE) - assert scons.stderr is not None - - compile_output = [] - - # Read progress from stderr and update spinner - while scons.poll() is None: - try: - line = scons.stderr.readline() - if line is None: - continue - line = line.rstrip() - - prefix = b'progress: ' - if line.startswith(prefix): - i = int(line[len(prefix):]) - spinner.update_progress(MAX_BUILD_PROGRESS * min(1., i / TOTAL_SCONS_NODES), 100.) - elif len(line): - compile_output.append(line) - print(line.decode('utf8', 'replace')) - except Exception: - pass - - if scons.returncode != 0: - # Read remaining output - r = scons.stderr.read().split(b'\n') - compile_output += r - - if retry and (not dirty): - if not os.getenv("CI"): - print("scons build failed, cleaning in") - for i in range(3, -1, -1): - print("....%d" % i) - time.sleep(1) - subprocess.check_call(["scons", "-c"], cwd=BASEDIR, env=env) - else: - print("scons build failed after retry") - sys.exit(1) - else: - # Build failed log errors - errors = [line.decode('utf8', 'replace') for line in compile_output - if any(err in line for err in [b'error: ', b'not found, needed by target'])] - error_s = "\n".join(errors) - add_file_handler(cloudlog) - cloudlog.error("scons build failed\n" + error_s) - - # Show TextWindow - spinner.close() - if not os.getenv("CI"): - error_s = "\n \n".join("\n".join(textwrap.wrap(e, 65)) for e in errors) - with TextWindow("openpilot failed to build\n \n" + error_s) as t: - t.wait_for_exit() - exit(1) - else: - break + scons: subprocess.Popen = subprocess.Popen(["scons", j_flag, "--cache-populate"], cwd=BASEDIR, env=env, stderr=subprocess.PIPE) + assert scons.stderr is not None + + compile_output = [] + + # Read progress from stderr and update spinner + while scons.poll() is None: + try: + line = scons.stderr.readline() + if line is None: + continue + line = line.rstrip() + + prefix = b'progress: ' + if line.startswith(prefix): + i = int(line[len(prefix):]) + spinner.update_progress(MAX_BUILD_PROGRESS * min(1., i / TOTAL_SCONS_NODES), 100.) + elif len(line): + compile_output.append(line) + print(line.decode('utf8', 'replace')) + except Exception: + pass + + if scons.returncode != 0: + # Read remaining output + r = scons.stderr.read().split(b'\n') + compile_output += r + + # Build failed log errors + errors = [line.decode('utf8', 'replace') for line in compile_output + if any(err in line for err in [b'error: ', b'not found, needed by target'])] + error_s = "\n".join(errors) + add_file_handler(cloudlog) + cloudlog.error("scons build failed\n" + error_s) + + # Show TextWindow + spinner.close() + if not os.getenv("CI"): + error_s = "\n \n".join("\n".join(textwrap.wrap(e, 65)) for e in errors) + with TextWindow("openpilot failed to build\n \n" + error_s) as t: + t.wait_for_exit() + exit(1) + # enforce max cache size cache_files = [f for f in CACHE_DIR.rglob('*') if f.is_file()] From 75f5282e832a844aafccac3bfa53ffb029c53a2b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 28 Jun 2022 14:19:17 -0700 Subject: [PATCH 191/436] Chrysler: fix steering angle signals (#24926) * Chrysler_Update * only steering * revert other changes for now only steering * bump * Update ref_commit * bump opendbc * update refs Co-authored-by: Jonathan --- opendbc | 2 +- selfdrive/car/chrysler/carstate.py | 4 ++-- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/opendbc b/opendbc index 82be71072c52fc..47b79c4d5ab5ad 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 82be71072c52fc78cf0e1eabc396af26c18ddc11 +Subproject commit 47b79c4d5ab5adc0bdd9d228c3d9594da0355c49 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index f71a3002632679..202526df05c597 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -47,8 +47,6 @@ def update(self, cp, cp_cam): ret.leftBlinker = cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 1 ret.rightBlinker = cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 2 - ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] - ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) ret.cruiseState.available = cp.vl["DAS_3"]["ACC_AVAILABLE"] == 1 # ACC is white @@ -58,6 +56,8 @@ def update(self, cp, cp_cam): ret.cruiseState.nonAdaptive = cp.vl["DASHBOARD"]["CRUISE_STATE"] in (1, 2) ret.accFaulted = cp.vl["DAS_3"]["ACC_FAULTED"] != 0 + ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] + ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 3e02830bf4e3f9..946ddce19e5451 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2ee969b34585f8055bb3eabab2dcc4061cc4bef9 \ No newline at end of file +a0b5ce7b2e0b9c073e51ac8908402d53e1d99722 \ No newline at end of file From 4db3ca4cf2eca777c1cff7cb5a3010569f6cc7d5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 28 Jun 2022 20:38:08 -0700 Subject: [PATCH 192/436] Toyota: Add missing 2021 RAV4 TSS2 esp FW (#24989) Add missing Canadian TRD 2021 RAV4 --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 76f84302ce0da4..c7a26257f39ab9 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1262,6 +1262,7 @@ class ToyotaCarInfo(CarInfo): b'\x01F152642711\x00\x00\x00\x00\x00\x00', b'\x01F152642750\x00\x00\x00\x00\x00\x00', b'\x01F152642751\x00\x00\x00\x00\x00\x00', + b'\x01F15260R292\x00\x00\x00\x00\x00\x00', ], (Ecu.eps, 0x7a1, None): [ b'8965B42170\x00\x00\x00\x00\x00\x00', From e0cd3bf5fc56229c2a0b835d0ef43a0bf683bfa5 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Wed, 29 Jun 2022 17:27:37 +0800 Subject: [PATCH 193/436] framereader.cc: remove nv12toyuv_buffer (#24991) remove nv12toyuv_buffer --- tools/replay/framereader.cc | 9 +++------ tools/replay/framereader.h | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index bfa85a0625cbe2..ecde9ad5d2b14e 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -117,8 +117,6 @@ bool FrameReader::load(const std::byte *data, size_t size, bool no_hw_decoder, s if (has_hw_decoder && !no_hw_decoder) { if (!initHardwareDecoder(HW_DEVICE_TYPE)) { rWarning("No device with hardware decoder found. fallback to CPU decoding."); - } else { - nv12toyuv_buffer.resize(getYUVSize()); } } @@ -227,17 +225,16 @@ AVFrame *FrameReader::decodeFrame(AVPacket *pkt) { } bool FrameReader::copyBuffers(AVFrame *f, uint8_t *yuv) { + assert(f != nullptr && yuv != nullptr); + uint8_t *y = yuv; + uint8_t *uv = y + width * height; if (hw_pix_fmt == HW_PIX_FMT) { - uint8_t *y = yuv ? yuv : nv12toyuv_buffer.data(); - uint8_t *uv = y + width * height; for (int i = 0; i < height/2; i++) { memcpy(y + (i*2 + 0)*width, f->data[0] + (i*2 + 0)*f->linesize[0], width); memcpy(y + (i*2 + 1)*width, f->data[0] + (i*2 + 1)*f->linesize[0], width); memcpy(uv + i*width, f->data[1] + i*f->linesize[1], width); } } else { - uint8_t *y = yuv ? yuv : nv12toyuv_buffer.data(); - uint8_t *uv = y + width * height; libyuv::I420ToNV12(f->data[0], f->linesize[0], f->data[1], f->linesize[1], f->data[2], f->linesize[2], diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index 10bf34a24e8ac6..e50b61d7f4f877 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -46,7 +46,6 @@ class FrameReader { AVPixelFormat hw_pix_fmt = AV_PIX_FMT_NONE; AVBufferRef *hw_device_ctx = nullptr; - std::vector nv12toyuv_buffer; int prev_idx = -1; inline static std::atomic has_hw_decoder = true; }; From 67cf20977d8fc250ac9d43261554712e1b74c689 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 29 Jun 2022 11:36:03 +0200 Subject: [PATCH 194/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 3fef4f7861e041..df08568318da97 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 3fef4f7861e04193fdb11dbd5b0eaf6d2b102ee1 +Subproject commit df08568318da97ed6f87747caee0a5b2c30086c4 From bef5350fa3cc99a79af75a108f50d6a76ce07988 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 29 Jun 2022 12:19:37 +0200 Subject: [PATCH 195/436] don't log LaikadEphemeris in initData --- common/params.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/params.cc b/common/params.cc index b8526bc231ac22..f93c87cd98286a 100644 --- a/common/params.cc +++ b/common/params.cc @@ -128,7 +128,7 @@ std::unordered_map keys = { {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, {"IsUpdateAvailable", CLEAR_ON_MANAGER_START}, {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, - {"LaikadEphemeris", PERSISTENT}, + {"LaikadEphemeris", PERSISTENT | DONT_LOG}, {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, {"LastGPSPosition", PERSISTENT}, {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, From 879a7c3201a037c27c92ad6b92143114f52f29d3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 29 Jun 2022 14:47:46 -0700 Subject: [PATCH 196/436] UI: wrap all text for translation (#24961) * rough multiple language demo * more wrappings * stash * add some bad translations * updates * map from french to spanish still has same problem of needing to call setText on everything * add files * restart UI * use return code * relative path * more translations * don't loop restart * Toggle and prime translations * try on device * try QComboBox with readable style * stash * not yet scrollable * stash * dynamic translations (doesn't work for dynamic widget strings yet) * clean up multiple option selector * store languages in json * try transparent * Try transparent popup * see how this looks * tweaks * clean up * clean up * clean up 2 and missing tr * wrap more strings * missing updater * fixes * add basic test to ensure all strings wrapped * try in CI * clean up * test name * fix test * always install qt dev tools * fix deps * fast test * add section so it prints multiple errors * debug * debug get rid of those * make any difference? * comment * oh... * run with offscreen platform * try out section * clean up * fix missing wrappings (it works!) * move down * space * clear relevant params, set TICI=1 --- .github/workflows/selfdrive_tests.yaml | 2 + selfdrive/ui/SConscript | 3 + selfdrive/ui/installer/installer.cc | 8 +- selfdrive/ui/qt/home.cc | 4 +- selfdrive/ui/qt/maps/map_settings.cc | 16 +-- selfdrive/ui/qt/offroad/driverview.cc | 2 +- selfdrive/ui/qt/offroad/networking.cc | 32 ++--- selfdrive/ui/qt/offroad/onboarding.cc | 14 +-- selfdrive/ui/qt/offroad/settings.cc | 110 +++++++++--------- selfdrive/ui/qt/onroad.cc | 14 +-- selfdrive/ui/qt/setup/reset.cc | 18 +-- selfdrive/ui/qt/setup/setup.cc | 42 +++---- selfdrive/ui/qt/setup/updater.cc | 16 +-- selfdrive/ui/qt/sidebar.cc | 16 +-- selfdrive/ui/qt/text.cc | 4 +- selfdrive/ui/qt/util.cc | 8 +- selfdrive/ui/qt/widgets/drive_stats.cc | 8 +- selfdrive/ui/qt/widgets/drive_stats.h | 2 +- selfdrive/ui/qt/widgets/input.cc | 10 +- selfdrive/ui/qt/widgets/offroad_alerts.cc | 6 +- selfdrive/ui/qt/widgets/prime.cc | 28 ++--- selfdrive/ui/qt/widgets/ssh_keys.cc | 18 +-- selfdrive/ui/qt/widgets/ssh_keys.h | 2 +- selfdrive/ui/tests/.gitignore | 1 + .../ui/tests/create_test_translations.sh | 18 +++ selfdrive/ui/tests/test_runner.cc | 19 ++- selfdrive/ui/tests/test_translations.cc | 51 ++++++++ tools/ubuntu_setup.sh | 1 + 28 files changed, 282 insertions(+), 191 deletions(-) create mode 100755 selfdrive/ui/tests/create_test_translations.sh create mode 100644 selfdrive/ui/tests/test_translations.cc diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index a40aa31341dc28..f5edfe59299e75 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -305,6 +305,8 @@ jobs: $UNIT_TEST selfdrive/hardware/tici && \ $UNIT_TEST selfdrive/modeld && \ $UNIT_TEST tools/lib/tests && \ + ./selfdrive/ui/tests/create_test_translations.sh && \ + QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ ./common/tests/test_util && \ ./common/tests/test_swaglog && \ ./selfdrive/boardd/tests/test_boardd_usbprotocol && \ diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 970f715f54b087..0835c163727f8a 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -58,6 +58,9 @@ qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc"] qt_env.Program("_ui", qt_src + [asset_obj], LIBS=qt_libs) +if GetOption('test'): + qt_src.remove("main.cc") # replaced by test_runner + qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) # setup and factory resetter diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index 3588026ba68724..fe31a2dcb17697 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -53,7 +53,7 @@ Installer::Installer(QWidget *parent) : QWidget(parent) { layout->setContentsMargins(150, 290, 150, 150); layout->setSpacing(0); - QLabel *title = new QLabel("Installing..."); + QLabel *title = new QLabel(tr("Installing...")); title->setStyleSheet("font-size: 90px; font-weight: 600;"); layout->addWidget(title, 0, Qt::AlignTop); @@ -141,9 +141,9 @@ void Installer::cachedFetch(const QString &cache) { void Installer::readProgress() { const QVector> stages = { // prefix, weight in percentage - {"Receiving objects: ", 91}, - {"Resolving deltas: ", 2}, - {"Updating files: ", 7}, + {tr("Receiving objects: "), 91}, + {tr("Resolving deltas: "), 2}, + {tr("Updating files: "), 7}, }; auto line = QString(proc.readAllStandardError()); diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index aec9bffbc0c3ee..0edeb252b7e5cc 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -111,7 +111,7 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { date = new QLabel(); header_layout->addWidget(date, 1, Qt::AlignHCenter | Qt::AlignLeft); - update_notif = new QPushButton("UPDATE"); + update_notif = new QPushButton(tr("UPDATE")); update_notif->setVisible(false); update_notif->setStyleSheet("background-color: #364DEF;"); QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); @@ -202,6 +202,6 @@ void OffroadHome::refresh() { update_notif->setVisible(updateAvailable); alert_notif->setVisible(alerts); if (alerts) { - alert_notif->setText(QString::number(alerts) + (alerts > 1 ? " ALERTS" : " ALERT")); + alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT"))); } } diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc index 674c7fa7cb71f9..eaa8b1f703dfb7 100644 --- a/selfdrive/ui/qt/maps/map_settings.cc +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -59,11 +59,11 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { current_widget = new QWidget(this); QVBoxLayout *current_layout = new QVBoxLayout(current_widget); - QLabel *title = new QLabel("Current Destination"); + QLabel *title = new QLabel(tr("Current Destination")); title->setStyleSheet("font-size: 55px"); current_layout->addWidget(title); - current_route = new ButtonControl("", "CLEAR"); + current_route = new ButtonControl("", tr("CLEAR")); current_route->setStyleSheet("padding-left: 40px;"); current_layout->addWidget(current_route); QObject::connect(current_route, &ButtonControl::clicked, [=]() { @@ -78,7 +78,7 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { main_layout->addWidget(current_widget); // Recents - QLabel *recents_title = new QLabel("Recent Destinations"); + QLabel *recents_title = new QLabel(tr("Recent Destinations")); recents_title->setStyleSheet("font-size: 55px"); main_layout->addWidget(recents_title); main_layout->addSpacing(20); @@ -92,7 +92,7 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { QWidget * no_prime_widget = new QWidget; { QVBoxLayout *no_prime_layout = new QVBoxLayout(no_prime_widget); - QLabel *signup_header = new QLabel("Try the Navigation Beta"); + QLabel *signup_header = new QLabel(tr("Try the Navigation Beta")); signup_header->setStyleSheet(R"(font-size: 75px; color: white; font-weight:600;)"); signup_header->setAlignment(Qt::AlignCenter); @@ -104,7 +104,7 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { screenshot->setPixmap(pm.scaledToWidth(1080, Qt::SmoothTransformation)); no_prime_layout->addWidget(screenshot, 0, Qt::AlignHCenter); - QLabel *signup = new QLabel("Get turn-by-turn directions displayed and more with a comma \nprime subscription. Sign up now: https://connect.comma.ai"); + QLabel *signup = new QLabel(tr("Get turn-by-turn directions displayed and more with a comma \nprime subscription. Sign up now: https://connect.comma.ai")); signup->setStyleSheet(R"(font-size: 45px; color: white; font-weight:300;)"); signup->setAlignment(Qt::AlignCenter); @@ -161,12 +161,12 @@ void MapPanel::showEvent(QShowEvent *event) { void MapPanel::clear() { home_button->setIcon(QPixmap("../assets/navigation/home_inactive.png")); home_address->setStyleSheet(R"(font-size: 50px; color: grey;)"); - home_address->setText("No home\nlocation set"); + home_address->setText(tr("No home\nlocation set")); home_button->disconnect(); work_button->setIcon(QPixmap("../assets/navigation/work_inactive.png")); work_address->setStyleSheet(R"(font-size: 50px; color: grey;)"); - work_address->setText("No work\nlocation set"); + work_address->setText(tr("No work\nlocation set")); work_button->disconnect(); clearLayout(recent_layout); @@ -279,7 +279,7 @@ void MapPanel::parseResponse(const QString &response, bool success) { } if (!has_recents) { - QLabel *no_recents = new QLabel("no recent destinations"); + QLabel *no_recents = new QLabel(tr("no recent destinations")); no_recents->setStyleSheet(R"(font-size: 50px; color: #9c9c9c)"); recent_layout->addWidget(no_recents); } diff --git a/selfdrive/ui/qt/offroad/driverview.cc b/selfdrive/ui/qt/offroad/driverview.cc index dc9f292df19ca4..5bf70584a1fb73 100644 --- a/selfdrive/ui/qt/offroad/driverview.cc +++ b/selfdrive/ui/qt/offroad/driverview.cc @@ -53,7 +53,7 @@ void DriverViewScene::paintEvent(QPaintEvent* event) { p.setPen(Qt::white); p.setRenderHint(QPainter::TextAntialiasing); configFont(p, "Inter", 100, "Bold"); - p.drawText(geometry(), Qt::AlignCenter, "camera starting"); + p.drawText(geometry(), Qt::AlignCenter, tr("camera starting")); return; } diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 418ccd31793270..536ca495cad291 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -27,7 +27,7 @@ Networking::Networking(QWidget* parent, bool show_advanced) : QFrame(parent) { QVBoxLayout* vlayout = new QVBoxLayout(wifiScreen); vlayout->setContentsMargins(20, 20, 20, 20); if (show_advanced) { - QPushButton* advancedSettings = new QPushButton("Advanced"); + QPushButton* advancedSettings = new QPushButton(tr("Advanced")); advancedSettings->setObjectName("advanced_btn"); advancedSettings->setStyleSheet("margin-right: 30px;"); advancedSettings->setFixedSize(400, 100); @@ -84,7 +84,7 @@ void Networking::connectToNetwork(const Network &n) { } else if (n.security_type == SecurityType::OPEN) { wifi->connect(n); } else if (n.security_type == SecurityType::WPA) { - QString pass = InputDialog::getText("Enter password", this, "for \"" + n.ssid + "\"", true, 8); + QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"") + n.ssid + "\"", true, 8); if (!pass.isEmpty()) { wifi->connect(n, pass); } @@ -94,7 +94,7 @@ void Networking::connectToNetwork(const Network &n) { void Networking::wrongPassword(const QString &ssid) { if (wifi->seenNetworks.contains(ssid)) { const Network &n = wifi->seenNetworks.value(ssid); - QString pass = InputDialog::getText("Wrong password", this, "for \"" + n.ssid +"\"", true, 8); + QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"") + n.ssid +"\"", true, 8); if (!pass.isEmpty()) { wifi->connect(n, pass); } @@ -118,7 +118,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid main_layout->setSpacing(20); // Back button - QPushButton* back = new QPushButton("Back"); + QPushButton* back = new QPushButton(tr("Back")); back->setObjectName("back_btn"); back->setFixedSize(400, 100); connect(back, &QPushButton::clicked, [=]() { emit backPress(); }); @@ -126,14 +126,14 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid ListWidget *list = new ListWidget(this); // Enable tethering layout - tetheringToggle = new ToggleControl("Enable Tethering", "", "", wifi->isTetheringEnabled()); + tetheringToggle = new ToggleControl(tr("Enable Tethering"), "", "", wifi->isTetheringEnabled()); list->addItem(tetheringToggle); QObject::connect(tetheringToggle, &ToggleControl::toggleFlipped, this, &AdvancedNetworking::toggleTethering); // Change tethering password - ButtonControl *editPasswordButton = new ButtonControl("Tethering Password", "EDIT"); + ButtonControl *editPasswordButton = new ButtonControl(tr("Tethering Password"), tr("EDIT")); connect(editPasswordButton, &ButtonControl::clicked, [=]() { - QString pass = InputDialog::getText("Enter new tethering password", this, "", true, 8, wifi->getTetheringPassword()); + QString pass = InputDialog::getText(tr("Enter new tethering password"), this, "", true, 8, wifi->getTetheringPassword()); if (!pass.isEmpty()) { wifi->changeTetheringPassword(pass); } @@ -141,7 +141,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid list->addItem(editPasswordButton); // IP address - ipLabel = new LabelControl("IP Address", wifi->ipv4_address); + ipLabel = new LabelControl(tr("IP Address"), wifi->ipv4_address); list->addItem(ipLabel); // SSH keys @@ -150,7 +150,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid // Roaming toggle const bool roamingEnabled = params.getBool("GsmRoaming"); - ToggleControl *roamingToggle = new ToggleControl("Enable Roaming", "", "", roamingEnabled); + ToggleControl *roamingToggle = new ToggleControl(tr("Enable Roaming"), "", "", roamingEnabled); QObject::connect(roamingToggle, &SshToggle::toggleFlipped, [=](bool state) { params.putBool("GsmRoaming", state); wifi->updateGsmSettings(state, QString::fromStdString(params.get("GsmApn"))); @@ -158,11 +158,11 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid list->addItem(roamingToggle); // APN settings - ButtonControl *editApnButton = new ButtonControl("APN Setting", "EDIT"); + ButtonControl *editApnButton = new ButtonControl(tr("APN Setting"), tr("EDIT")); connect(editApnButton, &ButtonControl::clicked, [=]() { const bool roamingEnabled = params.getBool("GsmRoaming"); const QString cur_apn = QString::fromStdString(params.get("GsmApn")); - QString apn = InputDialog::getText("Enter APN", this, "leave blank for automatic configuration", false, -1, cur_apn).trimmed(); + QString apn = InputDialog::getText(tr("Enter APN"), this, tr("leave blank for automatic configuration"), false, -1, cur_apn).trimmed(); if (apn.isEmpty()) { params.remove("GsmApn"); @@ -207,7 +207,7 @@ WifiUI::WifiUI(QWidget *parent, WifiManager* wifi) : QWidget(parent), wifi(wifi) checkmark = QPixmap(ASSET_PATH + "offroad/icon_checkmark.svg").scaledToWidth(49, Qt::SmoothTransformation); circled_slash = QPixmap(ASSET_PATH + "img_circled_slash.svg").scaledToWidth(49, Qt::SmoothTransformation); - QLabel *scanning = new QLabel("Scanning for networks..."); + QLabel *scanning = new QLabel(tr("Scanning for networks...")); scanning->setStyleSheet("font-size: 65px;"); main_layout->addWidget(scanning, 0, Qt::AlignCenter); @@ -260,7 +260,7 @@ void WifiUI::refresh() { clearLayout(main_layout); if (wifi->seenNetworks.size() == 0) { - QLabel *scanning = new QLabel("Scanning for networks..."); + QLabel *scanning = new QLabel(tr("Scanning for networks...")); scanning->setStyleSheet("font-size: 65px;"); main_layout->addWidget(scanning, 0, Qt::AlignCenter); return; @@ -286,17 +286,17 @@ void WifiUI::refresh() { hlayout->addWidget(ssidLabel, network.connected == ConnectedType::CONNECTING ? 0 : 1); if (network.connected == ConnectedType::CONNECTING) { - QPushButton *connecting = new QPushButton("CONNECTING..."); + QPushButton *connecting = new QPushButton(tr("CONNECTING...")); connecting->setObjectName("connecting"); hlayout->addWidget(connecting, 2, Qt::AlignLeft); } // Forget button if (wifi->isKnownConnection(network.ssid) && !wifi->isTetheringEnabled()) { - QPushButton *forgetBtn = new QPushButton("FORGET"); + QPushButton *forgetBtn = new QPushButton(tr("FORGET")); forgetBtn->setObjectName("forgetBtn"); QObject::connect(forgetBtn, &QPushButton::clicked, [=]() { - if (ConfirmationDialog::confirm("Forget Wi-Fi Network \"" + QString::fromUtf8(network.ssid) + "\"?", this)) { + if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"") + QString::fromUtf8(network.ssid) + "\"?", this)) { wifi->forgetConnection(network.ssid); } }); diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index d32cc26e43f965..77e84293b244c7 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -76,7 +76,7 @@ void TermsPage::showEvent(QShowEvent *event) { main_layout->setContentsMargins(45, 35, 45, 45); main_layout->setSpacing(0); - QLabel *title = new QLabel("Terms & Conditions"); + QLabel *title = new QLabel(tr("Terms & Conditions")); title->setStyleSheet("font-size: 90px; font-weight: 600;"); main_layout->addWidget(title); @@ -104,11 +104,11 @@ void TermsPage::showEvent(QShowEvent *event) { buttons->setSpacing(45); main_layout->addLayout(buttons); - QPushButton *decline_btn = new QPushButton("Decline"); + QPushButton *decline_btn = new QPushButton(tr("Decline")); buttons->addWidget(decline_btn); QObject::connect(decline_btn, &QPushButton::clicked, this, &TermsPage::declinedTerms); - accept_btn = new QPushButton("Scroll to accept"); + accept_btn = new QPushButton(tr("Scroll to accept")); accept_btn->setEnabled(false); accept_btn->setStyleSheet(R"( QPushButton { @@ -123,7 +123,7 @@ void TermsPage::showEvent(QShowEvent *event) { } void TermsPage::enableAccept() { - accept_btn->setText("Agree"); + accept_btn->setText(tr("Agree")); accept_btn->setEnabled(true); } @@ -137,7 +137,7 @@ void DeclinePage::showEvent(QShowEvent *event) { main_layout->setSpacing(40); QLabel *text = new QLabel(this); - text->setText("You must accept the Terms and Conditions in order to use openpilot."); + text->setText(tr("You must accept the Terms and Conditions in order to use openpilot.")); text->setStyleSheet(R"(font-size: 80px; font-weight: 300; margin: 200px;)"); text->setWordWrap(true); main_layout->addWidget(text, 0, Qt::AlignCenter); @@ -146,12 +146,12 @@ void DeclinePage::showEvent(QShowEvent *event) { buttons->setSpacing(45); main_layout->addLayout(buttons); - QPushButton *back_btn = new QPushButton("Back"); + QPushButton *back_btn = new QPushButton(tr("Back")); buttons->addWidget(back_btn); QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack); - QPushButton *uninstall_btn = new QPushButton(QString("Decline, uninstall %1").arg(getBrand())); + QPushButton *uninstall_btn = new QPushButton(QString(tr("Decline, uninstall %1")).arg(getBrand())); uninstall_btn->setStyleSheet("background-color: #B73D3D"); buttons->addWidget(uninstall_btn); QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() { diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 175e6cf5a3d32d..392287d65d2c25 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -29,45 +29,45 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { std::vector> toggles{ { "OpenpilotEnabledToggle", - "Enable openpilot", - "Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off.", + tr("Enable openpilot"), + tr("Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off."), "../assets/offroad/icon_openpilot.png", }, { "IsLdwEnabled", - "Enable Lane Departure Warnings", - "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h).", + tr("Enable Lane Departure Warnings"), + tr("Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h)."), "../assets/offroad/icon_warning.png", }, { "IsRHD", - "Enable Right-Hand Drive", - "Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat.", + tr("Enable Right-Hand Drive"), + tr("Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat."), "../assets/offroad/icon_openpilot_mirrored.png", }, { "IsMetric", - "Use Metric System", - "Display speed in km/h instead of mph.", + tr("Use Metric System"), + tr("Display speed in km/h instead of mph."), "../assets/offroad/icon_metric.png", }, { "RecordFront", - "Record and Upload Driver Camera", - "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", + tr("Record and Upload Driver Camera"), + tr("Upload data from the driver facing camera and help improve the driver monitoring algorithm."), "../assets/offroad/icon_monitoring.png", }, { "DisengageOnAccelerator", - "Disengage On Accelerator Pedal", - "When enabled, pressing the accelerator pedal will disengage openpilot.", + tr("Disengage On Accelerator Pedal"), + tr("When enabled, pressing the accelerator pedal will disengage openpilot."), "../assets/offroad/icon_disengage_on_accelerator.svg", }, #ifdef ENABLE_MAPS { "NavSettingTime24h", - "Show ETA in 24h format", - "Use 24h format instead of am/pm", + tr("Show ETA in 24h format"), + tr("Use 24h format instead of am/pm"), "../assets/offroad/icon_metric.png", }, #endif @@ -79,8 +79,8 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { if (params.getBool("DisableRadar_Allow")) { toggles.push_back({ "DisableRadar", - "openpilot Longitudinal Control", - "openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB!", + tr("openpilot Longitudinal Control"), + tr("openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB!"), "../assets/offroad/icon_speed_limit.png", }); } @@ -95,29 +95,29 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { setSpacing(50); - addItem(new LabelControl("Dongle ID", getDongleId().value_or("N/A"))); - addItem(new LabelControl("Serial", params.get("HardwareSerial").c_str())); + addItem(new LabelControl(tr("Dongle ID"), getDongleId().value_or(tr("N/A")))); + addItem(new LabelControl(tr("Serial"), params.get("HardwareSerial").c_str())); // offroad-only buttons - auto dcamBtn = new ButtonControl("Driver Camera", "PREVIEW", - "Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off)"); + auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), + tr("Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off)")); connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); addItem(dcamBtn); - auto resetCalibBtn = new ButtonControl("Reset Calibration", "RESET", " "); + auto resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescription, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm("Are you sure you want to reset calibration?", this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset calibration?"), this)) { params.remove("CalibrationParams"); } }); addItem(resetCalibBtn); if (!params.getBool("Passive")) { - auto retrainingBtn = new ButtonControl("Review Training Guide", "REVIEW", "Review the rules, features, and limitations of openpilot"); + auto retrainingBtn = new ButtonControl(tr("Review Training Guide"), tr("REVIEW"), tr("Review the rules, features, and limitations of openpilot")); connect(retrainingBtn, &ButtonControl::clicked, [=]() { - if (ConfirmationDialog::confirm("Are you sure you want to review the training guide?", this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), this)) { emit reviewTrainingGuide(); } }); @@ -125,7 +125,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { } if (Hardware::TICI()) { - auto regulatoryBtn = new ButtonControl("Regulatory", "VIEW", ""); + auto regulatoryBtn = new ButtonControl(tr("Regulatory"), tr("VIEW"), ""); connect(regulatoryBtn, &ButtonControl::clicked, [=]() { const std::string txt = util::read_file("../assets/offroad/fcc.html"); RichTextDialog::alert(QString::fromStdString(txt), this); @@ -143,12 +143,12 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { QHBoxLayout *power_layout = new QHBoxLayout(); power_layout->setSpacing(30); - QPushButton *reboot_btn = new QPushButton("Reboot"); + QPushButton *reboot_btn = new QPushButton(tr("Reboot")); reboot_btn->setObjectName("reboot_btn"); power_layout->addWidget(reboot_btn); QObject::connect(reboot_btn, &QPushButton::clicked, this, &DevicePanel::reboot); - QPushButton *poweroff_btn = new QPushButton("Power Off"); + QPushButton *poweroff_btn = new QPushButton(tr("Power Off")); poweroff_btn->setObjectName("poweroff_btn"); power_layout->addWidget(poweroff_btn); QObject::connect(poweroff_btn, &QPushButton::clicked, this, &DevicePanel::poweroff); @@ -168,8 +168,8 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { void DevicePanel::updateCalibDescription() { QString desc = - "openpilot requires the device to be mounted within 4° left or right and " - "within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required."; + tr("openpilot requires the device to be mounted within 4° left or right and " + "within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required."); std::string calib_bytes = Params().get("CalibrationParams"); if (!calib_bytes.empty()) { try { @@ -179,9 +179,9 @@ void DevicePanel::updateCalibDescription() { if (calib.getCalStatus() != 0) { double pitch = calib.getRpyCalib()[1] * (180 / M_PI); double yaw = calib.getRpyCalib()[2] * (180 / M_PI); - desc += QString(" Your device is pointed %1° %2 and %3° %4.") - .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? "down" : "up", - QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? "left" : "right"); + desc += QString(tr(" Your device is pointed %1° %2 and %3° %4.")) + .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), + QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); } } catch (kj::Exception) { qInfo() << "invalid CalibrationParams"; @@ -192,51 +192,51 @@ void DevicePanel::updateCalibDescription() { void DevicePanel::reboot() { if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm("Are you sure you want to reboot?", this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot?"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { Params().putBool("DoReboot", true); } } } else { - ConfirmationDialog::alert("Disengage to Reboot", this); + ConfirmationDialog::alert(tr("Disengage to Reboot"), this); } } void DevicePanel::poweroff() { if (!uiState()->engaged()) { - if (ConfirmationDialog::confirm("Are you sure you want to power off?", this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to power off?"), this)) { // Check engaged again in case it changed while the dialog was open if (!uiState()->engaged()) { Params().putBool("DoShutdown", true); } } } else { - ConfirmationDialog::alert("Disengage to Power Off", this); + ConfirmationDialog::alert(tr("Disengage to Power Off"), this); } } SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { - gitBranchLbl = new LabelControl("Git Branch"); - gitCommitLbl = new LabelControl("Git Commit"); - osVersionLbl = new LabelControl("OS Version"); - versionLbl = new LabelControl("Version", "", QString::fromStdString(params.get("ReleaseNotes")).trimmed()); - lastUpdateLbl = new LabelControl("Last Update Check", "", "The last time openpilot successfully checked for an update. The updater only runs while the car is off."); - updateBtn = new ButtonControl("Check for Update", ""); + gitBranchLbl = new LabelControl(tr("Git Branch")); + gitCommitLbl = new LabelControl(tr("Git Commit")); + osVersionLbl = new LabelControl(tr("OS Version")); + versionLbl = new LabelControl(tr("Version"), "", QString::fromStdString(params.get("ReleaseNotes")).trimmed()); + lastUpdateLbl = new LabelControl(tr("Last Update Check"), "", tr("The last time openpilot successfully checked for an update. The updater only runs while the car is off.")); + updateBtn = new ButtonControl(tr("Check for Update"), ""); connect(updateBtn, &ButtonControl::clicked, [=]() { if (params.getBool("IsOffroad")) { fs_watch->addPath(QString::fromStdString(params.getParamPath("LastUpdateTime"))); fs_watch->addPath(QString::fromStdString(params.getParamPath("UpdateFailedCount"))); - updateBtn->setText("CHECKING"); + updateBtn->setText(tr("CHECKING")); updateBtn->setEnabled(false); } std::system("pkill -1 -f selfdrive.updated"); }); - auto uninstallBtn = new ButtonControl("Uninstall " + getBrand(), "UNINSTALL"); + auto uninstallBtn = new ButtonControl(tr("Uninstall ") + getBrand(), tr("UNINSTALL")); connect(uninstallBtn, &ButtonControl::clicked, [&]() { - if (ConfirmationDialog::confirm("Are you sure you want to uninstall?", this)) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), this)) { params.putBool("DoUninstall", true); } }); @@ -250,8 +250,8 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { fs_watch = new QFileSystemWatcher(this); QObject::connect(fs_watch, &QFileSystemWatcher::fileChanged, [=](const QString path) { if (path.contains("UpdateFailedCount") && std::atoi(params.get("UpdateFailedCount").c_str()) > 0) { - lastUpdateLbl->setText("failed to fetch update"); - updateBtn->setText("CHECK"); + lastUpdateLbl->setText(tr("failed to fetch update")); + updateBtn->setText(tr("CHECK")); updateBtn->setEnabled(true); } else if (path.contains("LastUpdateTime")) { updateLabels(); @@ -272,7 +272,7 @@ void SoftwarePanel::updateLabels() { versionLbl->setText(getBrandVersion()); lastUpdateLbl->setText(lastUpdate); - updateBtn->setText("CHECK"); + updateBtn->setText(tr("CHECK")); updateBtn->setEnabled(true); gitBranchLbl->setText(QString::fromStdString(params.get("GitBranch"))); gitCommitLbl->setText(QString::fromStdString(params.get("GitCommit")).left(10)); @@ -301,7 +301,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { )"); // close button - QPushButton *close_btn = new QPushButton("×"); + QPushButton *close_btn = new QPushButton(tr("×")); close_btn->setStyleSheet(R"( QPushButton { font-size: 140px; @@ -327,15 +327,15 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { QObject::connect(device, &DevicePanel::showDriverView, this, &SettingsWindow::showDriverView); QList> panels = { - {"Device", device}, - {"Network", network_panel(this)}, - {"Toggles", new TogglesPanel(this)}, - {"Software", new SoftwarePanel(this)}, + {tr("Device"), device}, + {tr("Network"), network_panel(this)}, + {tr("Toggles"), new TogglesPanel(this)}, + {tr("Software"), new SoftwarePanel(this)}, }; #ifdef ENABLE_MAPS auto map_panel = new MapPanel(this); - panels.push_back({"Navigation", map_panel}); + panels.push_back({tr("Navigation"), map_panel}); QObject::connect(map_panel, &MapPanel::closeSettings, this, &SettingsWindow::closeSettings); #endif @@ -367,7 +367,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { nav_btns->addButton(btn); sidebar_layout->addWidget(btn, 0, Qt::AlignRight); - const int lr_margin = name != "Network" ? 50 : 0; // Network panel handles its own margins + const int lr_margin = name != tr("Network") ? 50 : 0; // Network panel handles its own margins panel->setContentsMargins(lr_margin, 25, lr_margin, 25); ScrollView *panel_frame = new ScrollView(panel, this); diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index ecd6d61471cf8b..7032fc75184292 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -199,7 +199,7 @@ void NvgWindow::updateState(const UIState &s) { setProperty("is_metric", s.scene.is_metric); setProperty("speed", cur_speed); setProperty("setSpeed", set_speed); - setProperty("speedUnit", s.scene.is_metric ? "km/h" : "mph"); + setProperty("speedUnit", s.scene.is_metric ? tr("km/h") : tr("mph")); setProperty("hideDM", cs.getAlertSize() != cereal::ControlsState::AlertSize::NONE); setProperty("status", s.status); @@ -266,10 +266,10 @@ void NvgWindow::drawHud(QPainter &p) { p.setPen(QColor(0xa6, 0xa6, 0xa6, 0xff)); } configFont(p, "Inter", 40, "SemiBold"); - QRect max_rect = getTextRect(p, Qt::AlignCenter, "MAX"); + QRect max_rect = getTextRect(p, Qt::AlignCenter, tr("MAX")); max_rect.moveCenter({set_speed_rect.center().x(), 0}); max_rect.moveTop(set_speed_rect.top() + 27); - p.drawText(max_rect, Qt::AlignCenter, "MAX"); + p.drawText(max_rect, Qt::AlignCenter, tr("MAX")); // Draw set speed if (is_cruise_set) { @@ -313,16 +313,16 @@ void NvgWindow::drawHud(QPainter &p) { // "SPEED" configFont(p, "Inter", 28, "SemiBold"); - QRect text_speed_rect = getTextRect(p, Qt::AlignCenter, "SPEED"); + QRect text_speed_rect = getTextRect(p, Qt::AlignCenter, tr("SPEED")); text_speed_rect.moveCenter({sign_rect.center().x(), 0}); text_speed_rect.moveTop(sign_rect_outer.top() + 22); - p.drawText(text_speed_rect, Qt::AlignCenter, "SPEED"); + p.drawText(text_speed_rect, Qt::AlignCenter, tr("SPEED")); // "LIMIT" - QRect text_limit_rect = getTextRect(p, Qt::AlignCenter, "LIMIT"); + QRect text_limit_rect = getTextRect(p, Qt::AlignCenter, tr("LIMIT")); text_limit_rect.moveCenter({sign_rect.center().x(), 0}); text_limit_rect.moveTop(sign_rect_outer.top() + 51); - p.drawText(text_limit_rect, Qt::AlignCenter, "LIMIT"); + p.drawText(text_limit_rect, Qt::AlignCenter, tr("LIMIT")); // Speed limit value configFont(p, "Inter", 70, "Bold"); diff --git a/selfdrive/ui/qt/setup/reset.cc b/selfdrive/ui/qt/setup/reset.cc index 9ffcf7f6cf583b..582217c1d7c064 100644 --- a/selfdrive/ui/qt/setup/reset.cc +++ b/selfdrive/ui/qt/setup/reset.cc @@ -26,16 +26,16 @@ void Reset::doReset() { if (rm == 0 || fmt == 0) { std::system("sudo reboot"); } - body->setText("Reset failed. Reboot to try again."); + body->setText(tr("Reset failed. Reboot to try again.")); rebootBtn->show(); } void Reset::confirm() { - const QString confirm_txt = "Are you sure you want to reset your device?"; + const QString confirm_txt = tr("Are you sure you want to reset your device?"); if (body->text() != confirm_txt) { body->setText(confirm_txt); } else { - body->setText("Resetting device..."); + body->setText(tr("Resetting device...")); rejectBtn->hide(); rebootBtn->hide(); confirmBtn->hide(); @@ -50,13 +50,13 @@ Reset::Reset(bool recover, QWidget *parent) : QWidget(parent) { main_layout->setContentsMargins(45, 220, 45, 45); main_layout->setSpacing(0); - QLabel *title = new QLabel("System Reset"); + QLabel *title = new QLabel(tr("System Reset")); title->setStyleSheet("font-size: 90px; font-weight: 600;"); main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); main_layout->addSpacing(60); - body = new QLabel("System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot."); + body = new QLabel(tr("System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.")); body->setWordWrap(true); body->setStyleSheet("font-size: 80px; font-weight: light;"); main_layout->addWidget(body, 1, Qt::AlignTop | Qt::AlignLeft); @@ -65,11 +65,11 @@ Reset::Reset(bool recover, QWidget *parent) : QWidget(parent) { main_layout->addLayout(blayout); blayout->setSpacing(50); - rejectBtn = new QPushButton("Cancel"); + rejectBtn = new QPushButton(tr("Cancel")); blayout->addWidget(rejectBtn); QObject::connect(rejectBtn, &QPushButton::clicked, QCoreApplication::instance(), &QCoreApplication::quit); - rebootBtn = new QPushButton("Reboot"); + rebootBtn = new QPushButton(tr("Reboot")); blayout->addWidget(rebootBtn); #ifdef __aarch64__ QObject::connect(rebootBtn, &QPushButton::clicked, [=]{ @@ -77,7 +77,7 @@ Reset::Reset(bool recover, QWidget *parent) : QWidget(parent) { }); #endif - confirmBtn = new QPushButton("Confirm"); + confirmBtn = new QPushButton(tr("Confirm")); confirmBtn->setStyleSheet("background-color: #465BEA;"); blayout->addWidget(confirmBtn); QObject::connect(confirmBtn, &QPushButton::clicked, this, &Reset::confirm); @@ -85,7 +85,7 @@ Reset::Reset(bool recover, QWidget *parent) : QWidget(parent) { rejectBtn->setVisible(!recover); rebootBtn->setVisible(recover); if (recover) { - body->setText("Unable to mount data partition. Press confirm to reset your device."); + body->setText(tr("Unable to mount data partition. Press confirm to reset your device.")); } setStyleSheet(R"( diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc index 10a2d05f348baf..69dafcf741e8d9 100644 --- a/selfdrive/ui/qt/setup/setup.cc +++ b/selfdrive/ui/qt/setup/setup.cc @@ -70,13 +70,13 @@ QWidget * Setup::low_voltage() { inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft); inner_layout->addSpacing(80); - QLabel *title = new QLabel("WARNING: Low Voltage"); + QLabel *title = new QLabel(tr("WARNING: Low Voltage")); title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); inner_layout->addSpacing(25); - QLabel *body = new QLabel("Power your device in a car with a harness or proceed at your own risk."); + QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk.")); body->setWordWrap(true); body->setAlignment(Qt::AlignTop | Qt::AlignLeft); body->setStyleSheet("font-size: 80px; font-weight: 300;"); @@ -89,14 +89,14 @@ QWidget * Setup::low_voltage() { blayout->setSpacing(50); main_layout->addLayout(blayout, 0); - QPushButton *poweroff = new QPushButton("Power off"); + QPushButton *poweroff = new QPushButton(tr("Power off")); poweroff->setObjectName("navBtn"); blayout->addWidget(poweroff); QObject::connect(poweroff, &QPushButton::clicked, this, [=]() { Hardware::poweroff(); }); - QPushButton *cont = new QPushButton("Continue"); + QPushButton *cont = new QPushButton(tr("Continue")); cont->setObjectName("navBtn"); blayout->addWidget(cont); QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); @@ -114,12 +114,12 @@ QWidget * Setup::getting_started() { vlayout->setContentsMargins(165, 280, 100, 0); main_layout->addLayout(vlayout); - QLabel *title = new QLabel("Getting Started"); + QLabel *title = new QLabel(tr("Getting Started")); title->setStyleSheet("font-size: 90px; font-weight: 500;"); vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); vlayout->addSpacing(90); - QLabel *desc = new QLabel("Before we get on the road, let’s finish installation and cover some details."); + QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details.")); desc->setWordWrap(true); desc->setStyleSheet("font-size: 80px; font-weight: 300;"); vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); @@ -144,7 +144,7 @@ QWidget * Setup::network_setup() { main_layout->setContentsMargins(55, 50, 55, 50); // title - QLabel *title = new QLabel("Connect to Wi-Fi"); + QLabel *title = new QLabel(tr("Connect to Wi-Fi")); title->setStyleSheet("font-size: 90px; font-weight: 500;"); main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); @@ -162,7 +162,7 @@ QWidget * Setup::network_setup() { main_layout->addLayout(blayout); blayout->setSpacing(50); - QPushButton *back = new QPushButton("Back"); + QPushButton *back = new QPushButton(tr("Back")); back->setObjectName("navBtn"); QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); blayout->addWidget(back); @@ -179,9 +179,9 @@ QWidget * Setup::network_setup() { cont->setEnabled(success); if (success) { const bool cell = networking->wifi->currentNetworkType() == NetworkType::CELL; - cont->setText(cell ? "Continue without Wi-Fi" : "Continue"); + cont->setText(cell ? tr("Continue without Wi-Fi") : tr("Continue")); } else { - cont->setText("Waiting for internet"); + cont->setText(tr("Waiting for internet")); } repaint(); }); @@ -235,7 +235,7 @@ QWidget * Setup::software_selection() { main_layout->setSpacing(0); // title - QLabel *title = new QLabel("Choose Software to Install"); + QLabel *title = new QLabel(tr("Choose Software to Install")); title->setStyleSheet("font-size: 90px; font-weight: 500;"); main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); @@ -245,12 +245,12 @@ QWidget * Setup::software_selection() { QButtonGroup *group = new QButtonGroup(widget); group->setExclusive(true); - QWidget *dashcam = radio_button("Dashcam", group); + QWidget *dashcam = radio_button(tr("Dashcam"), group); main_layout->addWidget(dashcam); main_layout->addSpacing(30); - QWidget *custom = radio_button("Custom Software", group); + QWidget *custom = radio_button(tr("Custom Software"), group); main_layout->addWidget(custom); main_layout->addStretch(); @@ -260,12 +260,12 @@ QWidget * Setup::software_selection() { main_layout->addLayout(blayout); blayout->setSpacing(50); - QPushButton *back = new QPushButton("Back"); + QPushButton *back = new QPushButton(tr("Back")); back->setObjectName("navBtn"); QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); blayout->addWidget(back); - QPushButton *cont = new QPushButton("Continue"); + QPushButton *cont = new QPushButton(tr("Continue")); cont->setObjectName("navBtn"); cont->setEnabled(false); cont->setProperty("primary", true); @@ -278,7 +278,7 @@ QWidget * Setup::software_selection() { }); QString url = DASHCAM_URL; if (group->checkedButton() != dashcam) { - url = InputDialog::getText("Enter URL", this, "for Custom Software"); + url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); } if (!url.isEmpty()) { QTimer::singleShot(1000, this, [=]() { @@ -300,7 +300,7 @@ QWidget * Setup::software_selection() { QWidget * Setup::downloading() { QWidget *widget = new QWidget(); QVBoxLayout *main_layout = new QVBoxLayout(widget); - QLabel *txt = new QLabel("Downloading..."); + QLabel *txt = new QLabel(tr("Downloading...")); txt->setStyleSheet("font-size: 90px; font-weight: 500;"); main_layout->addWidget(txt, 0, Qt::AlignCenter); return widget; @@ -312,13 +312,13 @@ QWidget * Setup::download_failed() { main_layout->setContentsMargins(55, 225, 55, 55); main_layout->setSpacing(0); - QLabel *title = new QLabel("Download Failed"); + QLabel *title = new QLabel(tr("Download Failed")); title->setStyleSheet("font-size: 90px; font-weight: 500;"); main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); main_layout->addSpacing(67); - QLabel *body = new QLabel("Ensure the entered URL is valid, and the device’s internet connection is good."); + QLabel *body = new QLabel(tr("Ensure the entered URL is valid, and the device’s internet connection is good.")); body->setWordWrap(true); body->setAlignment(Qt::AlignTop | Qt::AlignLeft); body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); @@ -331,14 +331,14 @@ QWidget * Setup::download_failed() { blayout->setSpacing(50); main_layout->addLayout(blayout, 0); - QPushButton *reboot = new QPushButton("Reboot device"); + QPushButton *reboot = new QPushButton(tr("Reboot device")); reboot->setObjectName("navBtn"); blayout->addWidget(reboot); QObject::connect(reboot, &QPushButton::clicked, this, [=]() { Hardware::reboot(); }); - QPushButton *restart = new QPushButton("Start over"); + QPushButton *restart = new QPushButton(tr("Start over")); restart->setObjectName("navBtn"); restart->setProperty("primary", true); blayout->addWidget(restart); diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc index 6e8189f4bab1ce..fd7148c5340d42 100644 --- a/selfdrive/ui/qt/setup/updater.cc +++ b/selfdrive/ui/qt/setup/updater.cc @@ -20,13 +20,13 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid QVBoxLayout *layout = new QVBoxLayout(prompt); layout->setContentsMargins(100, 250, 100, 100); - QLabel *title = new QLabel("Update Required"); + QLabel *title = new QLabel(tr("Update Required")); title->setStyleSheet("font-size: 80px; font-weight: bold;"); layout->addWidget(title); layout->addSpacing(75); - QLabel *desc = new QLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB."); + QLabel *desc = new QLabel(tr("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB.")); desc->setWordWrap(true); desc->setStyleSheet("font-size: 65px;"); layout->addWidget(desc); @@ -37,14 +37,14 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid hlayout->setSpacing(30); layout->addLayout(hlayout); - QPushButton *connect = new QPushButton("Connect to Wi-Fi"); + QPushButton *connect = new QPushButton(tr("Connect to Wi-Fi")); connect->setObjectName("navBtn"); QObject::connect(connect, &QPushButton::clicked, [=]() { setCurrentWidget(wifi); }); hlayout->addWidget(connect); - QPushButton *install = new QPushButton("Install"); + QPushButton *install = new QPushButton(tr("Install")); install->setObjectName("navBtn"); install->setStyleSheet("background-color: #465BEA;"); QObject::connect(install, &QPushButton::clicked, this, &Updater::installUpdate); @@ -61,7 +61,7 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); layout->addWidget(networking, 1); - QPushButton *back = new QPushButton("Back"); + QPushButton *back = new QPushButton(tr("Back")); back->setObjectName("navBtn"); back->setStyleSheet("padding-left: 60px; padding-right: 60px;"); QObject::connect(back, &QPushButton::clicked, [=]() { @@ -77,7 +77,7 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid layout->setContentsMargins(150, 330, 150, 150); layout->setSpacing(0); - text = new QLabel("Loading..."); + text = new QLabel(tr("Loading...")); text->setStyleSheet("font-size: 90px; font-weight: 600;"); layout->addWidget(text, 0, Qt::AlignTop); @@ -91,7 +91,7 @@ Updater::Updater(const QString &updater_path, const QString &manifest_path, QWid layout->addStretch(); - reboot = new QPushButton("Reboot"); + reboot = new QPushButton(tr("Reboot")); reboot->setObjectName("navBtn"); reboot->setStyleSheet("padding-left: 60px; padding-right: 60px;"); QObject::connect(reboot, &QPushButton::clicked, [=]() { @@ -161,7 +161,7 @@ void Updater::updateFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode == 0) { Hardware::reboot(); } else { - text->setText("Update failed"); + text->setText(tr("Update failed")); reboot->show(); } } diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index 00e72b352c4d93..accab67bf071eb 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -64,26 +64,26 @@ void Sidebar::updateState(const UIState &s) { ItemStatus connectStatus; auto last_ping = deviceState.getLastAthenaPingTime(); if (last_ping == 0) { - connectStatus = ItemStatus{{"CONNECT", "OFFLINE"}, warning_color}; + connectStatus = ItemStatus{{tr("CONNECT"), tr("OFFLINE")}, warning_color}; } else { - connectStatus = nanos_since_boot() - last_ping < 80e9 ? ItemStatus{{"CONNECT", "ONLINE"}, good_color} : ItemStatus{{"CONNECT", "ERROR"}, danger_color}; + connectStatus = nanos_since_boot() - last_ping < 80e9 ? ItemStatus{{tr("CONNECT"), tr("ONLINE")}, good_color} : ItemStatus{{tr("CONNECT"), tr("ERROR")}, danger_color}; } setProperty("connectStatus", QVariant::fromValue(connectStatus)); - ItemStatus tempStatus = {{"TEMP", "HIGH"}, danger_color}; + ItemStatus tempStatus = {{tr("TEMP"), tr("HIGH")}, danger_color}; auto ts = deviceState.getThermalStatus(); if (ts == cereal::DeviceState::ThermalStatus::GREEN) { - tempStatus = {{"TEMP", "GOOD"}, good_color}; + tempStatus = {{tr("TEMP"), tr("GOOD")}, good_color}; } else if (ts == cereal::DeviceState::ThermalStatus::YELLOW) { - tempStatus = {{"TEMP", "OK"}, warning_color}; + tempStatus = {{tr("TEMP"), tr("OK")}, warning_color}; } setProperty("tempStatus", QVariant::fromValue(tempStatus)); - ItemStatus pandaStatus = {{"VEHICLE", "ONLINE"}, good_color}; + ItemStatus pandaStatus = {{tr("VEHICLE"), tr("ONLINE")}, good_color}; if (s.scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { - pandaStatus = {{"NO", "PANDA"}, danger_color}; + pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; } else if (s.scene.started && !sm["liveLocationKalman"].getLiveLocationKalman().getGpsOK()) { - pandaStatus = {{"GPS", "SEARCH"}, warning_color}; + pandaStatus = {{tr("GPS"), tr("SEARCH")}, warning_color}; } setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); } diff --git a/selfdrive/ui/qt/text.cc b/selfdrive/ui/qt/text.cc index ac8e7bcd18e531..21ec5eedcf6be4 100644 --- a/selfdrive/ui/qt/text.cc +++ b/selfdrive/ui/qt/text.cc @@ -33,12 +33,12 @@ int main(int argc, char *argv[]) { QPushButton *btn = new QPushButton(); #ifdef __aarch64__ - btn->setText("Reboot"); + btn->setText(QObject::tr("Reboot")); QObject::connect(btn, &QPushButton::clicked, [=]() { Hardware::reboot(); }); #else - btn->setText("Exit"); + btn->setText(QObject::tr("Exit")); QObject::connect(btn, &QPushButton::clicked, &a, &QApplication::quit); #endif main_layout->addWidget(btn, 0, 0, Qt::AlignRight | Qt::AlignBottom); diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index 366aa76f494d53..f82d9bf496c517 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -15,7 +15,7 @@ QString getVersion() { } QString getBrand() { - return Params().getBool("Passive") ? "dashcam" : "openpilot"; + return Params().getBool("Passive") ? QObject::tr("dashcam") : QObject::tr("openpilot"); } QString getBrandVersion() { @@ -63,13 +63,13 @@ QString timeAgo(const QDateTime &date) { s = "now"; } else if (diff < 60 * 60) { int minutes = diff / 60; - s = QString("%1 minute%2 ago").arg(minutes).arg(minutes > 1 ? "s" : ""); + s = QString(QObject::tr("%1 minute%2 ago")).arg(minutes).arg(minutes > 1 ? "s" : ""); } else if (diff < 60 * 60 * 24) { int hours = diff / (60 * 60); - s = QString("%1 hour%2 ago").arg(hours).arg(hours > 1 ? "s" : ""); + s = QString(QObject::tr("%1 hour%2 ago")).arg(hours).arg(hours > 1 ? "s" : ""); } else if (diff < 3600 * 24 * 7) { int days = diff / (60 * 60 * 24); - s = QString("%1 day%2 ago").arg(days).arg(days > 1 ? "s" : ""); + s = QString(QObject::tr("%1 day%2 ago")).arg(days).arg(days > 1 ? "s" : ""); } else { s = date.date().toString(); } diff --git a/selfdrive/ui/qt/widgets/drive_stats.cc b/selfdrive/ui/qt/widgets/drive_stats.cc index 61d47db1c3d7c1..31009f03ca4ac2 100644 --- a/selfdrive/ui/qt/widgets/drive_stats.cc +++ b/selfdrive/ui/qt/widgets/drive_stats.cc @@ -34,16 +34,16 @@ DriveStats::DriveStats(QWidget* parent) : QFrame(parent) { grid_layout->addWidget(labels.distance = newLabel("0", "number"), row, 1, Qt::AlignLeft); grid_layout->addWidget(labels.hours = newLabel("0", "number"), row, 2, Qt::AlignLeft); - grid_layout->addWidget(newLabel("Drives", "unit"), row + 1, 0, Qt::AlignLeft); + grid_layout->addWidget(newLabel((tr("Drives")), "unit"), row + 1, 0, Qt::AlignLeft); grid_layout->addWidget(labels.distance_unit = newLabel(getDistanceUnit(), "unit"), row + 1, 1, Qt::AlignLeft); - grid_layout->addWidget(newLabel("Hours ", "unit"), row + 1, 2, Qt::AlignLeft); + grid_layout->addWidget(newLabel(tr("Hours"), "unit"), row + 1, 2, Qt::AlignLeft); main_layout->addLayout(grid_layout); }; - add_stats_layouts("ALL TIME", all_); + add_stats_layouts(tr("ALL TIME"), all_); main_layout->addStretch(); - add_stats_layouts("PAST WEEK", week_); + add_stats_layouts(tr("PAST WEEK"), week_); if (auto dongleId = getDongleId()) { QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/stats"; diff --git a/selfdrive/ui/qt/widgets/drive_stats.h b/selfdrive/ui/qt/widgets/drive_stats.h index 9446294516be6b..5e2d96b24086e3 100644 --- a/selfdrive/ui/qt/widgets/drive_stats.h +++ b/selfdrive/ui/qt/widgets/drive_stats.h @@ -12,7 +12,7 @@ class DriveStats : public QFrame { private: void showEvent(QShowEvent *event) override; void updateStats(); - inline QString getDistanceUnit() const { return metric_ ? "KM" : "Miles"; } + inline QString getDistanceUnit() const { return metric_ ? tr("KM") : tr("Miles"); } bool metric_; QJsonDocument stats_; diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 8a5d4928041032..755ccfe8c5aa65 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -67,7 +67,7 @@ InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &s vlayout->addWidget(sublabel, 1, Qt::AlignTop | Qt::AlignLeft); } - QPushButton* cancel_btn = new QPushButton("Cancel"); + QPushButton* cancel_btn = new QPushButton(tr("Cancel")); cancel_btn->setFixedSize(386, 125); cancel_btn->setStyleSheet(R"( font-size: 48px; @@ -164,7 +164,7 @@ void InputDialog::handleEnter() { done(QDialog::Accepted); emitText(line->text()); } else { - setMessage("Need at least "+QString::number(minLength)+" characters!", false); + setMessage(tr("Need at least ") + QString::number(minLength) + tr(" characters!"), false); } } @@ -217,12 +217,12 @@ ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString } bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d = ConfirmationDialog(prompt_text, "Ok", "", parent); + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", parent); return d.exec(); } bool ConfirmationDialog::confirm(const QString &prompt_text, QWidget *parent) { - ConfirmationDialog d = ConfirmationDialog(prompt_text, "Ok", "Cancel", parent); + ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), tr("Cancel"), parent); return d.exec(); } @@ -254,6 +254,6 @@ RichTextDialog::RichTextDialog(const QString &prompt_text, const QString &btn_te } bool RichTextDialog::alert(const QString &prompt_text, QWidget *parent) { - auto d = RichTextDialog(prompt_text, "Ok", parent); + auto d = RichTextDialog(prompt_text, tr("Ok"), parent); return d.exec(); } diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc index 433193df5d9c9e..937ea02f867aed 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -22,12 +22,12 @@ AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent QHBoxLayout *footer_layout = new QHBoxLayout(); main_layout->addLayout(footer_layout); - QPushButton *dismiss_btn = new QPushButton("Close"); + QPushButton *dismiss_btn = new QPushButton(tr("Close")); dismiss_btn->setFixedSize(400, 125); footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - snooze_btn = new QPushButton("Snooze Update"); + snooze_btn = new QPushButton(tr("Snooze Update")); snooze_btn->setVisible(false); snooze_btn->setFixedSize(550, 125); footer_layout->addWidget(snooze_btn, 0, Qt::AlignBottom | Qt::AlignRight); @@ -38,7 +38,7 @@ AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent snooze_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)"); if (hasRebootBtn) { - QPushButton *rebootBtn = new QPushButton("Reboot and Update"); + QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); rebootBtn->setFixedSize(600, 125); footer_layout->addWidget(rebootBtn, 0, Qt::AlignBottom | Qt::AlignRight); QObject::connect(rebootBtn, &QPushButton::clicked, [=]() { Hardware::reboot(); }); diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 8a208bf3cf1231..d2529821f4e00c 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -83,18 +83,18 @@ PairingPopup::PairingPopup(QWidget *parent) : QDialogBase(parent) { vlayout->addSpacing(30); - QLabel *title = new QLabel("Pair your device to your comma account", this); + QLabel *title = new QLabel(tr("Pair your device to your comma account"), this); title->setStyleSheet("font-size: 75px; color: black;"); title->setWordWrap(true); vlayout->addWidget(title); - QLabel *instructions = new QLabel(R"( + QLabel *instructions = new QLabel(tr(R"(

  1. Go to https://connect.comma.ai on your phone
  2. Click "add new device" and scan the QR code on the right
  3. Bookmark connect.comma.ai to your home screen to use it like an app
- )", this); + )"), this); instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); instructions->setWordWrap(true); vlayout->addWidget(instructions); @@ -120,19 +120,19 @@ PrimeUserWidget::PrimeUserWidget(QWidget* parent) : QWidget(parent) { primeLayout->setMargin(0); primeWidget->setContentsMargins(60, 50, 60, 50); - QLabel* subscribed = new QLabel("✓ SUBSCRIBED"); + QLabel* subscribed = new QLabel(tr("✓ SUBSCRIBED")); subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;"); primeLayout->addWidget(subscribed, 0, Qt::AlignTop); primeLayout->addSpacing(60); - QLabel* commaPrime = new QLabel("comma prime"); + QLabel* commaPrime = new QLabel(tr("comma prime")); commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); primeLayout->addWidget(commaPrime, 0, Qt::AlignTop); primeLayout->addSpacing(20); - QLabel* connectUrl = new QLabel("CONNECT.COMMA.AI"); + QLabel* connectUrl = new QLabel(tr("CONNECT.COMMA.AI")); connectUrl->setStyleSheet("font-size: 41px; font-family: Inter SemiBold; color: #A0A0A0;"); primeLayout->addWidget(connectUrl, 0, Qt::AlignTop); @@ -145,7 +145,7 @@ PrimeUserWidget::PrimeUserWidget(QWidget* parent) : QWidget(parent) { pointsLayout->setMargin(0); pointsWidget->setContentsMargins(60, 50, 60, 50); - QLabel* commaPoints = new QLabel("COMMA POINTS"); + QLabel* commaPoints = new QLabel(tr("COMMA POINTS")); commaPoints->setStyleSheet("font-size: 41px; font-family: Inter SemiBold;"); pointsLayout->addWidget(commaPoints, 0, Qt::AlignTop); @@ -181,24 +181,24 @@ PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) { main_layout->setContentsMargins(80, 90, 80, 60); main_layout->setSpacing(0); - QLabel *upgrade = new QLabel("Upgrade Now"); + QLabel *upgrade = new QLabel(tr("Upgrade Now")); upgrade->setStyleSheet("font-size: 75px; font-weight: bold;"); main_layout->addWidget(upgrade, 0, Qt::AlignTop); main_layout->addSpacing(50); - QLabel *description = new QLabel("Become a comma prime member at connect.comma.ai"); + QLabel *description = new QLabel(tr("Become a comma prime member at connect.comma.ai")); description->setStyleSheet("font-size: 60px; font-weight: light; color: white;"); description->setWordWrap(true); main_layout->addWidget(description, 0, Qt::AlignTop); main_layout->addStretch(); - QLabel *features = new QLabel("PRIME FEATURES:"); + QLabel *features = new QLabel(tr("PRIME FEATURES:")); features->setStyleSheet("font-size: 41px; font-weight: bold; color: #E5E5E5;"); main_layout->addWidget(features, 0, Qt::AlignBottom); main_layout->addSpacing(30); - QVector bullets = {"Remote access", "1 year of storage", "Developer perks"}; + QVector bullets = {tr("Remote access"), tr("1 year of storage"), tr("Developer perks")}; for (auto &b: bullets) { const QString check = " "; QLabel *l = new QLabel(check + b); @@ -227,20 +227,20 @@ SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) { finishRegistationLayout->setContentsMargins(30, 75, 30, 45); finishRegistationLayout->setSpacing(0); - QLabel* registrationTitle = new QLabel("Finish Setup"); + QLabel* registrationTitle = new QLabel(tr("Finish Setup")); registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold; margin-left: 55px;"); finishRegistationLayout->addWidget(registrationTitle); finishRegistationLayout->addSpacing(30); - QLabel* registrationDescription = new QLabel("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."); + QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer.")); registrationDescription->setWordWrap(true); registrationDescription->setStyleSheet("font-size: 55px; font-weight: light; margin-left: 55px;"); finishRegistationLayout->addWidget(registrationDescription); finishRegistationLayout->addStretch(); - QPushButton* pair = new QPushButton("Pair device"); + QPushButton* pair = new QPushButton(tr("Pair device")); pair->setFixedHeight(220); pair->setStyleSheet(R"( QPushButton { diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc index 46ccf6aee3aa86..1f48c72735ae7e 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.cc +++ b/selfdrive/ui/qt/widgets/ssh_keys.cc @@ -4,16 +4,16 @@ #include "selfdrive/ui/qt/api.h" #include "selfdrive/ui/qt/widgets/input.h" -SshControl::SshControl() : ButtonControl("SSH Keys", "", "Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username.") { +SshControl::SshControl() : ButtonControl(tr("SSH Keys"), "", tr("Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username.")) { username_label.setAlignment(Qt::AlignRight | Qt::AlignVCenter); username_label.setStyleSheet("color: #aaaaaa"); hlayout->insertWidget(1, &username_label); QObject::connect(this, &ButtonControl::clicked, [=]() { - if (text() == "ADD") { - QString username = InputDialog::getText("Enter your GitHub username", this); + if (text() == tr("ADD")) { + QString username = InputDialog::getText(tr("Enter your GitHub username"), this); if (username.length() > 0) { - setText("LOADING"); + setText(tr("LOADING")); setEnabled(false); getUserKeys(username); } @@ -31,10 +31,10 @@ void SshControl::refresh() { QString param = QString::fromStdString(params.get("GithubSshKeys")); if (param.length()) { username_label.setText(QString::fromStdString(params.get("GithubUsername"))); - setText("REMOVE"); + setText(tr("REMOVE")); } else { username_label.setText(""); - setText("ADD"); + setText(tr("ADD")); } setEnabled(true); } @@ -47,13 +47,13 @@ void SshControl::getUserKeys(const QString &username) { params.put("GithubUsername", username.toStdString()); params.put("GithubSshKeys", resp.toStdString()); } else { - ConfirmationDialog::alert(QString("Username '%1' has no keys on GitHub").arg(username), this); + ConfirmationDialog::alert(QString(tr("Username '%1' has no keys on GitHub")).arg(username), this); } } else { if (request->timeout()) { - ConfirmationDialog::alert("Request timed out", this); + ConfirmationDialog::alert(tr("Request timed out"), this); } else { - ConfirmationDialog::alert(QString("Username '%1' doesn't exist on GitHub").arg(username), this); + ConfirmationDialog::alert(QString(tr("Username '%1' doesn't exist on GitHub")).arg(username), this); } } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.h b/selfdrive/ui/qt/widgets/ssh_keys.h index 596d1d83b9b6d9..01e2ab83ce897d 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.h +++ b/selfdrive/ui/qt/widgets/ssh_keys.h @@ -10,7 +10,7 @@ class SshToggle : public ToggleControl { Q_OBJECT public: - SshToggle() : ToggleControl("Enable SSH", "", "", Hardware::get_ssh_enabled()) { + SshToggle() : ToggleControl(tr("Enable SSH"), "", "", Hardware::get_ssh_enabled()) { QObject::connect(this, &SshToggle::toggleFlipped, [=](bool state) { Hardware::set_ssh_enabled(state); }); diff --git a/selfdrive/ui/tests/.gitignore b/selfdrive/ui/tests/.gitignore index 7765bab17def47..26335744f3daf4 100644 --- a/selfdrive/ui/tests/.gitignore +++ b/selfdrive/ui/tests/.gitignore @@ -1,3 +1,4 @@ test playsound test_sound +test_translations diff --git a/selfdrive/ui/tests/create_test_translations.sh b/selfdrive/ui/tests/create_test_translations.sh new file mode 100755 index 00000000000000..451a3cbfb04c10 --- /dev/null +++ b/selfdrive/ui/tests/create_test_translations.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +UI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"/.. +TEST_TEXT="(WRAPPED_SOURCE_TEXT)" +TEST_TS_FILE=$UI_DIR/translations/main_test_en.ts +TEST_QM_FILE=$UI_DIR/translations/main_test_en.qm + +# translation strings +UNFINISHED="<\/translation>" +TRANSLATED="$TEST_TEXT<\/translation>" + +mkdir -p $UI_DIR/translations +rm -f $TEST_TS_FILE $TEST_QM_FILE +lupdate -recursive "$UI_DIR" -ts $TEST_TS_FILE +sed -i "s/$UNFINISHED/$TRANSLATED/" $TEST_TS_FILE +lrelease $TEST_TS_FILE diff --git a/selfdrive/ui/tests/test_runner.cc b/selfdrive/ui/tests/test_runner.cc index b20ac86c64847b..ac63139d178a89 100644 --- a/selfdrive/ui/tests/test_runner.cc +++ b/selfdrive/ui/tests/test_runner.cc @@ -1,10 +1,25 @@ #define CATCH_CONFIG_RUNNER #include "catch2/catch.hpp" -#include + +#include +#include +#include +#include int main(int argc, char **argv) { // unit tests for Qt - QCoreApplication app(argc, argv); + QApplication app(argc, argv); + + QString language_file = "main_test_en"; + qDebug() << "Loading language:" << language_file; + + QTranslator translator; + QString translationsPath = QDir::cleanPath(qApp->applicationDirPath() + "/../translations"); + if (!translator.load(language_file, translationsPath)) { + qDebug() << "Failed to load translation file!"; + } + app.installTranslator(&translator); + const int res = Catch::Session().run(argc, argv); return (res < 0xff ? res : 0xff); } diff --git a/selfdrive/ui/tests/test_translations.cc b/selfdrive/ui/tests/test_translations.cc new file mode 100644 index 00000000000000..fecb9da44a1527 --- /dev/null +++ b/selfdrive/ui/tests/test_translations.cc @@ -0,0 +1,51 @@ +#include "catch2/catch.hpp" + +#include "common/params.h" +#include "selfdrive/ui/qt/window.h" + +const QString TEST_TEXT = "(WRAPPED_SOURCE_TEXT)"; // what each string should be translated to +QRegExp RE_NUM("\\d*"); + +QStringList getParentWidgets(QWidget* widget){ + QStringList parentWidgets; + while (widget->parentWidget() != Q_NULLPTR) { + widget = widget->parentWidget(); + parentWidgets.append(widget->metaObject()->className()); + } + return parentWidgets; +} + +template +void checkWidgetTrWrap(MainWindow &w) { + int i = 0; + for (auto widget : w.findChildren()) { + const QString text = widget->text(); + SECTION(text.toStdString() + "-" + std::to_string(i)) { + bool isNumber = RE_NUM.exactMatch(text); + bool wrapped = text.contains(TEST_TEXT); + QString parentWidgets = getParentWidgets(widget).join("->"); + + if (!text.isEmpty() && !isNumber && !wrapped) { + FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString()); + } + + // warn if source string wrapped, but UI adds text + // TODO: add way to ignore this + if (wrapped && text != TEST_TEXT) { + WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString()); + } + } + i++; + } +} + +// Tests all strings in the UI are wrapped with tr() +TEST_CASE("UI: test all strings wrapped") { + Params().remove("HardwareSerial"); + Params().remove("DongleId"); + qputenv("TICI", "1"); + + MainWindow w; + checkWidgetTrWrap(w); + checkWidgetTrWrap(w); +} diff --git a/tools/ubuntu_setup.sh b/tools/ubuntu_setup.sh index 60d41a477117dc..5bc3630766f843 100755 --- a/tools/ubuntu_setup.sh +++ b/tools/ubuntu_setup.sh @@ -59,6 +59,7 @@ function install_ubuntu_common_requirements() { qtmultimedia5-dev \ qtlocation5-dev \ qtpositioning5-dev \ + qttools5-dev-tools \ libqt5sql5-sqlite \ libqt5svg5-dev \ libqt5x11extras5-dev \ From f79b068a71607700c4bbcc9e33f1bb2d10bb8e60 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 29 Jun 2022 20:31:22 -0700 Subject: [PATCH 197/436] Honda Civic 2022: remove LKAS fault reinitialization (#24979) * no lkas problem * remove frame --- selfdrive/car/honda/carcontroller.py | 2 +- selfdrive/car/honda/hondacan.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index 4d04927b2fc6aa..14049c9997f071 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -241,7 +241,7 @@ def update(self, CC, CS): idx = (self.frame // 10) % 4 hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_control.leadVisible, hud_control.lanesVisible, fcw_display, acc_alert, steer_required) - can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud, self.frame)) + can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud)) if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH: self.speed = pcm_speed diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 5de29b4f37c374..7246b98686bd47 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -101,7 +101,7 @@ def create_bosch_supplemental_1(packer, car_fingerprint, idx): return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values, idx) -def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud, frame): +def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud): commands = [] bus_pt = get_pt_bus(CP.carFingerprint) radar_disabled = CP.carFingerprint in HONDA_BOSCH and CP.openpilotLongitudinalControl @@ -141,8 +141,6 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc if CP.carFingerprint in HONDA_BOSCH_RADARLESS: lkas_hud_values['LANE_LINES'] = 3 lkas_hud_values['DASHED_LANES'] = hud.lanes_visible - # TODO: understand this better, does car need to see it fall after start up? - lkas_hud_values['LKAS_PROBLEM'] = 0 if frame > 200 else 1 if not (CP.flags & HondaFlags.BOSCH_EXT_HUD): lkas_hud_values['SET_ME_X48'] = 0x48 From e643f8e6815eee04511dceecd23b434e5255f15b Mon Sep 17 00:00:00 2001 From: Greg Hogan Date: Thu, 30 Jun 2022 01:52:56 -0700 Subject: [PATCH 198/436] docs: ssh.comma.ai (#25000) --- tools/ssh/README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tools/ssh/README.md b/tools/ssh/README.md index 66f030de5284e3..29d33493282076 100644 --- a/tools/ssh/README.md +++ b/tools/ssh/README.md @@ -22,3 +22,52 @@ The public keys are only fetched from your GitHub account once. In order to upda The `id_rsa` key in this directory only works while your device is in the setup state with no software installed. After installation, that default key will be removed. See the [community wiki](https://github.com/commaai/openpilot/wiki/SSH) for more detailed instructions and information. + +# Connecting to ssh.comma.ai +SSH into your comma device from anywhere with `ssh.comma.ai`. + +## Setup + +With software version 0.6.1 or newer, enter your GitHub username on your device under Developer Settings. Your GitHub authorized public keys will become your authorized SSH keys for `ssh.comma.ai`. You can add any additional keys in `/system/comma/home/.ssh/authorized_keys.persist`. + +Requires [comma SIM with comma prime](https://comma.ai/shop) activated with comma connect, available on iOS and Android. comma two and EON ship with a pre-inserted comma SIM. + +## Recommended .ssh/config + +With the below ssh configuration, you can type `ssh comma-{dongleid}` to connect to your device through `ssh.comma.ai`. For example, `ssh comma-ffffffffffffffff`. + +``` +Host comma-* + Port 22 + User comma + IdentityFile ~/.ssh/my_github_key + ProxyCommand ssh %h@ssh.comma.ai -W %h:%p +Host ssh.comma.ai + Hostname ssh.comma.ai + Port 22 + IdentityFile ~/.ssh/my_github_key +``` + +## One-off connection + +``` +ssh -i ~/.ssh/my_github_key -o ProxyCommand="ssh -i ~/.ssh/my_github_key -W %h:%p -p %p %h@ssh.comma.ai" comma@ffffffffffffffff +``` +(Replace `ffffffffffffffff` with your dongle_id) + +## ssh.comma.ai host key fingerprint + +``` +Host key fingerprint is SHA256:X22GOmfjGb9J04IA2+egtdaJ7vW9Fbtmpz9/x8/W1X4 ++---[RSA 4096]----+ +| | +| | +| . | +| + o | +| S = + +..| +| + @ = .=| +| . B @ ++=| +| o * B XE| +| .o o OB/| ++----[SHA256]-----+ +``` From 8d53e2c2b4e549119538d1baf36d202dbe33d012 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 16:55:49 +0800 Subject: [PATCH 199/436] Multilang: remove redundant QString() around tr() (#25003) remove qstring --- selfdrive/ui/qt/offroad/onboarding.cc | 2 +- selfdrive/ui/qt/offroad/settings.cc | 2 +- selfdrive/ui/qt/util.cc | 6 +++--- selfdrive/ui/qt/widgets/ssh_keys.cc | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/qt/offroad/onboarding.cc b/selfdrive/ui/qt/offroad/onboarding.cc index 77e84293b244c7..f3e50b572baf72 100644 --- a/selfdrive/ui/qt/offroad/onboarding.cc +++ b/selfdrive/ui/qt/offroad/onboarding.cc @@ -151,7 +151,7 @@ void DeclinePage::showEvent(QShowEvent *event) { QObject::connect(back_btn, &QPushButton::clicked, this, &DeclinePage::getBack); - QPushButton *uninstall_btn = new QPushButton(QString(tr("Decline, uninstall %1")).arg(getBrand())); + QPushButton *uninstall_btn = new QPushButton(tr("Decline, uninstall %1").arg(getBrand())); uninstall_btn->setStyleSheet("background-color: #B73D3D"); buttons->addWidget(uninstall_btn); QObject::connect(uninstall_btn, &QPushButton::clicked, [=]() { diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 392287d65d2c25..547ad168f1a667 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -179,7 +179,7 @@ void DevicePanel::updateCalibDescription() { if (calib.getCalStatus() != 0) { double pitch = calib.getRpyCalib()[1] * (180 / M_PI); double yaw = calib.getRpyCalib()[2] * (180 / M_PI); - desc += QString(tr(" Your device is pointed %1° %2 and %3° %4.")) + desc += tr(" Your device is pointed %1° %2 and %3° %4.") .arg(QString::number(std::abs(pitch), 'g', 1), pitch > 0 ? tr("down") : tr("up"), QString::number(std::abs(yaw), 'g', 1), yaw > 0 ? tr("left") : tr("right")); } diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index f82d9bf496c517..cab7299cd68843 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -63,13 +63,13 @@ QString timeAgo(const QDateTime &date) { s = "now"; } else if (diff < 60 * 60) { int minutes = diff / 60; - s = QString(QObject::tr("%1 minute%2 ago")).arg(minutes).arg(minutes > 1 ? "s" : ""); + s = QObject::tr("%1 minute%2 ago").arg(minutes).arg(minutes > 1 ? "s" : ""); } else if (diff < 60 * 60 * 24) { int hours = diff / (60 * 60); - s = QString(QObject::tr("%1 hour%2 ago")).arg(hours).arg(hours > 1 ? "s" : ""); + s = QObject::tr("%1 hour%2 ago").arg(hours).arg(hours > 1 ? "s" : ""); } else if (diff < 3600 * 24 * 7) { int days = diff / (60 * 60 * 24); - s = QString(QObject::tr("%1 day%2 ago")).arg(days).arg(days > 1 ? "s" : ""); + s = QObject::tr("%1 day%2 ago").arg(days).arg(days > 1 ? "s" : ""); } else { s = date.date().toString(); } diff --git a/selfdrive/ui/qt/widgets/ssh_keys.cc b/selfdrive/ui/qt/widgets/ssh_keys.cc index 1f48c72735ae7e..f17604b3e5e8da 100644 --- a/selfdrive/ui/qt/widgets/ssh_keys.cc +++ b/selfdrive/ui/qt/widgets/ssh_keys.cc @@ -47,13 +47,13 @@ void SshControl::getUserKeys(const QString &username) { params.put("GithubUsername", username.toStdString()); params.put("GithubSshKeys", resp.toStdString()); } else { - ConfirmationDialog::alert(QString(tr("Username '%1' has no keys on GitHub")).arg(username), this); + ConfirmationDialog::alert(tr("Username '%1' has no keys on GitHub").arg(username), this); } } else { if (request->timeout()) { ConfirmationDialog::alert(tr("Request timed out"), this); } else { - ConfirmationDialog::alert(QString(tr("Username '%1' doesn't exist on GitHub")).arg(username), this); + ConfirmationDialog::alert(tr("Username '%1' doesn't exist on GitHub").arg(username), this); } } From 867a1cf35a200c10c9ea513140b74be04ce4bb83 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 16:57:47 +0800 Subject: [PATCH 200/436] ui: replace line_vertices_data with QPolygonF (#25001) use QPolygonF use push_front --- selfdrive/ui/qt/onroad.cc | 6 +++--- selfdrive/ui/ui.cc | 20 +++++++------------- selfdrive/ui/ui.h | 12 ++++-------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 7032fc75184292..604d3c09a945f7 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -430,13 +430,13 @@ void NvgWindow::drawLaneLines(QPainter &painter, const UIState *s) { // lanelines for (int i = 0; i < std::size(scene.lane_line_vertices); ++i) { painter.setBrush(QColor::fromRgbF(1.0, 1.0, 1.0, std::clamp(scene.lane_line_probs[i], 0.0, 0.7))); - painter.drawPolygon(scene.lane_line_vertices[i].v, scene.lane_line_vertices[i].cnt); + painter.drawPolygon(scene.lane_line_vertices[i]); } // road edges for (int i = 0; i < std::size(scene.road_edge_vertices); ++i) { painter.setBrush(QColor::fromRgbF(1.0, 0, 0, std::clamp(1.0 - scene.road_edge_stds[i], 0.0, 1.0))); - painter.drawPolygon(scene.road_edge_vertices[i].v, scene.road_edge_vertices[i].cnt); + painter.drawPolygon(scene.road_edge_vertices[i]); } // paint path @@ -455,7 +455,7 @@ void NvgWindow::drawLaneLines(QPainter &painter, const UIState *s) { bg.setColorAt(0.75 / 1.5, QColor::fromHslF(curve_hue / 360., 1.0, 0.68, 0.35)); bg.setColorAt(1.0, QColor::fromHslF(curve_hue / 360., 1.0, 0.68, 0.0)); painter.setBrush(bg); - painter.drawPolygon(scene.track_vertices.v, scene.track_vertices.cnt); + painter.drawPolygon(scene.track_vertices); painter.restore(); } diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index c8fc645cf2031d..f6193f97a613de 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -55,10 +55,13 @@ static void update_leads(UIState *s, const cereal::RadarState::Reader &radar_sta } static void update_line_data(const UIState *s, const cereal::ModelDataV2::XYZTData::Reader &line, - float y_off, float z_off, line_vertices_data *pvd, int max_idx, bool allow_invert=true) { + float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert=true) { const auto line_x = line.getX(), line_y = line.getY(), line_z = line.getZ(); - std::vector left_points, right_points; + QPolygonF left_points, right_points; + left_points.reserve(max_idx + 1); + right_points.reserve(max_idx + 1); + for (int i = 0; i <= max_idx; i++) { QPointF left, right; bool l = calib_frame_to_full_frame(s, line_x[i], line_y[i] - y_off, line_z[i] + z_off, &left); @@ -69,19 +72,10 @@ static void update_line_data(const UIState *s, const cereal::ModelDataV2::XYZTDa continue; } left_points.push_back(left); - right_points.push_back(right); + right_points.push_front(right); } } - - pvd->cnt = 2 * left_points.size(); - assert(left_points.size() == right_points.size()); - assert(pvd->cnt <= std::size(pvd->v)); - - for (int left_idx = 0; left_idx < left_points.size(); left_idx++){ - int right_idx = 2 * left_points.size() - left_idx - 1; - pvd->v[left_idx] = left_points[left_idx]; - pvd->v[right_idx] = right_points[left_idx]; - } + *pvd = left_points + right_points; } static void update_model(UIState *s, const cereal::ModelDataV2::Reader &model) { diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 7364b81a40833b..1aee3df9a13fc1 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "cereal/messaging/messaging.h" @@ -84,11 +85,6 @@ const QColor bg_colors [] = { [STATUS_ALERT] = QColor(0xC9, 0x22, 0x31, 0xf1), }; -typedef struct { - QPointF v[TRAJECTORY_SIZE * 2]; - int cnt; -} line_vertices_data; - typedef struct UIScene { bool calibration_valid = false; mat3 view_from_calib = DEFAULT_CALIBRATION; @@ -97,9 +93,9 @@ typedef struct UIScene { // modelV2 float lane_line_probs[4]; float road_edge_stds[2]; - line_vertices_data track_vertices; - line_vertices_data lane_line_vertices[4]; - line_vertices_data road_edge_vertices[2]; + QPolygonF track_vertices; + QPolygonF lane_line_vertices[4]; + QPolygonF road_edge_vertices[2]; // lead QPointF lead_vertices[2]; From b3f4e94169f459fc5cd6ad45fd69baffdef838da Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 16:58:37 +0800 Subject: [PATCH 201/436] remove selfdrive/common (#24997) --- selfdrive/common/tests/.gitignore | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 selfdrive/common/tests/.gitignore diff --git a/selfdrive/common/tests/.gitignore b/selfdrive/common/tests/.gitignore deleted file mode 100644 index 1350b3b825c050..00000000000000 --- a/selfdrive/common/tests/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -test_util -test_swaglog From 1ac1fe632f8945a24c12413cfad7aa93fac17f9b Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 16:59:08 +0800 Subject: [PATCH 202/436] swaglog.cc/cloudlog_common: pass json object by reference (#24996) * pass json object by reference * space between functions --- common/swaglog.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/swaglog.cc b/common/swaglog.cc index 6b0028326a7872..22682dc54c57b6 100644 --- a/common/swaglog.cc +++ b/common/swaglog.cc @@ -66,8 +66,9 @@ static void log(int levelnum, const char* filename, int lineno, const char* func char levelnum_c = levelnum; zmq_send(s.sock, (levelnum_c + log_s).c_str(), log_s.length() + 1, ZMQ_NOBLOCK); } + static void cloudlog_common(int levelnum, const char* filename, int lineno, const char* func, - char* msg_buf, json11::Json::object msg_j={}) { + char* msg_buf, const json11::Json::object &msg_j={}) { std::lock_guard lk(s.lock); if (!s.initialized) s.initialize(); From 7178800d844469601cb779d8f76b568ff82ac766 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 30 Jun 2022 02:02:53 -0700 Subject: [PATCH 203/436] Multilang prerequisites (#24999) * some supporting code for multilang * for now just english * test for missing language files * test for checking if ts file is up to date * Skip test if causes exception, other test catches this Test test Should also work should now fail revert rmn * add to files_common * fix files_common * newlines * no need to not update * comment * only english * double quotes * switch around --- .github/workflows/selfdrive_tests.yaml | 1 + release/files_common | 7 ++-- selfdrive/ui/tests/test_translations.py | 50 ++++++++++++++++++++++++ selfdrive/ui/translations/languages.json | 3 ++ selfdrive/ui/update_translations.py | 37 ++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100755 selfdrive/ui/tests/test_translations.py create mode 100644 selfdrive/ui/translations/languages.json create mode 100755 selfdrive/ui/update_translations.py diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index f5edfe59299e75..0ff0092b02cbcf 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -307,6 +307,7 @@ jobs: $UNIT_TEST tools/lib/tests && \ ./selfdrive/ui/tests/create_test_translations.sh && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ + ./selfdrive/ui/tests/test_translations.py && \ ./common/tests/test_util && \ ./common/tests/test_swaglog && \ ./selfdrive/boardd/tests/test_boardd_usbprotocol && \ diff --git a/release/files_common b/release/files_common index 7e8dbd37a63609..acf74e21375be4 100644 --- a/release/files_common +++ b/release/files_common @@ -60,6 +60,8 @@ release/* tools/lib/* tools/joystick/* +tools/replay/*.cc +tools/replay/*.h selfdrive/__init__.py selfdrive/sentry.py @@ -287,6 +289,7 @@ selfdrive/ui/soundd/*.cc selfdrive/ui/soundd/*.h selfdrive/ui/soundd/soundd selfdrive/ui/soundd/.gitignore +selfdrive/ui/translations/* selfdrive/ui/qt/*.cc selfdrive/ui/qt/*.h @@ -295,10 +298,6 @@ selfdrive/ui/qt/offroad/*.h selfdrive/ui/qt/offroad/*.qml selfdrive/ui/qt/widgets/*.cc selfdrive/ui/qt/widgets/*.h - -tools/replay/*.cc -tools/replay/*.h - selfdrive/ui/qt/maps/*.cc selfdrive/ui/qt/maps/*.h diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py new file mode 100755 index 00000000000000..ccea748e24054e --- /dev/null +++ b/selfdrive/ui/tests/test_translations.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import json +import os +import unittest + +from selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations + + +class TestTranslations(unittest.TestCase): + @classmethod + def setUpClass(cls): + with open(LANGUAGES_FILE, "r") as f: + cls.translation_files = json.load(f) + + def test_missing_translation_files(self): + for name, file in self.translation_files.items(): + with self.subTest(name=name, file=file): + if not len(file): + self.skipTest(f"{name} translation has no file") + + self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")), + f"{name} has no XML translation file, run selfdrive/ui/update_translations.py") + self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.qm")), + f"{name} has no compiled QM translation file, run selfdrive/ui/update_translations.py --release") + + def test_translations_updated(self): + suffix = "_test" + update_translations(suffix=suffix) + + for name, file in self.translation_files.items(): + with self.subTest(name=name, file=file): + cur_tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}.ts") + new_tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}{suffix}.ts") + + if not len(file): + self.skipTest(f"{name} translation has no file") + elif not os.path.exists(cur_tr_file): + self.skipTest(f"{name} missing translation file") # caught by test_missing_translation_files + + with open(cur_tr_file, "r") as f: + cur_translations = f.read() + with open(new_tr_file, "r") as f: + new_translations = f.read() + + self.assertEqual(cur_translations, new_translations, + f"{name} translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") + + +if __name__ == "__main__": + unittest.main() diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json new file mode 100644 index 00000000000000..f2f9400d6412f5 --- /dev/null +++ b/selfdrive/ui/translations/languages.json @@ -0,0 +1,3 @@ +{ + "English": "" +} diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py new file mode 100755 index 00000000000000..5d57fa39d245bc --- /dev/null +++ b/selfdrive/ui/update_translations.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import argparse +import os +import json + +from common.basedir import BASEDIR + +UI_DIR = os.path.join(BASEDIR, "selfdrive", "ui") +TRANSLATIONS_DIR = os.path.join(UI_DIR, "translations") +LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") + + +def update_translations(release=False, suffix=""): + with open(LANGUAGES_FILE, "r") as f: + translation_files = json.load(f) + + for name, file in translation_files.items(): + if not len(file): + print(f"{name} has no translation file, skipping...") + continue + + tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}{suffix}.ts") + ret = os.system(f"lupdate -recursive {UI_DIR} -ts {tr_file}") + assert ret == 0 + + if release: + ret = os.system(f"lrelease {tr_file}") + assert ret == 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update translation files for UI", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--release", action="store_true", help="Create compiled QM translation files used by UI") + args = parser.parse_args() + + update_translations(args.release) From e20d2cfa9b80e41c6af3b7879c15c6df1cfb81c1 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 17:05:49 +0800 Subject: [PATCH 204/436] Camerad: small cleanup (#24992) * remove CameraExpInfo * remove release callback --- system/camerad/cameras/camera_common.cc | 7 ++----- system/camerad/cameras/camera_common.h | 10 +--------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 34dde9389c55ef..506b3fb995d003 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -66,11 +66,10 @@ class Debayer { bool hdr_; }; -void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType init_rgb_type, VisionStreamType init_yuv_type, release_cb init_release_callback) { +void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType init_rgb_type, VisionStreamType init_yuv_type) { vipc_server = v; this->rgb_type = init_rgb_type; this->yuv_type = init_yuv_type; - this->release_callback = init_release_callback; const CameraInfo *ci = &s->ci; camera_state = s; @@ -169,9 +168,7 @@ bool CameraBuf::acquire() { } void CameraBuf::release() { - if (release_callback) { - release_callback((void*)camera_state, cur_buf_idx); - } + // Empty } void CameraBuf::queue(size_t buf_idx) { diff --git a/system/camerad/cameras/camera_common.h b/system/camerad/cameras/camera_common.h index 6b483372bb70a7..4695d4e2c92066 100644 --- a/system/camerad/cameras/camera_common.h +++ b/system/camerad/cameras/camera_common.h @@ -48,8 +48,6 @@ const bool env_disable_driver = getenv("DISABLE_DRIVER") != NULL; const bool env_debug_frames = getenv("DEBUG_FRAMES") != NULL; const bool env_log_raw_frames = getenv("LOG_RAW_FRAMES") != NULL; -typedef void (*release_cb)(void *cookie, int buf_idx); - typedef struct CameraInfo { uint32_t frame_width, frame_height; uint32_t frame_stride; @@ -85,11 +83,6 @@ typedef struct FrameMetadata { float processing_time; } FrameMetadata; -typedef struct CameraExpInfo { - int op_id; - float grey_frac; -} CameraExpInfo; - struct MultiCameraState; struct CameraState; class Debayer; @@ -108,7 +101,6 @@ class CameraBuf { SafeQueue safe_queue; int frame_buf_count; - release_cb release_callback; public: cl_command_queue q; @@ -124,7 +116,7 @@ class CameraBuf { CameraBuf() = default; ~CameraBuf(); - void init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType rgb_type, VisionStreamType yuv_type, release_cb release_callback=nullptr); + void init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType rgb_type, VisionStreamType yuv_type); bool acquire(); void release(); void queue(size_t buf_idx); From d13137a83f2036d436c4bc56b4a65fa6962b519d Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 30 Jun 2022 17:29:25 +0800 Subject: [PATCH 205/436] camerad: reduce cpu usage (#24993) wait for 50ms --- system/camerad/cameras/camera_common.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 506b3fb995d003..04f3136485f603 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -126,7 +126,7 @@ CameraBuf::~CameraBuf() { } bool CameraBuf::acquire() { - if (!safe_queue.try_pop(cur_buf_idx, 1)) return false; + if (!safe_queue.try_pop(cur_buf_idx, 50)) return false; if (camera_bufs_metadata[cur_buf_idx].frame_id == -1) { LOGE("no frame data? wtf"); From 30ddadc8b4d510c824221d2010e4aef853741a92 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 30 Jun 2022 13:25:42 +0200 Subject: [PATCH 206/436] test onroad: lower camerad cpu usage --- selfdrive/test/test_onroad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index f10c79313b124c..a7ab55fe2a58bb 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -24,7 +24,7 @@ "selfdrive.controls.controlsd": 35.0, "./loggerd": 10.0, "./encoderd": 12.5, - "./camerad": 16.5, + "./camerad": 14.5, "./locationd": 9.1, "selfdrive.controls.plannerd": 11.7, "./_ui": 19.2, From af7d3c115a4780ad734c3f3a8d0084dd506aefec Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 30 Jun 2022 14:22:55 +0200 Subject: [PATCH 207/436] cameraview.cc: prev_frame_id static -> class member --- selfdrive/ui/qt/widgets/cameraview.cc | 7 +++---- selfdrive/ui/qt/widgets/cameraview.h | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index 0bc90b530719c1..305f8a3abb91cd 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -243,13 +243,12 @@ void CameraViewWidget::paintGL() { // } // Log duplicate/dropped frames - static int prev_id = 0; - if (frames[frame_idx].first == prev_id) { + if (frames[frame_idx].first == prev_frame_id) { qInfo() << "Drawing same frame twice" << frames[frame_idx].first; - } else if (frames[frame_idx].first != prev_id + 1) { + } else if (frames[frame_idx].first != prev_frame_id + 1) { qInfo() << "Skipped frame" << frames[frame_idx].first; } - prev_id = frames[frame_idx].first; + prev_frame_id = frames[frame_idx].first; glViewport(0, 0, width(), height()); glBindVertexArray(frame_vao); diff --git a/selfdrive/ui/qt/widgets/cameraview.h b/selfdrive/ui/qt/widgets/cameraview.h index cc11ec2c277d0e..016522b05c275a 100644 --- a/selfdrive/ui/qt/widgets/cameraview.h +++ b/selfdrive/ui/qt/widgets/cameraview.h @@ -78,6 +78,7 @@ class CameraViewWidget : public QOpenGLWidget, protected QOpenGLFunctions { std::deque> frames; uint32_t draw_frame_id = 0; + int prev_frame_id = 0; protected slots: void vipcConnected(VisionIpcClient *vipc_client); From ca800da8951f83892a3593b2203e74be6af53844 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 30 Jun 2022 14:25:32 +0200 Subject: [PATCH 208/436] cameraview.cc: qInfo -> qDebug --- selfdrive/ui/qt/widgets/cameraview.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/qt/widgets/cameraview.cc b/selfdrive/ui/qt/widgets/cameraview.cc index 305f8a3abb91cd..63d15660a08607 100644 --- a/selfdrive/ui/qt/widgets/cameraview.cc +++ b/selfdrive/ui/qt/widgets/cameraview.cc @@ -244,9 +244,9 @@ void CameraViewWidget::paintGL() { // Log duplicate/dropped frames if (frames[frame_idx].first == prev_frame_id) { - qInfo() << "Drawing same frame twice" << frames[frame_idx].first; + qDebug() << "Drawing same frame twice" << frames[frame_idx].first; } else if (frames[frame_idx].first != prev_frame_id + 1) { - qInfo() << "Skipped frame" << frames[frame_idx].first; + qDebug() << "Skipped frame" << frames[frame_idx].first; } prev_frame_id = frames[frame_idx].first; From c49f997be505760a18e85a241e6d45b98952c894 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 30 Jun 2022 17:39:12 +0200 Subject: [PATCH 209/436] Add laikadOffline subtest to process replay. (#24995) * Add subtests to process replay. Adds laikadOffline subtest * Update cpp. * Update ref * Update ref again * Update ref again * update ref * Fix disabling fetching orbits * Add proc name to event exception * update ref * Update setup_env * Fix offline test and update refs --- selfdrive/locationd/laikad.py | 15 ++++--- .../test/process_replay/process_replay.py | 41 +++++++++++++++---- selfdrive/test/process_replay/ref_commit | 2 +- .../test/process_replay/test_processes.py | 4 +- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 5098b25d38f1b7..d51ac10816454b 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import math import os import time from collections import defaultdict @@ -29,11 +30,12 @@ class Laikad: - def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), + def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), save_ephemeris=False, last_known_position=None): self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) self.gnss_kf = GNSSKalman(GENERATED_DIR, cython=True) + self.auto_fetch_orbits = auto_fetch_orbits self.orbit_fetch_executor: Optional[ProcessPoolExecutor] = None self.orbit_fetch_future: Optional[Future] = None @@ -41,6 +43,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_update=False, valid_ephe self.last_cached_t = None self.save_ephemeris = save_ephemeris self.load_cache() + self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} self.last_pos_fix = last_known_position if last_known_position is not None else [] self.last_pos_residual = [] @@ -85,7 +88,8 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): report = ublox_msg.measurementReport if report.gpsWeek > 0: latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) - self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) + if self.auto_fetch_orbits: + self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) new_meas = read_raw_ublox(report) processed_measurements = process_measurements(new_meas, self.astro_dog) @@ -146,8 +150,8 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement def kf_valid(self, t: float) -> List[bool]: filter_time = self.gnss_kf.filter.get_filter_time() - return [filter_time is not None, - filter_time is not None and abs(t - filter_time) < MAX_TIME_GAP, + return [not math.isnan(filter_time), + abs(t - filter_time) < MAX_TIME_GAP, all(np.isfinite(self.gnss_kf.x[GStates.ECEF_POS]))] def init_gnss_localizer(self, est_pos): @@ -275,7 +279,8 @@ def main(sm=None, pm=None): replay = "REPLAY" in os.environ # todo get last_known_position - laikad = Laikad(save_ephemeris=not replay) + use_internet = "LAIKAD_NO_INTERNET" not in os.environ + laikad = Laikad(save_ephemeris=not replay, auto_fetch_orbits=use_internet) while True: sm.update() diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index def61a10a19951..fc83b4b61f4461 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -27,14 +27,14 @@ PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'init_callback', 'should_recv_callback', 'tolerance', 'fake_pubsubmaster', 'submaster_config'], defaults=({},)) +ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'init_callback', 'should_recv_callback', 'tolerance', 'fake_pubsubmaster', 'submaster_config', 'environ', 'subtest_name'], defaults=({}, {}, "")) def wait_for_event(evt): if not evt.wait(TIMEOUT): if threading.currentThread().getName() == "MainThread": # tested process likely died. don't let test just hang - raise Exception("Timeout reached. Tested process likely crashed.") + raise Exception(f"Timeout reached. Tested process {os.environ['PROC_NAME']} likely crashed.") else: # done testing this process, let it die sys.exit(0) @@ -190,6 +190,7 @@ def get_car_params(msgs, fsm, can_sock, fingerprint): _, CP = get_car(can, sendcan) Params().put("CarParams", CP.to_bytes()) + def controlsd_rcv_callback(msg, CP, cfg, fsm): # no sendcan until controlsd is initialized socks = [s for s in cfg.pub_sub[msg.which()] if @@ -198,6 +199,7 @@ def controlsd_rcv_callback(msg, CP, cfg, fsm): socks.remove("sendcan") return socks, len(socks) > 0 + def radar_rcv_callback(msg, CP, cfg, fsm): if msg.which() != "can": return [], False @@ -240,7 +242,7 @@ def laika_rcv_callback(msg, CP, cfg, fsm): if msg.ubloxGnss.which() == "measurementReport": return ["gnssMeasurements"], True else: - return [], False + return [], True CONFIGS = [ @@ -345,6 +347,19 @@ def laika_rcv_callback(msg, CP, cfg, fsm): tolerance=None, fake_pubsubmaster=False, ), + ProcessConfig( + proc_name="laikad", + subtest_name="Offline", + pub_sub={ + "ubloxGnss": ["gnssMeasurements"], + }, + ignore=["logMonoTime"], + init_callback=get_car_params, + should_recv_callback=laika_rcv_callback, + tolerance=NUMPY_TOLERANCE, + fake_pubsubmaster=True, + environ={"LAIKAD_NO_INTERNET": "1"}, + ), ProcessConfig( proc_name="laikad", pub_sub={ @@ -366,7 +381,8 @@ def replay_process(cfg, lr, fingerprint=None): else: return cpp_replay_process(cfg, lr, fingerprint) -def setup_env(simulation=False, CP=None): + +def setup_env(simulation=False, CP=None, cfg=None): params = Params() params.clear_all() params.put_bool("OpenpilotEnabledToggle", True) @@ -380,6 +396,16 @@ def setup_env(simulation=False, CP=None): os.environ['SKIP_FW_QUERY'] = "" os.environ['FINGERPRINT'] = "" + if cfg is not None: + # Clear all custom processConfig environment variables + for cfg in CONFIGS: + for k, _ in cfg.environ.items(): + if k in os.environ: + del os.environ[k] + + os.environ.update(cfg.environ) + os.environ['PROC_NAME'] = cfg.proc_name + if simulation: os.environ["SIMULATION"] = "1" elif "SIMULATION" in os.environ: @@ -396,6 +422,7 @@ def setup_env(simulation=False, CP=None): os.environ['SKIP_FW_QUERY'] = "1" os.environ['FINGERPRINT'] = CP.carFingerprint + def python_replay_process(cfg, lr, fingerprint=None): sub_sockets = [s for _, sub in cfg.pub_sub.items() for s in sub] pub_sockets = [s for s in cfg.pub_sub.keys() if s != 'can'] @@ -413,10 +440,10 @@ def python_replay_process(cfg, lr, fingerprint=None): if fingerprint is not None: os.environ['SKIP_FW_QUERY'] = "1" os.environ['FINGERPRINT'] = fingerprint - setup_env() + setup_env(cfg=cfg) else: CP = [m for m in lr if m.which() == 'carParams'][0].carParams - setup_env(CP=CP) + setup_env(CP=CP, cfg=cfg) assert(type(managed_processes[cfg.proc_name]) is PythonProcess) managed_processes[cfg.proc_name].prepare() @@ -477,7 +504,7 @@ def cpp_replay_process(cfg, lr, fingerprint=None): log_msgs = [] # We need to fake SubMaster alive since we can't inject a fake clock - setup_env(simulation=True) + setup_env(simulation=True, cfg=cfg) managed_processes[cfg.proc_name].prepare() managed_processes[cfg.proc_name].start() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 946ddce19e5451..c3e8eca42d08c2 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a0b5ce7b2e0b9c073e51ac8908402d53e1d99722 \ No newline at end of file +a9adebff7ce27d6233d443217a30337b761898ee \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 25fbd210cc0a72..4e7ba4a6ddcd19 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -200,11 +200,11 @@ def format_diff(results, ref_commit): if cfg.proc_name not in tested_procs: continue - cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{cur_commit}.bz2") + cur_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}{cfg.subtest_name}_{cur_commit}.bz2") if args.update_refs: # reference logs will not exist if routes were just regenerated ref_log_path = get_url(*segment.rsplit("--", 1)) else: - ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}_{ref_commit}.bz2") + ref_log_fn = os.path.join(FAKEDATA, f"{segment}_{cfg.proc_name}{cfg.subtest_name}_{ref_commit}.bz2") ref_log_path = ref_log_fn if os.path.exists(ref_log_fn) else BASE_URL + os.path.basename(ref_log_fn) dat = None if args.upload_only else log_data[segment] From dd43ae2856e144ec13eff462556d8e681aa57789 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Thu, 30 Jun 2022 14:49:17 -0700 Subject: [PATCH 210/436] Full localizer: Use standard naming conventions (#25007) Use standard naming conventions --- selfdrive/locationd/models/loc_kf.py | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/selfdrive/locationd/models/loc_kf.py b/selfdrive/locationd/models/loc_kf.py index 6b08828695a4eb..b1ed8599f1e08b 100755 --- a/selfdrive/locationd/models/loc_kf.py +++ b/selfdrive/locationd/models/loc_kf.py @@ -25,15 +25,15 @@ class States(): ODO_SCALE_UNUSED = slice(18, 19) # odometer scale ACCELERATION = slice(19, 22) # Acceleration in device frame in m/s**2 FOCAL_SCALE_UNUSED = slice(22, 23) # focal length scale - IMU_OFFSET = slice(23, 26) # imu offset angles in radians + IMU_FROM_DEVICE_EULER = slice(23, 26) # imu offset angles in radians GLONASS_BIAS = slice(26, 27) # GLONASS bias in m expressed as bias + freq_num*freq_slope GLONASS_FREQ_SLOPE = slice(27, 28) # GLONASS bias in m expressed as bias + freq_num*freq_slope CLOCK_ACCELERATION = slice(28, 29) # clock acceleration in light-meters/s**2, ACCELEROMETER_SCALE_UNUSED = slice(29, 30) # scale of mems accelerometer ACCELEROMETER_BIAS = slice(30, 33) # bias of mems accelerometer # TODO the offset is likely a translation of the sensor, not a rotation of the camera - WIDE_CAM_OFFSET = slice(33, 36) # wide camera offset angles in radians (tici only) - # We curently do not use ACCELEROMETER_SCALE to avoid instability due to too many free variables (ACCELEROMETER_SCALE, ACCELEROMETER_BIAS, IMU_OFFSET). + WIDE_FROM_DEVICE_EULER = slice(33, 36) # wide camera offset angles in radians (tici only) + # We curently do not use ACCELEROMETER_SCALE to avoid instability due to too many free variables (ACCELEROMETER_SCALE, ACCELEROMETER_BIAS, IMU_FROM_DEVICE_EULER). # From experiments we see that ACCELEROMETER_BIAS is more correct than ACCELEROMETER_SCALE # Error-state has different slices because it is an ESKF @@ -47,13 +47,13 @@ class States(): ODO_SCALE_ERR_UNUSED = slice(17, 18) ACCELERATION_ERR = slice(18, 21) FOCAL_SCALE_ERR_UNUSED = slice(21, 22) - IMU_OFFSET_ERR = slice(22, 25) + IMU_FROM_DEVICE_EULER_ERR = slice(22, 25) GLONASS_BIAS_ERR = slice(25, 26) GLONASS_FREQ_SLOPE_ERR = slice(26, 27) CLOCK_ACCELERATION_ERR = slice(27, 28) ACCELEROMETER_SCALE_ERR_UNUSED = slice(28, 29) ACCELEROMETER_BIAS_ERR = slice(29, 32) - WIDE_CAM_OFFSET_ERR = slice(32, 35) + WIDE_FROM_DEVICE_EULER_ERR = slice(32, 35) class LocKalman(): @@ -140,15 +140,15 @@ def generate_code(generated_dir, N=4): cd = state[States.CLOCK_DRIFT, :] roll_bias, pitch_bias, yaw_bias = state[States.GYRO_BIAS, :] acceleration = state[States.ACCELERATION, :] - imu_angles = state[States.IMU_OFFSET, :] - imu_angles[0, 0] = 0 # not observable enough - imu_angles[2, 0] = 0 # not observable enough + imu_from_device_euler = state[States.IMU_FROM_DEVICE_EULER, :] + imu_from_device_euler[0, 0] = 0 # not observable enough + imu_from_device_euler[2, 0] = 0 # not observable enough glonass_bias = state[States.GLONASS_BIAS, :] glonass_freq_slope = state[States.GLONASS_FREQ_SLOPE, :] ca = state[States.CLOCK_ACCELERATION, :] accel_bias = state[States.ACCELEROMETER_BIAS, :] - wide_cam_angles = state[States.WIDE_CAM_OFFSET, :] - wide_cam_angles[0, 0] = 0 # not observable enough + wide_from_device_euler = state[States.WIDE_FROM_DEVICE_EULER, :] + wide_from_device_euler[0, 0] = 0 # not observable enough dt = sp.Symbol('dt') @@ -273,15 +273,15 @@ def generate_code(generated_dir, N=4): los_vector[2] * (sat_vz - vz) + cd[0]]) - imu_rot = euler_rotate(*imu_angles) - h_gyro_sym = imu_rot * sp.Matrix([vroll + roll_bias, + imu_from_device = euler_rotate(*imu_from_device_euler) + h_gyro_sym = imu_from_device * sp.Matrix([vroll + roll_bias, vpitch + pitch_bias, vyaw + yaw_bias]) pos = sp.Matrix([x, y, z]) # add 1 for stability, prevent division by 0 gravity = quat_rot.T * ((EARTH_GM / ((x**2 + y**2 + z**2 + 1)**(3.0 / 2.0))) * pos) - h_acc_sym = imu_rot * (gravity + acceleration + accel_bias) + h_acc_sym = imu_from_device * (gravity + acceleration + accel_bias) h_acc_stationary_sym = acceleration h_phone_rot_sym = sp.Matrix([vroll, vpitch, vyaw]) h_relative_motion = sp.Matrix(quat_rot.T * v) @@ -297,7 +297,7 @@ def generate_code(generated_dir, N=4): [h_phone_rot_sym, ObservationKind.CAMERA_ODO_ROTATION, None], [h_acc_stationary_sym, ObservationKind.NO_ACCEL, None]] - wide_cam_rot = euler_rotate(*wide_cam_angles) + wide_from_device = euler_rotate(*wide_from_device_euler) # MSCKF configuration if N > 0: # experimentally found this is correct value for imx298 with 910 focal length @@ -312,7 +312,7 @@ def generate_code(generated_dir, N=4): track_pos_sym = sp.Matrix([track_x - x, track_y - y, track_z - z]) track_pos_rot_sym = quat_rot.T * track_pos_sym - track_pos_rot_wide_cam_sym = wide_cam_rot * track_pos_rot_sym + track_pos_rot_wide_cam_sym = wide_from_device * track_pos_rot_sym h_track_sym[-2:, :] = sp.Matrix([focal_scale * (track_pos_rot_sym[1] / track_pos_rot_sym[0]), focal_scale * (track_pos_rot_sym[2] / track_pos_rot_sym[0])]) h_track_wide_cam_sym[-2:, :] = sp.Matrix([focal_scale * (track_pos_rot_wide_cam_sym[1] / track_pos_rot_wide_cam_sym[0]), @@ -329,7 +329,7 @@ def generate_code(generated_dir, N=4): quat_rot = quat_rotate(*q) track_pos_sym = sp.Matrix([track_x - x, track_y - y, track_z - z]) track_pos_rot_sym = quat_rot.T * track_pos_sym - track_pos_rot_wide_cam_sym = wide_cam_rot * track_pos_rot_sym + track_pos_rot_wide_cam_sym = wide_from_device * track_pos_rot_sym h_track_sym[n * 2:n * 2 + 2, :] = sp.Matrix([focal_scale * (track_pos_rot_sym[1] / track_pos_rot_sym[0]), focal_scale * (track_pos_rot_sym[2] / track_pos_rot_sym[0])]) h_track_wide_cam_sym[n * 2: n * 2 + 2, :] = sp.Matrix([focal_scale * (track_pos_rot_wide_cam_sym[1] / track_pos_rot_wide_cam_sym[0]), From f0062f624260bcbdadeec643c993ac597b73c4fc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 30 Jun 2022 15:01:52 -0700 Subject: [PATCH 211/436] Toyota: fix BSM detection (#24964) * revert to 1 second * Revert "revert to 1 second" This reverts commit 6ab3f75cb73fdfe254431c479b6d337030b0d538. * Revert "remove toyota can fingerprinting exceptions (#22803)" This reverts commit d8f5e8b7a4b6c9132ce2515a9106aeb971ee2579. fix static analysis * Revert "Revert "remove toyota can fingerprinting exceptions (#22803)"" This reverts commit fc359fc9b2db146d77b6533be9e7434f08a665df. * 1 second for all brands * update refs Co-authored-by: Adeeb Shihadeh --- selfdrive/car/car_helpers.py | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 8c0fd7c90d6cdd..7f83732153acc1 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -115,7 +115,7 @@ def fingerprint(logcan, sendcan): finger = gen_empty_fingerprint() candidate_cars = {i: all_legacy_fingerprint_cars() for i in [0, 1]} # attempt fingerprint on both bus 0 and 1 frame = 0 - frame_fingerprint = 25 # 0.25s + frame_fingerprint = 100 # 1s car_fingerprint = None done = False diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index c3e8eca42d08c2..b9ba47e8f746ad 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a9adebff7ce27d6233d443217a30337b761898ee \ No newline at end of file +a98dfc72bb4c5624c2223ca65d52b151f419460c \ No newline at end of file From 2027d5311d419addfb67ad94d45a198592b2e459 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Thu, 30 Jun 2022 15:03:15 -0700 Subject: [PATCH 212/436] tools: add support for nv12 in compressed_vipc (#24962) reshape for nv12 --- tools/camerastream/compressed_vipc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index 290610e45f7d1c..d321d6fd2b4286 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -73,6 +73,10 @@ def decoder(addr, sock_name, vipc_server, vst, nvidia): continue assert len(frames) == 1 img_yuv = frames[0].to_ndarray(format=av.video.format.VideoFormat('yuv420p')).flatten() + uv_offset = H*W + y = img_yuv[:uv_offset] + uv = img_yuv[uv_offset:].reshape(2, -1).ravel('F') + img_yuv = np.hstack((y, uv)) vipc_server.send(vst, img_yuv.data, cnt, int(time_q[0]*1e9), int(time.monotonic()*1e9)) cnt += 1 From 8b32e1b060e8c2c3c084393a569d4e78992ca598 Mon Sep 17 00:00:00 2001 From: Jason Shuler Date: Thu, 30 Jun 2022 18:26:25 -0400 Subject: [PATCH 213/436] GM: Lower LKA loopback CAN Error timing threshold to accommodate dropped packets (#24927) * LKA loopback timing to 10Hz * Typo Co-authored-by: Willem Melching Co-authored-by: Willem Melching --- selfdrive/car/gm/carstate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index 48b9f25444189f..3605dab48663ff 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -133,7 +133,9 @@ def get_loopback_can_parser(CP): ] checks = [ - ("ASCMLKASteeringCmd", 50), + ("ASCMLKASteeringCmd", 10), # 10 Hz is the stock inactive rate (every 100ms). + # While active 50 Hz (every 20 ms) is normal + # EPS will tolerate around 200ms when active before faulting ] return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, CanBus.LOOPBACK) From 11b5d51da61cac111fb4e5af3b1df6091d860dfc Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 30 Jun 2022 15:36:40 -0700 Subject: [PATCH 214/436] remove mypy ignore from a few scripts --- selfdrive/debug/check_freq.py | 11 +++++++---- selfdrive/debug/check_lag.py | 4 ++-- selfdrive/debug/check_timings.py | 5 +++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/selfdrive/debug/check_freq.py b/selfdrive/debug/check_freq.py index 424ad67b6dcef8..b6f3c91bd09a31 100755 --- a/selfdrive/debug/check_freq.py +++ b/selfdrive/debug/check_freq.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -# type: ignore - import argparse import numpy as np from collections import defaultdict, deque +from typing import DefaultDict, Deque + from common.realtime import sec_since_boot import cereal.messaging as messaging @@ -19,8 +19,8 @@ socket_names = args.socket sockets = {} - rcv_times = defaultdict(lambda: deque(maxlen=100)) - valids = defaultdict(lambda: deque(maxlen=100)) + rcv_times: DefaultDict[str, Deque[float]] = defaultdict(lambda: deque(maxlen=100)) + valids: DefaultDict[str, Deque[bool]] = defaultdict(lambda: deque(maxlen=100)) t = sec_since_boot() for name in socket_names: @@ -31,6 +31,9 @@ while True: for socket in poller.poll(100): msg = messaging.recv_one(socket) + if msg is None: + continue + name = msg.which() t = sec_since_boot() diff --git a/selfdrive/debug/check_lag.py b/selfdrive/debug/check_lag.py index c922642982aa06..141156db913934 100755 --- a/selfdrive/debug/check_lag.py +++ b/selfdrive/debug/check_lag.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# type: ignore +from typing import Dict import cereal.messaging as messaging from cereal.services import service_list @@ -10,7 +10,7 @@ if __name__ == "__main__": sm = messaging.SubMaster(TO_CHECK) - prev_t = {} + prev_t: Dict[str, float] = {} while True: sm.update() diff --git a/selfdrive/debug/check_timings.py b/selfdrive/debug/check_timings.py index 03e39fd70df462..69304f97b56ff1 100755 --- a/selfdrive/debug/check_timings.py +++ b/selfdrive/debug/check_timings.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 -# type: ignore + import sys import time import numpy as np +from typing import DefaultDict, Deque from collections import defaultdict, deque import cereal.messaging as messaging socks = {s: messaging.sub_sock(s, conflate=False) for s in sys.argv[1:]} -ts = defaultdict(lambda: deque(maxlen=100)) +ts: DefaultDict[str, Deque[float]] = defaultdict(lambda: deque(maxlen=100)) if __name__ == "__main__": while True: From 9279c02258e7ab2f60b4e5ea0198bdcb757bf533 Mon Sep 17 00:00:00 2001 From: Jason Shuler Date: Thu, 30 Jun 2022 18:47:26 -0400 Subject: [PATCH 215/436] GM: prep and cleanup for future ports (#24910) * Interface radarOffCan set, comments * pass pcmCruise value to common events * add transType and networkLoc to iface * carstate use transtype to detect EV * ctrl: limit sends by config * Add clarifying comments for new vals * clean up * comment on new line * these have the same frequency * remove 25hz * add to upper comment * update refs * update refs * move into same block move into same block Co-authored-by: Shane Smiskol --- selfdrive/car/gm/carcontroller.py | 94 ++++++++++++------------ selfdrive/car/gm/carstate.py | 8 +- selfdrive/car/gm/interface.py | 19 +++-- selfdrive/test/process_replay/ref_commit | 2 +- 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/selfdrive/car/gm/carcontroller.py b/selfdrive/car/gm/carcontroller.py index ae2a188e3f6787..8ad5049e3210a5 100644 --- a/selfdrive/car/gm/carcontroller.py +++ b/selfdrive/car/gm/carcontroller.py @@ -8,10 +8,12 @@ from selfdrive.car.gm.values import DBC, CanBus, CarControllerParams VisualAlert = car.CarControl.HUDControl.VisualAlert +NetworkLocation = car.CarParams.NetworkLocation class CarController: def __init__(self, dbc_name, CP, VM): + self.CP = CP self.start_time = 0. self.apply_steer_last = 0 self.apply_gas = 0 @@ -24,9 +26,9 @@ def __init__(self, dbc_name, CP, VM): self.params = CarControllerParams() - self.packer_pt = CANPacker(DBC[CP.carFingerprint]['pt']) - self.packer_obj = CANPacker(DBC[CP.carFingerprint]['radar']) - self.packer_ch = CANPacker(DBC[CP.carFingerprint]['chassis']) + self.packer_pt = CANPacker(DBC[self.CP.carFingerprint]['pt']) + self.packer_obj = CANPacker(DBC[self.CP.carFingerprint]['radar']) + self.packer_ch = CANPacker(DBC[self.CP.carFingerprint]['chassis']) def update(self, CC, CS): actuators = CC.actuators @@ -60,48 +62,48 @@ def update(self, CC, CS): can_sends.append(gmcan.create_steering_control(self.packer_pt, CanBus.POWERTRAIN, apply_steer, idx, lkas_enabled)) - # Gas/regen and brakes - all at 25Hz - if (self.frame % 4) == 0: - if not CC.longActive: - # Stock ECU sends max regen when not enabled - self.apply_gas = self.params.MAX_ACC_REGEN - self.apply_brake = 0 - else: - self.apply_gas = int(round(interp(actuators.accel, self.params.GAS_LOOKUP_BP, self.params.GAS_LOOKUP_V))) - self.apply_brake = int(round(interp(actuators.accel, self.params.BRAKE_LOOKUP_BP, self.params.BRAKE_LOOKUP_V))) - - idx = (self.frame // 4) % 4 - - at_full_stop = CC.longActive and CS.out.standstill - near_stop = CC.longActive and (CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE) - # GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation - can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, CC.enabled, at_full_stop)) - can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, CanBus.CHASSIS, self.apply_brake, idx, near_stop, at_full_stop)) - - # Send dashboard UI commands (ACC status), 25hz - if (self.frame % 4) == 0: - send_fcw = hud_alert == VisualAlert.fcw - can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled, - hud_v_cruise * CV.MS_TO_KPH, hud_control.leadVisible, send_fcw)) - - # Radar needs to know current speed and yaw rate (50hz), - # and that ADAS is alive (10hz) - time_and_headlights_step = 10 - tt = self.frame * DT_CTRL - - if self.frame % time_and_headlights_step == 0: - idx = (self.frame // time_and_headlights_step) % 4 - can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx)) - can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE)) - - speed_and_accelerometer_step = 2 - if self.frame % speed_and_accelerometer_step == 0: - idx = (self.frame // speed_and_accelerometer_step) % 4 - can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx)) - can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx)) - - if self.frame % self.params.ADAS_KEEPALIVE_STEP == 0: - can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN) + if self.CP.openpilotLongitudinalControl: + # Gas/regen, brakes, and UI commands - all at 25Hz + if self.frame % 4 == 0: + if not CC.longActive: + # Stock ECU sends max regen when not enabled + self.apply_gas = self.params.MAX_ACC_REGEN + self.apply_brake = 0 + else: + self.apply_gas = int(round(interp(actuators.accel, self.params.GAS_LOOKUP_BP, self.params.GAS_LOOKUP_V))) + self.apply_brake = int(round(interp(actuators.accel, self.params.BRAKE_LOOKUP_BP, self.params.BRAKE_LOOKUP_V))) + + idx = (self.frame // 4) % 4 + + at_full_stop = CC.longActive and CS.out.standstill + near_stop = CC.longActive and (CS.out.vEgo < self.params.NEAR_STOP_BRAKE_PHASE) + # GasRegenCmdActive needs to be 1 to avoid cruise faults. It describes the ACC state, not actuation + can_sends.append(gmcan.create_gas_regen_command(self.packer_pt, CanBus.POWERTRAIN, self.apply_gas, idx, CC.enabled, at_full_stop)) + can_sends.append(gmcan.create_friction_brake_command(self.packer_ch, CanBus.CHASSIS, self.apply_brake, idx, near_stop, at_full_stop)) + + # Send dashboard UI commands (ACC status) + send_fcw = hud_alert == VisualAlert.fcw + can_sends.append(gmcan.create_acc_dashboard_command(self.packer_pt, CanBus.POWERTRAIN, CC.enabled, + hud_v_cruise * CV.MS_TO_KPH, hud_control.leadVisible, send_fcw)) + + # Radar needs to know current speed and yaw rate (50hz), + # and that ADAS is alive (10hz) + if not self.CP.radarOffCan: + tt = self.frame * DT_CTRL + time_and_headlights_step = 10 + if self.frame % time_and_headlights_step == 0: + idx = (self.frame // time_and_headlights_step) % 4 + can_sends.append(gmcan.create_adas_time_status(CanBus.OBSTACLE, int((tt - self.start_time) * 60), idx)) + can_sends.append(gmcan.create_adas_headlights_status(self.packer_obj, CanBus.OBSTACLE)) + + speed_and_accelerometer_step = 2 + if self.frame % speed_and_accelerometer_step == 0: + idx = (self.frame // speed_and_accelerometer_step) % 4 + can_sends.append(gmcan.create_adas_steering_status(CanBus.OBSTACLE, idx)) + can_sends.append(gmcan.create_adas_accelerometer_speed_status(CanBus.OBSTACLE, CS.out.vEgo, idx)) + + if self.CP.networkLocation == NetworkLocation.gateway and self.frame % self.params.ADAS_KEEPALIVE_STEP == 0: + can_sends += gmcan.create_adas_keepalive(CanBus.POWERTRAIN) # Show green icon when LKA torque is applied, and # alarming orange icon when approaching torque limit. @@ -110,7 +112,7 @@ def update(self, CC, CS): lka_active = CS.lkas_status == 1 lka_critical = lka_active and abs(actuators.steer) > 0.9 lka_icon_status = (lka_active, lka_critical) - if self.frame % self.params.CAMERA_KEEPALIVE_STEP == 0 or lka_icon_status != self.lka_icon_status_last: + if self.CP.networkLocation != NetworkLocation.fwdCamera and (self.frame % self.params.CAMERA_KEEPALIVE_STEP == 0 or lka_icon_status != self.lka_icon_status_last): steer_alert = hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw) can_sends.append(gmcan.create_lka_icon_command(CanBus.SW_GMLAN, lka_active, lka_critical, steer_alert)) self.lka_icon_status_last = lka_icon_status diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index 3605dab48663ff..c28abc6036c962 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -3,7 +3,9 @@ from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from selfdrive.car.interfaces import CarStateBase -from selfdrive.car.gm.values import DBC, CAR, AccState, CanBus, STEER_THRESHOLD +from selfdrive.car.gm.values import DBC, AccState, CanBus, STEER_THRESHOLD + +TransmissionType = car.CarParams.TransmissionType class CarState(CarStateBase): @@ -35,7 +37,7 @@ def update(self, pt_cp, loopback_cp): ret.brakePressed = pt_cp.vl["EBCMBrakePedalPosition"]["BrakePedalPosition"] >= 10 # Regen braking is braking - if self.car_fingerprint == CAR.VOLT: + if self.CP.transmissionType == TransmissionType.direct: ret.brakePressed = ret.brakePressed or pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0 ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254. @@ -120,7 +122,7 @@ def get_can_parser(CP): ("EBCMBrakePedalPosition", 100), ] - if CP.carFingerprint == CAR.VOLT: + if CP.transmissionType == TransmissionType.direct: signals.append(("RegenPaddle", "EBCMRegenPaddle")) checks.append(("EBCMRegenPaddle", 50)) diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 9c1744dfb4c60c..282844d095854c 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -9,6 +9,8 @@ ButtonType = car.CarState.ButtonEvent.Type EventName = car.CarEvent.EventName +TransmissionType = car.CarParams.TransmissionType +NetworkLocation = car.CarParams.NetworkLocation BUTTONS_DICT = {CruiseButtons.RES_ACCEL: ButtonType.accelCruise, CruiseButtons.DECEL_SET: ButtonType.decelCruise, CruiseButtons.MAIN: ButtonType.altButton3, CruiseButtons.CANCEL: ButtonType.cancel} @@ -45,7 +47,11 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret = CarInterfaceBase.get_std_params(candidate, fingerprint) ret.carName = "gm" ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.gm)] - ret.pcmCruise = False # stock cruise control is kept off + ret.pcmCruise = False # For ASCM, stock non-adaptive cruise control is kept off + ret.radarOffCan = False # For ASCM, radar exists + ret.transmissionType = TransmissionType.automatic + # NetworkLocation.gateway: OBD-II harness (typically ASCM), NetworkLocation.fwdCamera: non-ASCM + ret.networkLocation = NetworkLocation.gateway # These cars have been put into dashcam only due to both a lack of users and test coverage. # These cars likely still work fine. Once a user confirms each car works and a test route is @@ -77,17 +83,18 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.minEnableSpeed = 18 * CV.MPH_TO_MS if candidate == CAR.VOLT: + ret.transmissionType = TransmissionType.direct ret.mass = 1607. + STD_CARGO_KG ret.wheelbase = 2.69 ret.steerRatio = 17.7 # Stock 15.7, LiveParameters - tire_stiffness_factor = 0.469 # Stock Michelin Energy Saver A/S, LiveParameters - ret.centerToFront = ret.wheelbase * 0.45 # Volt Gen 1, TODO corner weigh + tire_stiffness_factor = 0.469 # Stock Michelin Energy Saver A/S, LiveParameters + ret.centerToFront = ret.wheelbase * 0.45 # Volt Gen 1, TODO corner weigh ret.lateralTuning.pid.kpBP = [0., 40.] ret.lateralTuning.pid.kpV = [0., 0.17] ret.lateralTuning.pid.kiBP = [0.] ret.lateralTuning.pid.kiV = [0.] - ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_volt() + ret.lateralTuning.pid.kf = 1. # get_steer_feedforward_volt() ret.steerActuatorDelay = 0.2 elif candidate == CAR.MALIBU: @@ -160,7 +167,7 @@ def _update(self, c): ret.buttonEvents = [be] - events = self.create_common_events(ret, pcm_enable=False) + events = self.create_common_events(ret, pcm_enable=self.CP.pcmCruise) if ret.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) @@ -170,7 +177,7 @@ def _update(self, c): events.add(car.CarEvent.EventName.belowSteerSpeed) # handle button presses - events.events.extend(create_button_enable_events(ret.buttonEvents)) + events.events.extend(create_button_enable_events(ret.buttonEvents, pcm_cruise=self.CP.pcmCruise)) ret.events = events.to_msg() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b9ba47e8f746ad..a1cb2bf36269f2 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a98dfc72bb4c5624c2223ca65d52b151f419460c \ No newline at end of file +d7c610172f3ff10b68403abc19b260c91c848ebb \ No newline at end of file From 61d21ff00ae6d4b5f510c5880d2ca747272da754 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 30 Jun 2022 15:52:53 -0700 Subject: [PATCH 216/436] update release notes --- RELEASES.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5d81b4308579eb..5b71fc33752a17 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,5 @@ -Version 0.8.15 (2022-XX-XX) +Version 0.8.15 (2022-07-XX) ======================== -* New driving model * New lateral controller based on physical wheel torque model * Much smoother control, consistent across the speed range * Effective feedforward that uses road roll @@ -10,8 +9,12 @@ Version 0.8.15 (2022-XX-XX) * takes a larger input frame * outputs a driver state for both driver and passenger * automatically determines which side the driver is on (soon) -* Display speed limit while navigating * Reduced power usage: device runs cooler and fan spins less +* Minor UI updates + * New font + * Refreshed max speed design + * Speed limits shown while navigating + * More consistent camera view perspective across cars * AGNOS 5 * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! From d2c2154a32db0876036f4f6b61b912de0c90fc5a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 30 Jun 2022 17:23:12 -0700 Subject: [PATCH 217/436] Clean up CarControllers (#25008) * do VW * Do the rest * unused * ford cc formatting * final clean ups * also just return update output --- selfdrive/car/body/carcontroller.py | 4 +-- selfdrive/car/ford/carcontroller.py | 15 +++++---- selfdrive/car/ford/interface.py | 4 +-- selfdrive/car/gm/carcontroller.py | 2 +- selfdrive/car/gm/interface.py | 3 +- selfdrive/car/hyundai/carcontroller.py | 6 ++-- selfdrive/car/hyundai/interface.py | 3 +- selfdrive/car/mazda/carcontroller.py | 31 ++++++++++-------- selfdrive/car/mazda/interface.py | 4 +-- selfdrive/car/nissan/carcontroller.py | 33 ++++++++++--------- selfdrive/car/nissan/interface.py | 11 ++----- selfdrive/car/subaru/carcontroller.py | 21 +++++++----- selfdrive/car/subaru/interface.py | 10 ++---- selfdrive/car/tesla/interface.py | 3 +- selfdrive/car/toyota/interface.py | 3 +- selfdrive/car/volkswagen/carcontroller.py | 40 +++++++++++++---------- selfdrive/car/volkswagen/interface.py | 10 +----- 17 files changed, 97 insertions(+), 106 deletions(-) diff --git a/selfdrive/car/body/carcontroller.py b/selfdrive/car/body/carcontroller.py index 5714445f675c79..c13acebacb78b3 100644 --- a/selfdrive/car/body/carcontroller.py +++ b/selfdrive/car/body/carcontroller.py @@ -1,8 +1,8 @@ import numpy as np from common.realtime import DT_CTRL -from selfdrive.car.body import bodycan from opendbc.can.packer import CANPacker +from selfdrive.car.body import bodycan from selfdrive.car.body.values import SPEED_FROM_RPM from selfdrive.controls.lib.pid import PIDController @@ -14,7 +14,7 @@ MAX_TURN_INTEGRATOR = 0.1 # meters -class CarController(): +class CarController: def __init__(self, dbc_name, CP, VM): self.frame = 0 self.packer = CANPacker(dbc_name) diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index c2f87a48068bd1..d7666c1d6522f7 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -1,8 +1,8 @@ from cereal import car from common.numpy_fast import clip, interp +from opendbc.can.packer import CANPacker from selfdrive.car.ford import fordcan from selfdrive.car.ford.values import CarControllerParams -from opendbc.can.packer import CANPacker VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -17,11 +17,12 @@ def apply_ford_steer_angle_limits(apply_steer, apply_steer_last, vEgo): return apply_steer -class CarController(): +class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP self.VM = VM self.packer = CANPacker(dbc_name) + self.frame = 0 self.apply_steer_last = 0 self.steer_rate_limited = False @@ -29,7 +30,7 @@ def __init__(self, dbc_name, CP, VM): self.lkas_enabled_last = False self.steer_alert_last = False - def update(self, CC, CS, frame): + def update(self, CC, CS): can_sends = [] actuators = CC.actuators @@ -48,7 +49,7 @@ def update(self, CC, CS, frame): self.steer_rate_limited = new_steer != apply_steer # send steering commands at 20Hz - if (frame % CarControllerParams.LKAS_STEER_STEP) == 0: + if (self.frame % CarControllerParams.LKAS_STEER_STEP) == 0: lca_rq = 1 if CC.latActive else 0 # use LatCtlPath_An_Actl to actuate steering for now until curvature control is implemented @@ -69,15 +70,14 @@ def update(self, CC, CS, frame): can_sends.append(fordcan.create_tja_command(self.packer, lca_rq, ramp_type, precision, path_offset, path_angle, curvature_rate, curvature)) - send_ui = (self.main_on_last != main_on) or (self.lkas_enabled_last != CC.latActive) or (self.steer_alert_last != steer_alert) # send lkas ui command at 1Hz or if ui state changes - if (frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui: + if (self.frame % CarControllerParams.LKAS_UI_STEP) == 0 or send_ui: can_sends.append(fordcan.create_lkas_ui_command(self.packer, main_on, CC.latActive, steer_alert, CS.lkas_status_stock_values)) # send acc ui command at 20Hz or if ui state changes - if (frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui: + if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui: can_sends.append(fordcan.create_acc_ui_command(self.packer, main_on, CC.latActive, CS.acc_tja_status_stock_values)) self.main_on_last = main_on @@ -87,4 +87,5 @@ def update(self, CC, CS, frame): new_actuators = actuators.copy() new_actuators.steeringAngleDeg = apply_steer + self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index c608cc08d97f82..1af04bee41d6b5 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -78,6 +78,4 @@ def _update(self, c): return ret def apply(self, c): - ret = self.CC.update(c, self.CS, self.frame) - self.frame += 1 - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/gm/carcontroller.py b/selfdrive/car/gm/carcontroller.py index 8ad5049e3210a5..f763a58532067c 100644 --- a/selfdrive/car/gm/carcontroller.py +++ b/selfdrive/car/gm/carcontroller.py @@ -1,7 +1,7 @@ from cereal import car from common.conversions import Conversions as CV -from common.realtime import DT_CTRL from common.numpy_fast import interp +from common.realtime import DT_CTRL from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.gm import gmcan diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 282844d095854c..e0dd10d4a8a841 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -184,5 +184,4 @@ def _update(self, c): return ret def apply(self, c): - ret = self.CC.update(c, self.CS) - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index fb3579464a0275..4fbb5ce0e1c08e 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -1,11 +1,11 @@ from cereal import car -from common.realtime import DT_CTRL -from common.numpy_fast import clip, interp from common.conversions import Conversions as CV +from common.numpy_fast import clip, interp +from common.realtime import DT_CTRL +from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.hyundai import hda2can, hyundaican from selfdrive.car.hyundai.values import Buttons, CarControllerParams, HDA2_CAR, CAR -from opendbc.can.packer import CANPacker VisualAlert = car.CarControl.HUDControl.VisualAlert LongCtrlState = car.CarControl.Actuators.LongControlState diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 58a67f58cef7fb..97119c77b770c1 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -349,5 +349,4 @@ def _update(self, c): return ret def apply(self, c): - ret = self.CC.update(c, self.CS) - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py index d003079d63eea0..0e43a11ceb5d07 100644 --- a/selfdrive/car/mazda/carcontroller.py +++ b/selfdrive/car/mazda/carcontroller.py @@ -1,46 +1,48 @@ from cereal import car from opendbc.can.packer import CANPacker +from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.mazda import mazdacan from selfdrive.car.mazda.values import CarControllerParams, Buttons -from selfdrive.car import apply_std_steer_torque_limits VisualAlert = car.CarControl.HUDControl.VisualAlert -class CarController(): + +class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 self.packer = CANPacker(dbc_name) self.steer_rate_limited = False self.brake_counter = 0 + self.frame = 0 - def update(self, c, CS, frame): + def update(self, CC, CS): can_sends = [] apply_steer = 0 self.steer_rate_limited = False - if c.latActive: + if CC.latActive: # calculate steer and also set limits due to driver torque - new_steer = int(round(c.actuators.steer * CarControllerParams.STEER_MAX)) + new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, CarControllerParams) self.steer_rate_limited = new_steer != apply_steer - if c.enabled: - if CS.out.standstill and frame % 5 == 0: + if CC.enabled: + if CS.out.standstill and self.frame % 5 == 0: # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds # Send Resume button at 20hz if we're engaged at standstill to support full stop and go! # TODO: improve the resume trigger logic by looking at actual radar data can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.RESUME)) - if c.cruiseControl.cancel: + if CC.cruiseControl.cancel: # If brake is pressed, let us wait >70ms before trying to disable crz to avoid # a race condition with the stock system, where the second cancel from openpilot # will disable the crz 'main on'. crz ctrl msg runs at 50hz. 70ms allows us to # read 3 messages and most likely sync state before we attempt cancel. self.brake_counter = self.brake_counter + 1 - if frame % 10 == 0 and not (CS.out.brakePressed and self.brake_counter < 7): + if self.frame % 10 == 0 and not (CS.out.brakePressed and self.brake_counter < 7): # Cancel Stock ACC if it's enabled while OP is disengaged # Send at a rate of 10hz until we sync with stock ACC state can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.CANCEL)) @@ -50,18 +52,19 @@ def update(self, c, CS, frame): self.apply_steer_last = apply_steer # send HUD alerts - if frame % 50 == 0: - ldw = c.hudControl.visualAlert == VisualAlert.ldw - steer_required = c.hudControl.visualAlert == VisualAlert.steerRequired + if self.frame % 50 == 0: + ldw = CC.hudControl.visualAlert == VisualAlert.ldw + steer_required = CC.hudControl.visualAlert == VisualAlert.steerRequired # TODO: find a way to silence audible warnings so we can add more hud alerts steer_required = steer_required and CS.lkas_allowed_speed can_sends.append(mazdacan.create_alert_command(self.packer, CS.cam_laneinfo, ldw, steer_required)) # send steering command can_sends.append(mazdacan.create_steering_control(self.packer, self.CP.carFingerprint, - frame, apply_steer, CS.cam_lkas)) + self.frame, apply_steer, CS.cam_lkas)) - new_actuators = c.actuators.copy() + new_actuators = CC.actuators.copy() new_actuators.steer = apply_steer / CarControllerParams.STEER_MAX + self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/mazda/interface.py b/selfdrive/car/mazda/interface.py index cbeb910de24121..35e3c1bb01f259 100755 --- a/selfdrive/car/mazda/interface.py +++ b/selfdrive/car/mazda/interface.py @@ -90,6 +90,4 @@ def _update(self, c): return ret def apply(self, c): - ret = self.CC.update(c, self.CS, self.frame) - self.frame += 1 - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/nissan/carcontroller.py b/selfdrive/car/nissan/carcontroller.py index 4ceb142d45d57a..7d9dad6693afce 100644 --- a/selfdrive/car/nissan/carcontroller.py +++ b/selfdrive/car/nissan/carcontroller.py @@ -1,25 +1,27 @@ from cereal import car from common.numpy_fast import clip, interp -from selfdrive.car.nissan import nissancan from opendbc.can.packer import CANPacker +from selfdrive.car.nissan import nissancan from selfdrive.car.nissan.values import CAR, CarControllerParams - VisualAlert = car.CarControl.HUDControl.VisualAlert -class CarController(): +class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP self.car_fingerprint = CP.carFingerprint + self.frame = 0 self.lkas_max_torque = 0 self.last_angle = 0 self.packer = CANPacker(dbc_name) - def update(self, c, CS, frame, actuators, cruise_cancel, hud_alert, - left_line, right_line, left_lane_depart, right_lane_depart): + def update(self, CC, CS): + actuators = CC.actuators + hud_control = CC.hudControl + pcm_cancel_cmd = CC.cruiseControl.cancel can_sends = [] @@ -28,9 +30,9 @@ def update(self, c, CS, frame, actuators, cruise_cancel, hud_alert, lkas_hud_info_msg = CS.lkas_hud_info_msg apply_angle = actuators.steeringAngleDeg - steer_hud_alert = 1 if hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw) else 0 + steer_hud_alert = 1 if hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw) else 0 - if c.latActive: + if CC.latActive: # # windup slower if self.last_angle * apply_angle > 0. and abs(apply_angle) > abs(self.last_angle): angle_rate_lim = interp(CS.out.vEgo, CarControllerParams.ANGLE_DELTA_BP, CarControllerParams.ANGLE_DELTA_V) @@ -57,25 +59,25 @@ def update(self, c, CS, frame, actuators, cruise_cancel, hud_alert, self.last_angle = apply_angle - if self.CP.carFingerprint in (CAR.ROGUE, CAR.XTRAIL, CAR.ALTIMA) and cruise_cancel: - can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg, frame)) + if self.CP.carFingerprint in (CAR.ROGUE, CAR.XTRAIL, CAR.ALTIMA) and pcm_cancel_cmd: + can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg, self.frame)) # TODO: Find better way to cancel! # For some reason spamming the cancel button is unreliable on the Leaf # We now cancel by making propilot think the seatbelt is unlatched, # this generates a beep and a warning message every time you disengage - if self.CP.carFingerprint in (CAR.LEAF, CAR.LEAF_IC) and frame % 2 == 0: - can_sends.append(nissancan.create_cancel_msg(self.packer, CS.cancel_msg, cruise_cancel)) + if self.CP.carFingerprint in (CAR.LEAF, CAR.LEAF_IC) and self.frame % 2 == 0: + can_sends.append(nissancan.create_cancel_msg(self.packer, CS.cancel_msg, pcm_cancel_cmd)) can_sends.append(nissancan.create_steering_control( - self.packer, apply_angle, frame, c.enabled, self.lkas_max_torque)) + self.packer, apply_angle, self.frame, CC.enabled, self.lkas_max_torque)) if lkas_hud_msg and lkas_hud_info_msg: - if frame % 2 == 0: + if self.frame % 2 == 0: can_sends.append(nissancan.create_lkas_hud_msg( - self.packer, lkas_hud_msg, c.enabled, left_line, right_line, left_lane_depart, right_lane_depart)) + self.packer, lkas_hud_msg, CC.enabled, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart)) - if frame % 50 == 0: + if self.frame % 50 == 0: can_sends.append(nissancan.create_lkas_hud_info_msg( self.packer, lkas_hud_info_msg, steer_hud_alert )) @@ -83,4 +85,5 @@ def update(self, c, CS, frame, actuators, cruise_cancel, hud_alert, new_actuators = actuators.copy() new_actuators.steeringAngleDeg = apply_angle + self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/nissan/interface.py b/selfdrive/car/nissan/interface.py index 436cef68bf151c..9c04d975f93161 100644 --- a/selfdrive/car/nissan/interface.py +++ b/selfdrive/car/nissan/interface.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 from cereal import car -from selfdrive.car.nissan.values import CAR from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase +from selfdrive.car.nissan.values import CAR + class CarInterface(CarInterfaceBase): @@ -67,10 +68,4 @@ def _update(self, c): return ret def apply(self, c): - hud_control = c.hudControl - ret = self.CC.update(c, self.CS, self.frame, c.actuators, - c.cruiseControl.cancel, hud_control.visualAlert, - hud_control.leftLaneVisible, hud_control.rightLaneVisible, - hud_control.leftLaneDepart, hud_control.rightLaneDepart) - self.frame += 1 - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index d37b82eed72cc3..dca86c30a6ee80 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -1,10 +1,10 @@ +from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.subaru import subarucan from selfdrive.car.subaru.values import DBC, PREGLOBAL_CARS, CarControllerParams -from opendbc.can.packer import CANPacker -class CarController(): +class CarController: def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 @@ -12,16 +12,20 @@ def __init__(self, dbc_name, CP, VM): self.es_lkas_cnt = -1 self.cruise_button_prev = 0 self.steer_rate_limited = False + self.frame = 0 self.p = CarControllerParams(CP) self.packer = CANPacker(DBC[CP.carFingerprint]['pt']) - def update(self, c, CS, frame, actuators, pcm_cancel_cmd, visual_alert, left_line, right_line, left_lane_depart, right_lane_depart): + def update(self, CC, CS): + actuators = CC.actuators + hud_control = CC.hudControl + pcm_cancel_cmd = CC.cruiseControl.cancel can_sends = [] # *** steering *** - if (frame % self.p.STEER_STEP) == 0: + if (self.frame % self.p.STEER_STEP) == 0: apply_steer = int(round(actuators.steer * self.p.STEER_MAX)) @@ -31,13 +35,13 @@ def update(self, c, CS, frame, actuators, pcm_cancel_cmd, visual_alert, left_lin apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.p) self.steer_rate_limited = new_steer != apply_steer - if not c.latActive: + if not CC.latActive: apply_steer = 0 if self.CP.carFingerprint in PREGLOBAL_CARS: - can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer, frame, self.p.STEER_STEP)) + can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer, self.frame, self.p.STEER_STEP)) else: - can_sends.append(subarucan.create_steering_control(self.packer, apply_steer, frame, self.p.STEER_STEP)) + can_sends.append(subarucan.create_steering_control(self.packer, apply_steer, self.frame, self.p.STEER_STEP)) self.apply_steer_last = apply_steer @@ -70,10 +74,11 @@ def update(self, c, CS, frame, actuators, pcm_cancel_cmd, visual_alert, left_lin self.es_distance_cnt = CS.es_distance_msg["Counter"] if self.es_lkas_cnt != CS.es_lkas_msg["Counter"]: - can_sends.append(subarucan.create_es_lkas(self.packer, CS.es_lkas_msg, c.enabled, visual_alert, left_line, right_line, left_lane_depart, right_lane_depart)) + can_sends.append(subarucan.create_es_lkas(self.packer, CS.es_lkas_msg, CC.enabled, hud_control.visualAlert, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart)) self.es_lkas_cnt = CS.es_lkas_msg["Counter"] new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX + self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index 8c5cab86e8731a..952885a7511220 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 from cereal import car -from selfdrive.car.subaru.values import CAR, PREGLOBAL_CARS from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase +from selfdrive.car.subaru.values import CAR, PREGLOBAL_CARS + class CarInterface(CarInterfaceBase): @@ -117,9 +118,4 @@ def _update(self, c): return ret def apply(self, c): - hud_control = c.hudControl - ret = self.CC.update(c, self.CS, self.frame, c.actuators, - c.cruiseControl.cancel, hud_control.visualAlert, - hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart) - self.frame += 1 - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/tesla/interface.py b/selfdrive/car/tesla/interface.py index d4eda38e6441a6..5e78c72b600da1 100755 --- a/selfdrive/car/tesla/interface.py +++ b/selfdrive/car/tesla/interface.py @@ -64,5 +64,4 @@ def _update(self, c): return ret def apply(self, c): - ret = self.CC.update(c, self.CS) - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index fdb923f693cc46..337c039565b5d7 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -274,5 +274,4 @@ def _update(self, c): # pass in a car.CarControl # to be called @ 100hz def apply(self, c): - ret = self.CC.update(c, self.CS) - return ret + return self.CC.update(c, self.CS) diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 842eb6d13911b0..4614463c6e93f7 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -1,15 +1,17 @@ from cereal import car +from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.volkswagen import volkswagencan from selfdrive.car.volkswagen.values import DBC_FILES, CANBUS, MQB_LDW_MESSAGES, BUTTON_STATES, CarControllerParams as P -from opendbc.can.packer import CANPacker VisualAlert = car.CarControl.HUDControl.VisualAlert -class CarController(): + +class CarController: def __init__(self, dbc_name, CP, VM): - self.apply_steer_last = 0 self.CP = CP + self.apply_steer_last = 0 + self.frame = 0 self.packer_pt = CANPacker(DBC_FILES.mqb) @@ -22,14 +24,15 @@ def __init__(self, dbc_name, CP, VM): self.steer_rate_limited = False - def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visible, right_lane_visible, left_lane_depart, right_lane_depart): - """ Controls thread """ + def update(self, CC, CS, ext_bus): + actuators = CC.actuators + hud_control = CC.hudControl can_sends = [] # **** Steering Controls ************************************************ # - if frame % P.HCA_STEP == 0: + if self.frame % P.HCA_STEP == 0: # Logic to avoid HCA state 4 "refused": # * Don't steer unless HCA is in state 3 "ready" or 5 "active" # * Don't steer at standstill @@ -40,7 +43,7 @@ def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visib # torque value. Do that anytime we happen to have 0 torque, or failing that, # when exceeding ~1/3 the 360 second timer. - if c.latActive: + if CC.latActive: new_steer = int(round(actuators.steer * P.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, P) self.steer_rate_limited = new_steer != apply_steer @@ -66,34 +69,34 @@ def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visib apply_steer = 0 self.apply_steer_last = apply_steer - idx = (frame / P.HCA_STEP) % 16 + idx = (self.frame / P.HCA_STEP) % 16 can_sends.append(volkswagencan.create_mqb_steering_control(self.packer_pt, CANBUS.pt, apply_steer, idx, hcaEnabled)) # **** HUD Controls ***************************************************** # - if frame % P.LDW_STEP == 0: - if visual_alert in (VisualAlert.steerRequired, VisualAlert.ldw): + if self.frame % P.LDW_STEP == 0: + if hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw): hud_alert = MQB_LDW_MESSAGES["laneAssistTakeOverSilent"] else: hud_alert = MQB_LDW_MESSAGES["none"] - can_sends.append(volkswagencan.create_mqb_hud_control(self.packer_pt, CANBUS.pt, c.enabled, - CS.out.steeringPressed, hud_alert, left_lane_visible, - right_lane_visible, CS.ldw_stock_values, - left_lane_depart, right_lane_depart)) + can_sends.append(volkswagencan.create_mqb_hud_control(self.packer_pt, CANBUS.pt, CC.enabled, + CS.out.steeringPressed, hud_alert, hud_control.leftLaneVisible, + hud_control.rightLaneVisible, CS.ldw_stock_values, + hud_control.leftLaneDepart, hud_control.rightLaneDepart)) # **** ACC Button Controls ********************************************** # # FIXME: this entire section is in desperate need of refactoring if self.CP.pcmCruise: - if frame > self.graMsgStartFramePrev + P.GRA_VBP_STEP: - if c.cruiseControl.cancel: + if self.frame > self.graMsgStartFramePrev + P.GRA_VBP_STEP: + if CC.cruiseControl.cancel: # Cancel ACC if it's engaged with OP disengaged. self.graButtonStatesToSend = BUTTON_STATES.copy() self.graButtonStatesToSend["cancel"] = True - elif c.enabled and CS.out.cruiseState.standstill: + elif CC.enabled and CS.out.cruiseState.standstill: # Blip the Resume button if we're engaged at standstill. # FIXME: This is a naive implementation, improve with visiond or radar input. self.graButtonStatesToSend = BUTTON_STATES.copy() @@ -103,7 +106,7 @@ def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visib self.graMsgBusCounterPrev = CS.graMsgBusCounter if self.graButtonStatesToSend is not None: if self.graMsgSentCount == 0: - self.graMsgStartFramePrev = frame + self.graMsgStartFramePrev = self.frame idx = (CS.graMsgBusCounter + 1) % 16 can_sends.append(volkswagencan.create_mqb_acc_buttons_control(self.packer_pt, ext_bus, self.graButtonStatesToSend, CS, idx)) self.graMsgSentCount += 1 @@ -114,4 +117,5 @@ def update(self, c, CS, frame, ext_bus, actuators, visual_alert, left_lane_visib new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / P.STEER_MAX + self.frame += 1 return new_actuators, can_sends diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 6782105c163af2..c2b077b6db493d 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -193,12 +193,4 @@ def _update(self, c): return ret def apply(self, c): - hud_control = c.hudControl - ret = self.CC.update(c, self.CS, self.frame, self.ext_bus, c.actuators, - hud_control.visualAlert, - hud_control.leftLaneVisible, - hud_control.rightLaneVisible, - hud_control.leftLaneDepart, - hud_control.rightLaneDepart) - self.frame += 1 - return ret + return self.CC.update(c, self.CS, self.ext_bus) From 38ff2982eb2e72b634af26e2adb0ac7a363999ce Mon Sep 17 00:00:00 2001 From: realfast Date: Thu, 30 Jun 2022 19:27:35 -0500 Subject: [PATCH 218/436] Chrysler: carState signals update (#24760) * carstate update * update refs Co-authored-by: Adeeb Shihadeh --- opendbc | 2 +- selfdrive/car/chrysler/carcontroller.py | 4 +- selfdrive/car/chrysler/carstate.py | 58 ++++++++++++------------ selfdrive/car/chrysler/chryslercan.py | 9 ++-- selfdrive/test/process_replay/ref_commit | 2 +- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/opendbc b/opendbc index 47b79c4d5ab5ad..7fbf7c2a685a90 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 47b79c4d5ab5adc0bdd9d228c3d9594da0355c49 +Subproject commit 7fbf7c2a685a90ff01e744cfb410f1a8a3c06278 diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 3d6295d77d7f81..49525646ca6297 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -1,7 +1,7 @@ from cereal import car from opendbc.can.packer import CANPacker from selfdrive.car import apply_toyota_steer_torque_limits -from selfdrive.car.chrysler.chryslercan import create_lkas_hud, create_lkas_command, create_wheel_buttons +from selfdrive.car.chrysler.chryslercan import create_lkas_hud, create_lkas_command, create_cruise_buttons from selfdrive.car.chrysler.values import CAR, CarControllerParams @@ -49,7 +49,7 @@ def update(self, CC, CS): # *** control msgs *** if CC.cruiseControl.cancel: - can_sends.append(create_wheel_buttons(self.packer, CS.button_counter + 1, cancel=True)) + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, cancel=True)) # LKAS_HEARTBIT is forwarded by Panda so no need to send it here. # frame is 100Hz (0.01s period) diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 202526df05c597..444557191a1872 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -16,13 +16,13 @@ def update(self, cp, cp_cam): ret = car.CarState.new_message() - self.frame = int(cp.vl["EPS_STATUS"]["COUNTER"]) + self.frame = int(cp.vl["EPS_2"]["COUNTER"]) ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"], cp.vl["BCM_1"]["DOOR_OPEN_FR"], cp.vl["BCM_1"]["DOOR_OPEN_RL"], cp.vl["BCM_1"]["DOOR_OPEN_RR"]]) - ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]["SEATBELT_DRIVER_UNLATCHED"] == 1 + ret.seatbeltUnlatched = cp.vl["ORC_1"]["SEATBELT_DRIVER_UNLATCHED"] == 1 # brake pedal ret.brake = 0 @@ -35,10 +35,10 @@ def update(self, cp, cp_cam): ret.espDisabled = (cp.vl["TRACTION_BUTTON"]["TRACTION_OFF"] == 1) ret.wheelSpeeds = self.get_wheel_speeds( - cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FL"], - cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_FR"], - cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RL"], - cp.vl["WHEEL_SPEEDS"]["WHEEL_SPEED_RR"], + cp.vl["ESP_6"]["WHEEL_SPEED_FL"], + cp.vl["ESP_6"]["WHEEL_SPEED_FR"], + cp.vl["ESP_6"]["WHEEL_SPEED_RL"], + cp.vl["ESP_6"]["WHEEL_SPEED_RR"], unit=1, ) ret.vEgoRaw = (cp.vl["SPEED_1"]["SPEED_LEFT"] + cp.vl["SPEED_1"]["SPEED_RIGHT"]) / 2. @@ -51,17 +51,17 @@ def update(self, cp, cp_cam): ret.cruiseState.available = cp.vl["DAS_3"]["ACC_AVAILABLE"] == 1 # ACC is white ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_ACTIVE"] == 1 # ACC is green - ret.cruiseState.speed = cp.vl["DASHBOARD"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS + ret.cruiseState.speed = cp.vl["DAS_4"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too - ret.cruiseState.nonAdaptive = cp.vl["DASHBOARD"]["CRUISE_STATE"] in (1, 2) + ret.cruiseState.nonAdaptive = cp.vl["DAS_4"]["CRUISE_STATE"] in (1, 2) ret.accFaulted = cp.vl["DAS_3"]["ACC_FAULTED"] != 0 ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] - ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] - ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] + ret.steeringTorque = cp.vl["EPS_2"]["COLUMN_TORQUE"] + ret.steeringTorqueEps = cp.vl["EPS_2"]["EPS_TORQUE_MOTOR"] ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD - steer_state = cp.vl["EPS_STATUS"]["LKAS_STATE"] + steer_state = cp.vl["EPS_2"]["LKAS_STATE"] ret.steerFaultPermanent = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]["HIGH_BEAM_FLASH"]) @@ -73,7 +73,7 @@ def update(self, cp, cp_cam): self.lkas_counter = cp_cam.vl["LKAS_COMMAND"]["COUNTER"] self.lkas_car_model = cp_cam.vl["LKAS_HUD"]["CAR_MODEL"] self.lkas_status_ok = cp_cam.vl["LKAS_HEARTBIT"]["LKAS_STATUS_OK"] - self.button_counter = cp.vl["WHEEL_BUTTONS"]["COUNTER"] + self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"] return ret @@ -90,10 +90,10 @@ def get_can_parser(CP): ("Accelerator_Position", "ECM_5"), ("SPEED_LEFT", "SPEED_1"), ("SPEED_RIGHT", "SPEED_1"), - ("WHEEL_SPEED_FL", "WHEEL_SPEEDS"), - ("WHEEL_SPEED_RR", "WHEEL_SPEEDS"), - ("WHEEL_SPEED_RL", "WHEEL_SPEEDS"), - ("WHEEL_SPEED_FR", "WHEEL_SPEEDS"), + ("WHEEL_SPEED_FL", "ESP_6"), + ("WHEEL_SPEED_RR", "ESP_6"), + ("WHEEL_SPEED_RL", "ESP_6"), + ("WHEEL_SPEED_FR", "ESP_6"), ("STEER_ANGLE", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), @@ -101,31 +101,31 @@ def get_can_parser(CP): ("ACC_ACTIVE", "DAS_3"), ("ACC_FAULTED", "DAS_3"), ("HIGH_BEAM_FLASH", "STEERING_LEVERS"), - ("ACC_SPEED_CONFIG_KPH", "DASHBOARD"), - ("CRUISE_STATE", "DASHBOARD"), - ("TORQUE_DRIVER", "EPS_STATUS"), - ("TORQUE_MOTOR", "EPS_STATUS"), - ("LKAS_STATE", "EPS_STATUS"), - ("COUNTER", "EPS_STATUS",), + ("ACC_SPEED_CONFIG_KPH", "DAS_4"), + ("CRUISE_STATE", "DAS_4"), + ("COLUMN_TORQUE", "EPS_2"), + ("EPS_TORQUE_MOTOR", "EPS_2"), + ("LKAS_STATE", "EPS_2"), + ("COUNTER", "EPS_2",), ("TRACTION_OFF", "TRACTION_BUTTON"), - ("SEATBELT_DRIVER_UNLATCHED", "SEATBELT_STATUS"), - ("COUNTER", "WHEEL_BUTTONS"), + ("SEATBELT_DRIVER_UNLATCHED", "ORC_1"), + ("COUNTER", "CRUISE_BUTTONS"), ] checks = [ # sig_address, frequency ("ESP_1", 50), - ("EPS_STATUS", 100), + ("EPS_2", 100), ("SPEED_1", 100), - ("WHEEL_SPEEDS", 50), + ("ESP_6", 50), ("STEERING", 100), ("DAS_3", 50), ("GEAR", 50), ("ECM_5", 50), - ("WHEEL_BUTTONS", 50), - ("DASHBOARD", 15), + ("CRUISE_BUTTONS", 50), + ("DAS_4", 15), ("STEERING_LEVERS", 10), - ("SEATBELT_STATUS", 2), + ("ORC_1", 2), ("BCM_1", 1), ("TRACTION_BUTTON", 1), ] diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index 896a6b15daed70..53b79cab73e566 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -48,10 +48,9 @@ def create_lkas_command(packer, apply_steer, moving_fast, frame): return packer.make_can_msg("LKAS_COMMAND", 0, values) -def create_wheel_buttons(packer, frame, cancel=False): - # WHEEL_BUTTONS (571) Message sent to cancel ACC. +def create_cruise_buttons(packer, frame, cancel=False): values = { - "ACC_CANCEL": cancel, - "COUNTER": frame % 0x10 + "ACC_Cancel": cancel, + "COUNTER": frame % 0x10, } - return packer.make_can_msg("WHEEL_BUTTONS", 0, values) + return packer.make_can_msg("CRUISE_BUTTONS", 0, values) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index a1cb2bf36269f2..90444b1fa7950b 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -d7c610172f3ff10b68403abc19b260c91c848ebb \ No newline at end of file +806984d4206056fb132625c5dad6c0ca1835a2d6 \ No newline at end of file From ab8592187f36ea9fda6767e86a0651366c12c487 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 30 Jun 2022 17:55:22 -0700 Subject: [PATCH 219/436] Chrysler Pacifica 2019 is supported (#25010) 19 is secretly supported --- docs/CARS.md | 2 +- selfdrive/car/chrysler/values.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index fb163b44e0f79a..25de6a2549272c 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -154,7 +154,7 @@ How We Rate The Cars |Audi|Q3 2020-21|ACC + Lane Assist|||||| |Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| |Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica 2020|Adaptive Cruise|||||| +|Chrysler|Pacifica 2019-20|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| |Genesis|G90 2018|All|||||| diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index bdd80757f47552..6faa76a4796c82 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -39,7 +39,7 @@ class ChryslerCarInfo(CarInfo): CAR.PACIFICA_2018_HYBRID: None, # same platforms CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-22"), CAR.PACIFICA_2018: ChryslerCarInfo("Chrysler Pacifica 2017-18"), - CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2020"), + CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2019-20"), CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-20", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), } From c21ee5b76411de1bea1b95a0d71b48be879d7734 Mon Sep 17 00:00:00 2001 From: TheWizard1328 Date: Thu, 30 Jun 2022 21:06:49 -0600 Subject: [PATCH 220/436] Chrysler: add missing 2022 Pacifica Hybrid fingerprint (#24685) * Added 2022 PacHy info Didn't really need to add this but thought it would be useful. * Added 2022 PacHy info * Added 2022 PacHy info Added 2022 PacHy FP * add to current platform * generate docs * should only need this Co-authored-by: Shane Smiskol --- selfdrive/car/chrysler/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 6faa76a4796c82..d624bd27272fc7 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -88,7 +88,7 @@ class ChryslerCarInfo(CarInfo): }, # Based on "8190c7275a24557b|2020-02-24--09-57-23" { - 168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 515: 7, 516: 7, 517: 7, 518: 7, 520: 8, 524: 8, 526: 6, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 640: 1, 650: 8, 653: 8, 654: 8, 655: 8, 656: 4, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 683: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 711: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 793: 8, 794: 8, 795: 8, 796: 8, 797: 8, 798: 8, 799: 8, 800: 8, 801: 8, 802: 8, 803: 8, 804: 8, 805: 8, 807: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 847: 1, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 886: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1216: 8, 1218: 8, 1220: 8, 1225: 8, 1235: 8, 1242: 8, 1246: 8, 1250: 8, 1251: 8, 1252: 8, 1258: 8, 1259: 8, 1260: 8, 1262: 8, 1284: 8, 1568: 8, 1570: 8, 1856: 8, 1858: 8, 1860: 8, 1863: 8, 1865: 8, 1875: 8, 1882: 8, 1886: 8, 1890: 8, 1891: 8, 1892: 8, 1898: 8, 1899: 8, 1900: 8, 1902: 8, 2015: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2023: 8, 2024: 8, 2026: 8, 2027: 8, 2028: 8, 2031: 8 + 168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 515: 7, 516: 7, 517: 7, 518: 7, 520: 8, 524: 8, 526: 6, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 640: 1, 650: 8, 653: 8, 654: 8, 655: 8, 656: 4, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 683: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 711: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 793: 8, 794: 8, 795: 8, 796: 8, 797: 8, 798: 8, 799: 8, 800: 8, 801: 8, 802: 8, 803: 8, 804: 8, 805: 8, 807: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 847: 1, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 886: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1216: 8, 1218: 8, 1220: 8, 1225: 8, 1235: 8, 1242: 8, 1246: 8, 1250: 8, 1251: 8, 1252: 8, 1258: 8, 1259: 8, 1260: 8, 1262: 8, 1284: 8, 1536: 8, 1568: 8, 1570: 8, 1856: 8, 1858: 8, 1860: 8, 1863: 8, 1865: 8, 1875: 8, 1882: 8, 1886: 8, 1890: 8, 1891: 8, 1892: 8, 1898: 8, 1899: 8, 1900: 8, 1902: 8, 2015: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2023: 8, 2024: 8, 2026: 8, 2027: 8, 2028: 8, 2031: 8 }], CAR.JEEP_CHEROKEE: [{ 55: 8, 168: 8, 181: 8, 256: 4, 257: 5, 258: 8, 264: 8, 268: 8, 272: 6, 273: 6, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 292: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 352: 8, 362: 8, 368: 8, 376: 3, 384: 8, 388: 4, 416: 7, 448: 6, 456: 4, 464: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 532: 8, 544: 8, 557: 8, 559: 8, 560: 4, 564: 4, 571: 3, 579: 8, 584: 8, 608: 8, 618: 8, 624: 8, 625: 8, 632: 8, 639: 8, 656: 4, 658: 6, 660: 8, 671: 8, 672: 8, 676: 8, 678: 8, 680: 8, 683: 8, 684: 8, 703: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 761: 8, 764: 8, 766: 8, 773: 8, 776: 8, 779: 8, 782: 8, 783: 8, 784: 8, 785: 8, 788: 3, 792: 8, 799: 8, 800: 8, 804: 8, 806: 2, 808: 8, 810: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 831: 6, 832: 8, 838: 2, 840: 8, 844: 5, 847: 1, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 874: 2, 882: 8, 897: 8, 906: 8, 924: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 956: 8, 968: 8, 969: 4, 970: 8, 973: 8, 974: 5, 975: 8, 976: 8, 977: 4, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8, 1543: 8, 1562: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8 From b51a90b5a87e0b6388191f7cc5857af8d72e79de Mon Sep 17 00:00:00 2001 From: Yu Yamaguchi Date: Fri, 1 Jul 2022 15:35:37 +0900 Subject: [PATCH 221/436] Mazda: add missing FW version for CX-5 2022 (#24925) --- selfdrive/car/mazda/values.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index 5c80303dbcd3e6..f5d63418f1c70d 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -65,6 +65,7 @@ class Buttons: ], (Ecu.engine, 0x7e0, None): [ b'PX2G-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'SH54-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'K131-67XK2-F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -77,6 +78,7 @@ class Buttons: ], (Ecu.transmission, 0x7e1, None): [ b'PYB2-21PS1-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'SH51-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], }, CAR.CX5: { From 20ccfed9c1d75de2a589294a5407e15304fd3f4a Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 1 Jul 2022 16:43:36 +0200 Subject: [PATCH 222/436] laikad: Improve logging, fix warning and more exception handling (#25005) * change logs and add some debugging. Add test * Less logging and better check for exceptions when parsing orbits * Fix debug log and fix kf initialization --- selfdrive/locationd/laikad.py | 28 ++++++++++++++++--------- selfdrive/locationd/laikad_helpers.py | 2 +- selfdrive/locationd/test/test_laikad.py | 12 +++++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index d51ac10816454b..427eb3dbe0bf20 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -31,7 +31,14 @@ class Laikad: def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), - save_ephemeris=False, last_known_position=None): + save_ephemeris=False): + """ + valid_const: GNSS constellation which can be used + auto_fetch_orbits: If true fetch orbits from internet when needed + auto_update: If true download AstroDog will download all files needed. This can be ephemeris or correction data like ionosphere. + valid_ephem_types: Valid ephemeris types to be used by AstroDog + save_ephemeris: If true saves and loads nav and orbit ephemeris to cache. + """ self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) self.gnss_kf = GNSSKalman(GENERATED_DIR, cython=True) @@ -45,7 +52,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_ self.load_cache() self.posfix_functions = {constellation: get_posfix_sympy_fun(constellation) for constellation in (ConstellationId.GPS, ConstellationId.GLONASS)} - self.last_pos_fix = last_known_position if last_known_position is not None else [] + self.last_pos_fix = [] self.last_pos_residual = [] self.last_pos_fix_t = None @@ -64,12 +71,16 @@ def load_cache(self): self.last_fetch_orbits_t = cache['last_fetch_orbits_t'] except json.decoder.JSONDecodeError: cloudlog.exception("Error parsing cache") + timestamp = self.last_fetch_orbits_t.as_datetime() if self.last_fetch_orbits_t is not None else 'Nan' + cloudlog.debug(f"Loaded nav and orbits cache with timestamp: {timestamp}. Unique orbit and nav sats: {list(cache['orbits'].keys())} {list(cache['nav'].keys())} " + + f"Total: {sum([len(v) for v in cache['orbits']])} and {sum([len(v) for v in cache['nav']])}") def cache_ephemeris(self, t: GPSTime): if self.save_ephemeris and (self.last_cached_t is None or t - self.last_cached_t > SECS_IN_MIN): put_nonblocking(EPHEMERIS_CACHE, json.dumps( {'version': CACHE_VERSION, 'last_fetch_orbits_t': self.last_fetch_orbits_t, 'orbits': self.astro_dog.orbits, 'nav': self.astro_dog.nav}, cls=CacheSerializer)) + cloudlog.debug("Cache saved") self.last_cached_t = t def get_est_pos(self, t, processed_measurements): @@ -130,9 +141,8 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement # Check time and outputs are valid valid = self.kf_valid(t) if not all(valid): - if not valid[0]: - cloudlog.info("Kalman filter uninitialized") - elif not valid[1]: + + if not valid[1]: cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") @@ -140,7 +150,6 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement cloudlog.info(f"Reset kalman filter with {est_pos}") self.init_gnss_localizer(est_pos) else: - cloudlog.info("Could not reset kalman filter") return if len(measurements) > 0: kf_add_observations(self.gnss_kf, t, measurements) @@ -189,8 +198,8 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): try: astro_dog.get_orbit_data(t, only_predictions=True) data = (astro_dog.orbits, astro_dog.orbit_fetched_times) - except RuntimeError as e: - cloudlog.warning(f"No orbit data found. {e}") + except (RuntimeError, ValueError, IOError) as e: + cloudlog.warning(f"No orbit data found or parsing failure: {e}") cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") return data @@ -241,7 +250,7 @@ def kf_add_observations(gnss_kf: GNSSKalman, t: float, measurements: List[GNSSMe ekf_data[ObservationKind.PSEUDORANGE_RATE_GPS] = ekf_data[ObservationKind.PSEUDORANGE_GPS] ekf_data[ObservationKind.PSEUDORANGE_RATE_GLONASS] = ekf_data[ObservationKind.PSEUDORANGE_GLONASS] for kind, data in ekf_data.items(): - if len(data) >0: + if len(data) > 0: gnss_kf.predict_and_observe(t, kind, data) @@ -278,7 +287,6 @@ def main(sm=None, pm=None): pm = messaging.PubMaster(['gnssMeasurements']) replay = "REPLAY" in os.environ - # todo get last_known_position use_internet = "LAIKAD_NO_INTERNET" not in os.environ laikad = Laikad(save_ephemeris=not replay, auto_fetch_orbits=use_internet) while True: diff --git a/selfdrive/locationd/laikad_helpers.py b/selfdrive/locationd/laikad_helpers.py index 81f5ac3dd676b1..f13e8e73bb22a7 100644 --- a/selfdrive/locationd/laikad_helpers.py +++ b/selfdrive/locationd/laikad_helpers.py @@ -86,4 +86,4 @@ def get_posfix_sympy_fun(constellation): res = [res] + [sympy.diff(res, v) for v in var] - return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res) + return sympy.lambdify([x, y, z, bc, bg, pr, sat_x, sat_y, sat_z, weight], res, modules=["numpy"]) diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index c353da9624e7e2..26c1d288203b4a 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -120,6 +120,18 @@ def test_laika_online(self): self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) + def test_kf_becomes_valid(self): + laikad = Laikad(auto_update=False) + m = self.logs[0] + self.assertFalse(all(laikad.kf_valid(m.logMonoTime * 1e-9))) + kf_valid = False + for m in self.logs: + laikad.process_ublox_msg(m.ubloxGnss, m.logMonoTime, block=True) + kf_valid = all(laikad.kf_valid(m.logMonoTime * 1e-9)) + if kf_valid: + break + self.assertTrue(kf_valid) + def test_laika_online_nav_only(self): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.NAV) # Disable fetch_orbits to test NAV only From c8e5912b6173c64fae18f79109eb72e8499908c9 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 1 Jul 2022 16:43:44 +0200 Subject: [PATCH 223/436] bump laika --- laika_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laika_repo b/laika_repo index 27a0d8a776fc8c..6e87f536dbe8cf 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 27a0d8a776fc8c1eaf8608d17ce81a00136f8bd0 +Subproject commit 6e87f536dbe8cf80040f724c89798e66ca17cf9d From 12f8237bfbdea668ae96ce6389cbe33ce1bea7f3 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 1 Jul 2022 16:50:18 +0200 Subject: [PATCH 224/436] process replay: Fix setting environment vars (#25015) Fix setting environments in process replay --- selfdrive/test/process_replay/process_replay.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index fc83b4b61f4461..9c45281b0c7c8a 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -398,8 +398,8 @@ def setup_env(simulation=False, CP=None, cfg=None): if cfg is not None: # Clear all custom processConfig environment variables - for cfg in CONFIGS: - for k, _ in cfg.environ.items(): + for config in CONFIGS: + for k, _ in config.environ.items(): if k in os.environ: del os.environ[k] From f10283072e4519ad95566dd0d54dbc7f52566f6d Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 1 Jul 2022 16:49:56 +0200 Subject: [PATCH 225/436] Laikad: dont log when filter is not initialized --- selfdrive/locationd/laikad.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 427eb3dbe0bf20..da00c7aa6f1370 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -141,8 +141,9 @@ def update_localizer(self, est_pos, t: float, measurements: List[GNSSMeasurement # Check time and outputs are valid valid = self.kf_valid(t) if not all(valid): - - if not valid[1]: + if not valid[0]: # Filter not initialized + pass + elif not valid[1]: cloudlog.error("Time gap of over 10s detected, gnss kalman reset") elif not valid[2]: cloudlog.error("Gnss kalman filter state is nan") From cdc7a6dbea75f10316e45833be6bade9f2e1694d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 1 Jul 2022 17:26:41 +0200 Subject: [PATCH 226/436] enable laikad (#25013) * enable laikad * increase logprint for onroad test --- selfdrive/controls/controlsd.py | 2 +- selfdrive/manager/process_config.py | 2 +- selfdrive/test/test_onroad.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 67eaf1588bcdec..cceb47947ab95f 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -38,7 +38,7 @@ SIMULATION = "SIMULATION" in os.environ NOSENSOR = "NOSENSOR" in os.environ IGNORE_PROCESSES = {"uploader", "deleter", "loggerd", "logmessaged", "tombstoned", "statsd", - "logcatd", "proclogd", "clocksd", "updated", "timezoned", "manage_athenad"} | \ + "logcatd", "proclogd", "clocksd", "updated", "timezoned", "manage_athenad", "laikad"} | \ {k for k, v in managed_processes.items() if not v.enabled} ThermalStatus = log.DeviceState.ThermalStatus diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 7e4029664d9280..dec51966a41dc8 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -41,6 +41,7 @@ def logging(started, params, CP: car.CarParams) -> bool: PythonProcess("controlsd", "selfdrive.controls.controlsd"), PythonProcess("deleter", "selfdrive.loggerd.deleter", offroad=True), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", enabled=(not PC or WEBCAM), callback=driverview), + PythonProcess("laikad", "selfdrive.locationd.laikad"), PythonProcess("navd", "selfdrive.navd.navd"), PythonProcess("pandad", "selfdrive.boardd.pandad", offroad=True), PythonProcess("paramsd", "selfdrive.locationd.paramsd"), @@ -57,7 +58,6 @@ def logging(started, params, CP: car.CarParams) -> bool: # Experimental PythonProcess("rawgpsd", "selfdrive.sensord.rawgps.rawgpsd", enabled=os.path.isfile("/persist/comma/use-quectel-rawgps")), - PythonProcess("laikad", "selfdrive.locationd.laikad", enabled=os.path.isfile("/persist/comma/use-laikad")), ] managed_processes = {p.name: p for p in procs} diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index a7ab55fe2a58bb..8007afb84cb751 100755 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -118,6 +118,7 @@ def setUpClass(cls): os.environ['REPLAY'] = "1" os.environ['SKIP_FW_QUERY'] = "1" os.environ['FINGERPRINT'] = "TOYOTA COROLLA TSS2 2019" + os.environ['LOGPRINT'] = 'debug' params = Params() params.clear_all() From 8d6799d95a0a0ab9b2b0bc4ed6998abf6cabf266 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 1 Jul 2022 17:44:10 +0200 Subject: [PATCH 227/436] Laikad: Allow fetching orbits every minute (#25016) * Allow fetching orbits every minute * Small cleanup --- selfdrive/locationd/laikad.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index da00c7aa6f1370..0df48dd8935830 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -13,7 +13,7 @@ from cereal import log, messaging from common.params import Params, put_nonblocking from laika import AstroDog -from laika.constants import SECS_IN_HR, SECS_IN_MIN +from laika.constants import SECS_IN_MIN from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId @@ -171,7 +171,7 @@ def init_gnss_localizer(self, est_pos): self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) def fetch_orbits(self, t: GPSTime, block): - if t not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or t - self.last_fetch_orbits_t > SECS_IN_HR): + if t not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or t - self.last_fetch_orbits_t > SECS_IN_MIN): astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types ret = None @@ -195,14 +195,12 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") start_time = time.monotonic() - data = None try: astro_dog.get_orbit_data(t, only_predictions=True) - data = (astro_dog.orbits, astro_dog.orbit_fetched_times) + cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") + return astro_dog.orbits, astro_dog.orbit_fetched_times except (RuntimeError, ValueError, IOError) as e: cloudlog.warning(f"No orbit data found or parsing failure: {e}") - cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") - return data def create_measurement_msg(meas: GNSSMeasurement): From fcc5b3d70c14b05e642eda93a528f7583f986468 Mon Sep 17 00:00:00 2001 From: Jason Shuler Date: Fri, 1 Jul 2022 16:45:32 -0400 Subject: [PATCH 228/436] GM: values.py cleanup & minor updates (#24908) * Comment update on static limits * Astra FP cleanup * DBC autogen & customizable * Add new Escalade FP, disable bad * Add DROPPED CanBus value * Update/cleanup CarInfo * DBC -> defaultdict * Fix DBC typing issue * Revert Escalade fix * clean up * comment spacing * revert this for now Co-authored-by: Shane Smiskol --- selfdrive/car/gm/values.py | 43 ++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 8ef33e8297c487..328ca7c286d24d 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass +from collections import defaultdict +from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Union @@ -9,9 +10,9 @@ class CarControllerParams: - STEER_MAX = 300 # Safety limit, not LKA max. Trucks use 600. - STEER_STEP = 2 # control frames per command - STEER_DELTA_UP = 7 + STEER_MAX = 300 # GM limit is 3Nm. Used by carcontroller to generate LKA output + STEER_STEP = 2 # Control frames per command (50hz) + STEER_DELTA_UP = 7 # Delta rates require review due to observed EPS weakness STEER_DELTA_DOWN = 17 MIN_STEER_SPEED = 3. # m/s STEER_DRIVER_ALLOWANCE = 50 @@ -24,19 +25,20 @@ class CarControllerParams: CAMERA_KEEPALIVE_STEP = 100 # Volt gasbrake lookups - MAX_GAS = 3072 # Safety limit, not ACC max. Stock ACC >4096 from standstill. - ZERO_GAS = 2048 # Coasting - MAX_BRAKE = 350 # ~ -3.5 m/s^2 with regen + # TODO: These values should be confirmed on non-Volt vehicles + MAX_GAS = 3072 # Safety limit, not ACC max. Stock ACC >4096 from standstill. + ZERO_GAS = 2048 # Coasting + MAX_BRAKE = 350 # ~ -3.5 m/s^2 with regen + MAX_ACC_REGEN = 1404 # Max ACC regen is slightly less than max paddle regen # Allow small margin below -3.5 m/s^2 from ISO 15622:2018 since we # perform the closed loop control, and might need some # to apply some more braking if we're on a downhill slope. # Our controller should still keep the 2 second average above # -3.5 m/s^2 as per planner limits - ACCEL_MAX = 2. # m/s^2 - ACCEL_MIN = -4. # m/s^2 + ACCEL_MAX = 2. # m/s^2 + ACCEL_MIN = -4. # m/s^2 - MAX_ACC_REGEN = 1404 # Max ACC regen is slightly less than max paddle regen GAS_LOOKUP_BP = [-1., 0., ACCEL_MAX] GAS_LOOKUP_V = [MAX_ACC_REGEN, ZERO_GAS, MAX_GAS] BRAKE_LOOKUP_BP = [ACCEL_MIN, -1.] @@ -67,16 +69,17 @@ class Footnote(Enum): class GMCarInfo(CarInfo): package: str = "Adaptive Cruise" harness: Enum = Harness.none + footnotes: List[Enum] = field(default_factory=lambda: [Footnote.OBD_II]) CAR_INFO: Dict[str, Union[GMCarInfo, List[GMCarInfo]]] = { CAR.HOLDEN_ASTRA: GMCarInfo("Holden Astra 2017", harness=Harness.custom), - CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", footnotes=[Footnote.OBD_II], min_enable_speed=0, harness=Harness.custom), + CAR.VOLT: GMCarInfo("Chevrolet Volt 2017-18", min_enable_speed=0, harness=Harness.custom), CAR.CADILLAC_ATS: GMCarInfo("Cadillac ATS Premium Performance 2018"), CAR.MALIBU: GMCarInfo("Chevrolet Malibu Premier 2017", harness=Harness.custom), - CAR.ACADIA: GMCarInfo("GMC Acadia 2018", video_link="https://www.youtube.com/watch?v=0ZN6DdsBUZo", footnotes=[Footnote.OBD_II]), + CAR.ACADIA: GMCarInfo("GMC Acadia 2018", video_link="https://www.youtube.com/watch?v=0ZN6DdsBUZo"), CAR.BUICK_REGAL: GMCarInfo("Buick Regal Essence 2018"), - CAR.ESCALADE_ESV: GMCarInfo("Cadillac Escalade ESV 2016", "ACC + LKAS", footnotes=[Footnote.OBD_II]), + CAR.ESCALADE_ESV: GMCarInfo("Cadillac Escalade ESV 2016", "ACC + LKAS"), } @@ -100,10 +103,12 @@ class CanBus: CHASSIS = 2 SW_GMLAN = 3 LOOPBACK = 128 + DROPPED = 192 FINGERPRINTS = { + CAR.HOLDEN_ASTRA: [ # Astra BK MY17, ASCM unplugged - CAR.HOLDEN_ASTRA: [{ + { 190: 8, 193: 8, 197: 8, 199: 4, 201: 8, 209: 7, 211: 8, 241: 6, 249: 8, 288: 5, 298: 8, 304: 1, 309: 8, 311: 8, 313: 8, 320: 3, 328: 1, 352: 5, 381: 6, 384: 4, 386: 8, 388: 8, 393: 8, 398: 8, 401: 8, 413: 8, 417: 8, 419: 8, 422: 1, 426: 7, 431: 8, 442: 8, 451: 8, 452: 8, 453: 8, 455: 7, 456: 8, 458: 5, 479: 8, 481: 7, 485: 8, 489: 8, 497: 8, 499: 3, 500: 8, 501: 8, 508: 8, 528: 5, 532: 6, 554: 3, 560: 8, 562: 8, 563: 5, 564: 5, 565: 5, 567: 5, 647: 5, 707: 8, 715: 8, 723: 8, 753: 5, 761: 7, 806: 1, 810: 8, 840: 5, 842: 5, 844: 8, 866: 4, 961: 8, 969: 8, 977: 8, 979: 8, 985: 5, 1001: 8, 1009: 8, 1011: 6, 1017: 8, 1019: 3, 1020: 8, 1105: 6, 1217: 8, 1221: 5, 1225: 8, 1233: 8, 1249: 8, 1257: 6, 1259: 8, 1261: 7, 1263: 4, 1265: 8, 1267: 8, 1280: 4, 1300: 8, 1328: 4, 1417: 8, 1906: 7, 1907: 7, 1908: 7, 1912: 7, 1919: 7, }], CAR.VOLT: [ @@ -145,12 +150,4 @@ class CanBus: }], } -DBC = { - CAR.HOLDEN_ASTRA: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.VOLT: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.MALIBU: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.ACADIA: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.CADILLAC_ATS: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.BUICK_REGAL: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), - CAR.ESCALADE_ESV: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis'), -} +DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict('gm_global_a_powertrain_generated', 'gm_global_a_object', chassis_dbc='gm_global_a_chassis')) From e11bb76a6408c0ceeacd0b4dd7e6ccb7a133387a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 1 Jul 2022 19:11:28 -0700 Subject: [PATCH 229/436] car docs: remove steering torque hard-coding (#25019) remove good torque hardcoding --- selfdrive/car/docs_definitions.py | 4 ++-- selfdrive/car/hyundai/values.py | 1 - selfdrive/car/mazda/values.py | 4 ++-- selfdrive/car/subaru/values.py | 2 +- selfdrive/car/toyota/values.py | 1 - selfdrive/car/volkswagen/values.py | 1 - 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 618c986d185730..1efa23037fd08b 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -55,7 +55,6 @@ class CarInfo: footnotes: Optional[List[Enum]] = None min_steer_speed: Optional[float] = None min_enable_speed: Optional[float] = None - good_torque: bool = False harness: Optional[Enum] = None def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dict[Enum, int]): @@ -82,10 +81,11 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic Column.LONGITUDINAL: Star.FULL if CP.openpilotLongitudinalControl and not CP.radarOffCan else Star.EMPTY, Column.FSR_LONGITUDINAL: Star.FULL if min_enable_speed <= 0. else Star.EMPTY, Column.FSR_STEERING: Star.FULL if min_steer_speed <= 0. else Star.EMPTY, - Column.STEERING_TORQUE: Star.FULL if self.good_torque else Star.EMPTY, # TODO: remove hardcoding and use maxLateralAccel + # Column.STEERING_TORQUE set below Column.MAINTAINED: Star.FULL if CP.carFingerprint not in non_tested_cars and self.harness is not Harness.none else Star.EMPTY, } + # Set steering torque star from max lateral acceleration if not math.isnan(CP.maxLateralAccel): if CP.maxLateralAccel >= GREAT_TORQUE_THRESHOLD: self.row[Column.STEERING_TORQUE] = Star.FULL diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 645d2f523ba621..0717ed1fbe4843 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -89,7 +89,6 @@ class HyundaiCarInfo(CarInfo): # TODO: we can probably remove LKAS. LKAS is standard on many # HKG and for others, it's likely packaged together with SCC package: str = "SCC + LKAS" - good_torque: bool = True CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = { diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index f5d63418f1c70d..09b9b7732b6262 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -40,8 +40,8 @@ class MazdaCarInfo(CarInfo): CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-17"), CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017"), CAR.MAZDA6: MazdaCarInfo("Mazda 6 2017"), - CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021", good_torque=True), - CAR.CX5_2022: MazdaCarInfo("Mazda CX-5 2022", good_torque=True), + CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021"), + CAR.CX5_2022: MazdaCarInfo("Mazda CX-5 2022"), } diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 9badc3ca50f79c..45358eb3a43cdd 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -41,7 +41,7 @@ class SubaruCarInfo(CarInfo): CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { - CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All", good_torque=True), + CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All"), CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index c7a26257f39ab9..43d923338e0f3e 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -99,7 +99,6 @@ class Footnote(Enum): class ToyotaCarInfo(CarInfo): package: str = "All" harness: Enum = Harness.toyota - good_torque: bool = True CAR_INFO: Dict[str, Union[ToyotaCarInfo, List[ToyotaCarInfo]]] = { diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 0148a138a29df9..6e64f705b0e25b 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -114,7 +114,6 @@ class Footnote(Enum): @dataclass class VWCarInfo(CarInfo): package: str = "Driver Assistance" - good_torque: bool = True harness: Enum = Harness.vw From 0c95493dc0cbc5d3dec59a50ac5476573041942d Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Fri, 1 Jul 2022 19:14:21 -0700 Subject: [PATCH 230/436] Torque control: max torque warning (#25018) * New steer torque warning * typo --- selfdrive/controls/controlsd.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index cceb47947ab95f..3ee47c620b6cdc 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -317,7 +317,7 @@ def update_events(self, CS): self.events.add(EventName.commIssue) elif not self.sm.all_freq_ok(): self.events.add(EventName.commIssueAvgFreq) - else: # invalid or can_rcv_error. + else: # invalid or can_rcv_error. self.events.add(EventName.commIssue) logs = { @@ -598,7 +598,14 @@ def state_control(self, CS): lac_log.saturated = abs(actuators.steer) >= 0.9 # Send a "steering required alert" if saturation count has reached the limit - if lac_log.active and lac_log.saturated and not CS.steeringPressed: + if lac_log.active and not CS.steeringPressed and self.CP.lateralTuning.which() == 'torque': + undershooting = abs(lac_log.desiredLateralAccel) / abs(1e-3 + lac_log.actualLateralAccel) > 1.2 + turning = abs(lac_log.desiredLateralAccel) > 1.0 + good_speed = CS.vEgo > 5 + max_torque = abs(self.last_actuators.steer) > 0.99 + if undershooting and turning and good_speed and max_torque: + self.events.add(EventName.steerSaturated) + elif lac_log.active and lac_log.saturated and not CS.steeringPressed: dpath_points = lat_plan.dPathPoints if len(dpath_points): # Check if we deviated from the path From 5f794fe49a1e0f2849e6a979c7bd12ca5ddb060a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 4 Jul 2022 19:55:00 +0800 Subject: [PATCH 231/436] settings.cc: remove function network_panel (#25030) remove function network_panel --- selfdrive/ui/qt/offroad/settings.cc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 547ad168f1a667..a03af23951b1f8 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -279,10 +279,6 @@ void SoftwarePanel::updateLabels() { osVersionLbl->setText(QString::fromStdString(Hardware::get_os_version()).trimmed()); } -QWidget *network_panel(QWidget *parent) { - return new Networking(parent); -} - void SettingsWindow::showEvent(QShowEvent *event) { panel_widget->setCurrentIndex(0); nav_btns->buttons()[0]->setChecked(true); @@ -328,7 +324,7 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) { QList> panels = { {tr("Device"), device}, - {tr("Network"), network_panel(this)}, + {tr("Network"), new Networking(this)}, {tr("Toggles"), new TogglesPanel(this)}, {tr("Software"), new SoftwarePanel(this)}, }; From 735387d5eea7e76d1940a82b0b5dcaddf1eb264a Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 4 Jul 2022 13:57:25 +0200 Subject: [PATCH 232/436] bump opendbc --- opendbc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opendbc b/opendbc index 7fbf7c2a685a90..9c7248ceb26992 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 7fbf7c2a685a90ff01e744cfb410f1a8a3c06278 +Subproject commit 9c7248ceb269928e3741103f978e9e4de2b38156 From d4cc13c88a8a9a9215777709fe2eccade1fd0ccd Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 4 Jul 2022 17:13:30 +0200 Subject: [PATCH 233/436] controlsd: avoid lag on first iteration due to get_short_branch (#25031) * controlsd: avoid lag on first iteration due to get_short_branch * always cache --- selfdrive/controls/controlsd.py | 4 ++++ selfdrive/controls/lib/events.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 3ee47c620b6cdc..9e3af9eb636972 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -12,6 +12,7 @@ from common.conversions import Conversions as CV from panda import ALTERNATIVE_EXPERIENCE from system.swaglog import cloudlog +from system.version import get_short_branch from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can from selfdrive.controls.lib.lane_planner import CAMERA_OFFSET @@ -62,6 +63,9 @@ class Controls: def __init__(self, sm=None, pm=None, can_sock=None, CI=None): config_realtime_process(4, Priority.CTRL_HIGH) + # Ensure the current branch is cached, otherwise the first iteration of controlsd lags + self.branch = get_short_branch("") + # Setup sockets self.pm = pm if self.pm is None: diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index cc63d4995d7071..95ccb7b7ecd105 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -222,7 +222,7 @@ def func(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: b return func def startup_master_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int) -> Alert: - branch = get_short_branch("") + branch = get_short_branch("") # Ensure get_short_branch is cached to avoid lags on startup if "REPLAY" in os.environ: branch = "replay" From 39007810927faa309d06e6ad9586302341547f64 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 5 Jul 2022 16:55:32 +0200 Subject: [PATCH 234/436] add casync support to agnos updater (#23654) * add casync option to agnos updater * open if necessary * add python implementation * last chunk can be small * check flags * cleaner check * add remote and file stores * remote caibx file * print stats * use python implementation * clean up imports * add progress * fix logging * fix duplicate chunks * add comments * json stats * cleanup tmp * normal image is still sparse * Update system/hardware/tici/agnos.py Co-authored-by: Adeeb Shihadeh * Update system/hardware/tici/agnos.py Co-authored-by: Adeeb Shihadeh * add some types * remove comment * create Chunk type * make readers a class * try agnos 5.2 * add download retries * catch all exceptions * sleep between retry * revert agnos.json changes Co-authored-by: Adeeb Shihadeh --- system/hardware/tici/agnos.py | 95 ++++++++++++---- system/hardware/tici/casync.py | 192 +++++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 22 deletions(-) create mode 100755 system/hardware/tici/casync.py diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index e664654c65ed18..527422aca75769 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 +import hashlib import json import lzma -import hashlib -import requests +import os import struct import subprocess import time -import os -from typing import Dict, Generator, Union +from typing import Dict, Generator, List, Tuple, Union + +import requests + +import system.hardware.tici.casync as casync SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') @@ -74,6 +77,7 @@ def unsparsify(f: StreamingDecompressor) -> Generator[bytes, None, None]: else: raise Exception("Unhandled sparse chunk type") + # noop wrapper with same API as unsparsify() for non sparse images def noop(f: StreamingDecompressor) -> Generator[bytes, None, None]: while not f.eof: @@ -99,8 +103,8 @@ def get_partition_path(target_slot_number: int, partition: dict) -> str: return path -def verify_partition(target_slot_number: int, partition: Dict[str, Union[str, int]]) -> bool: - full_check = partition['full_check'] +def verify_partition(target_slot_number: int, partition: Dict[str, Union[str, int]], force_full_check: bool = False) -> bool: + full_check = partition['full_check'] or force_full_check path = get_partition_path(target_slot_number, partition) if not isinstance(partition['size'], int): return False @@ -135,21 +139,10 @@ def clear_partition_hash(target_slot_number: int, partition: dict) -> None: os.sync() -def flash_partition(target_slot_number: int, partition: dict, cloudlog): - cloudlog.info(f"Downloading and writing {partition['name']}") - - if verify_partition(target_slot_number, partition): - cloudlog.info(f"Already flashed {partition['name']}") - return - +def extract_compressed_image(target_slot_number: int, partition: dict, cloudlog): + path = get_partition_path(target_slot_number, partition) downloader = StreamingDecompressor(partition['url']) - # Clear hash before flashing in case we get interrupted - full_check = partition['full_check'] - if not full_check: - clear_partition_hash(target_slot_number, partition) - - path = get_partition_path(target_slot_number, partition) with open(path, 'wb+') as out: # Flash partition last_p = 0 @@ -172,9 +165,67 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog): if out.tell() != partition['size']: raise Exception("Uncompressed size mismatch") - # Write hash after successfull flash os.sync() - if not full_check: + + +def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): + path = get_partition_path(target_slot_number, partition) + seed_path = path[:-1] + ('b' if path[-1] == 'a' else 'a') + + target = casync.parse_caibx(partition['casync_caibx']) + + sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] + + # First source is the current partition. Index file for current version is provided in the manifest + if 'casync_seed_caibx' in partition: + sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(partition['casync_seed_caibx'])))] + + # Second source is the target partition, this allows for resuming + sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] + + # Finally we add the remote source to download any missing chunks + sources += [('remote', casync.RemoteChunkReader(partition['casync_store']), casync.build_chunk_dict(target))] + + last_p = 0 + + def progress(cur): + nonlocal last_p + p = int(cur / partition['size'] * 100) + if p != last_p: + last_p = p + print(f"Installing {partition['name']}: {p}", flush=True) + + stats = casync.extract(target, sources, path, progress) + cloudlog.error(f'casync done {json.dumps(stats)}') + + os.sync() + if not verify_partition(target_slot_number, partition, force_full_check=True): + raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") + + +def flash_partition(target_slot_number: int, partition: dict, cloudlog): + cloudlog.info(f"Downloading and writing {partition['name']}") + + if verify_partition(target_slot_number, partition): + cloudlog.info(f"Already flashed {partition['name']}") + return + + # Clear hash before flashing in case we get interrupted + full_check = partition['full_check'] + if not full_check: + clear_partition_hash(target_slot_number, partition) + + path = get_partition_path(target_slot_number, partition) + + if 'casync_caibx' in partition: + extract_casync_image(target_slot_number, partition, cloudlog) + else: + extract_compressed_image(target_slot_number, partition, cloudlog) + + # Write hash after successfull flash + if not full_check: + with open(path, 'wb+') as out: + out.seek(partition['size']) out.write(partition['hash_raw'].lower().encode()) @@ -228,8 +279,8 @@ def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool: if __name__ == "__main__": - import logging import argparse + import logging parser = argparse.ArgumentParser(description="Flash and verify AGNOS update", formatter_class=argparse.ArgumentDefaultsHelpFormatter) diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py new file mode 100755 index 00000000000000..e77f473636e3e0 --- /dev/null +++ b/system/hardware/tici/casync.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +import io +import lzma +import os +import struct +import sys +import time +from abc import ABC, abstractmethod +from collections import defaultdict, namedtuple +from typing import Callable, Dict, List, Optional, Tuple + +import requests +from Crypto.Hash import SHA512 + +CA_FORMAT_INDEX = 0x96824d9c7b129ff9 +CA_FORMAT_TABLE = 0xe75b9e112f17417d +CA_FORMAT_TABLE_TAIL_MARKER = 0xe75b9e112f17417 +FLAGS = 0xb000000000000000 + +CA_HEADER_LEN = 48 +CA_TABLE_HEADER_LEN = 16 +CA_TABLE_ENTRY_LEN = 40 +CA_TABLE_MIN_LEN = CA_TABLE_HEADER_LEN + CA_TABLE_ENTRY_LEN + +CHUNK_DOWNLOAD_TIMEOUT = 10 +CHUNK_DOWNLOAD_RETRIES = 3 + +CAIBX_DOWNLOAD_TIMEOUT = 120 + +Chunk = namedtuple('Chunk', ['sha', 'offset', 'length']) +ChunkDict = Dict[bytes, Chunk] + + +class ChunkReader(ABC): + @abstractmethod + def read(self, chunk: Chunk) -> bytes: + ... + + +class FileChunkReader(ChunkReader): + """Reads chunks from a local file""" + def __init__(self, fn: str) -> None: + + super().__init__() + self.f = open(fn, 'rb') + + def read(self, chunk: Chunk) -> bytes: + self.f.seek(chunk.offset) + return self.f.read(chunk.length) + + +class RemoteChunkReader(ChunkReader): + """Reads lzma compressed chunks from a remote store""" + + def __init__(self, url: str) -> None: + super().__init__() + self.url = url + + def read(self, chunk: Chunk) -> bytes: + sha_hex = chunk.sha.hex() + url = os.path.join(self.url, sha_hex[:4], sha_hex + ".cacnk") + + for i in range(CHUNK_DOWNLOAD_RETRIES): + try: + resp = requests.get(url, timeout=CHUNK_DOWNLOAD_TIMEOUT) + break + except Exception: + if i == CHUNK_DOWNLOAD_RETRIES - 1: + raise + time.sleep(CHUNK_DOWNLOAD_TIMEOUT) + + resp.raise_for_status() + + decompressor = lzma.LZMADecompressor(format=lzma.FORMAT_AUTO) + return decompressor.decompress(resp.content) + + +def parse_caibx(caibx_path: str) -> List[Chunk]: + """Parses the chunks from a caibx file. Can handle both local and remote files. + Returns a list of chunks with hash, offset and length""" + if os.path.isfile(caibx_path): + caibx = open(caibx_path, 'rb') + else: + resp = requests.get(caibx_path, timeout=CAIBX_DOWNLOAD_TIMEOUT) + resp.raise_for_status() + caibx = io.BytesIO(resp.content) + + caibx.seek(0, os.SEEK_END) + caibx_len = caibx.tell() + caibx.seek(0, os.SEEK_SET) + + # Parse header + length, magic, flags, min_size, _, max_size = struct.unpack("= min_size + + chunks.append(Chunk(sha, offset, length)) + offset = new_offset + + return chunks + + +def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict: + """Turn a list of chunks into a dict for faster lookups based on hash""" + return {c.sha: c for c in chunks} + + +def extract(target: List[Chunk], + sources: List[Tuple[str, ChunkReader, ChunkDict]], + out_path: str, + progress: Optional[Callable[[int], None]] = None): + stats: Dict[str, int] = defaultdict(int) + + with open(out_path, 'wb') as out: + for cur_chunk in target: + + # Find source for desired chunk + for name, chunk_reader, store_chunks in sources: + if cur_chunk.sha in store_chunks: + bts = chunk_reader.read(store_chunks[cur_chunk.sha]) + + # Check length + if len(bts) != cur_chunk.length: + continue + + # Check hash + if SHA512.new(bts, truncate="256").digest() != cur_chunk.sha: + continue + + # Write to output + out.seek(cur_chunk.offset) + out.write(bts) + + stats[name] += cur_chunk.length + + if progress is not None: + progress(sum(stats.values())) + + break + else: + raise RuntimeError("Desired chunk not found in provided stores") + + return stats + + +def print_stats(stats: Dict[str, int]): + total_bytes = sum(stats.values()) + print(f"Total size: {total_bytes / 1024 / 1024:.2f} MB") + for name, total in stats.items(): + print(f" {name}: {total / 1024 / 1024:.2f} MB ({total / total_bytes * 100:.1f}%)") + + +def extract_simple(caibx_path, out_path, store_path): + # (name, callback, chunks) + target = parse_caibx(caibx_path) + sources = [ + # (store_path, RemoteChunkReader(store_path), build_chunk_dict(target)), + (store_path, FileChunkReader(store_path), build_chunk_dict(target)), + ] + + return extract(target, sources, out_path) + + +if __name__ == "__main__": + caibx = sys.argv[1] + out = sys.argv[2] + store = sys.argv[3] + + stats = extract_simple(caibx, out, store) + print_stats(stats) From 06a8ac627c0b8cf00b3bd4ea7186f5c876f71561 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 5 Jul 2022 18:06:08 +0200 Subject: [PATCH 235/436] casync: build_chunk_dict optimize for resuming (#25038) --- system/hardware/tici/casync.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py index e77f473636e3e0..b857c047952fde 100755 --- a/system/hardware/tici/casync.py +++ b/system/hardware/tici/casync.py @@ -123,8 +123,13 @@ def parse_caibx(caibx_path: str) -> List[Chunk]: def build_chunk_dict(chunks: List[Chunk]) -> ChunkDict: - """Turn a list of chunks into a dict for faster lookups based on hash""" - return {c.sha: c for c in chunks} + """Turn a list of chunks into a dict for faster lookups based on hash. + Keep first chunk since it's more likely to be already downloaded.""" + r = {} + for c in chunks: + if c.sha not in r: + r[c.sha] = c + return r def extract(target: List[Chunk], From 0bf1462ad0a459f8a2cc863f58089540789922f1 Mon Sep 17 00:00:00 2001 From: martinl Date: Tue, 5 Jul 2022 20:33:20 +0300 Subject: [PATCH 236/436] Update path for github workflow hardware unit tests (#25035) * Update hardware path for github workflow unit tests * Update release/files_common Co-authored-by: Adeeb Shihadeh --- .github/workflows/selfdrive_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 0ff0092b02cbcf..35a08d4fe9a44d 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -302,7 +302,7 @@ jobs: selfdrive/locationd/test/_test_locationd_lib.py && \ $UNIT_TEST selfdrive/athena && \ $UNIT_TEST selfdrive/thermald && \ - $UNIT_TEST selfdrive/hardware/tici && \ + $UNIT_TEST system/hardware/tici && \ $UNIT_TEST selfdrive/modeld && \ $UNIT_TEST tools/lib/tests && \ ./selfdrive/ui/tests/create_test_translations.sh && \ From d4f4809992c5c73e36ec1041445e6a11b2c68448 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 5 Jul 2022 14:28:54 -0700 Subject: [PATCH 237/436] always show avg power --- tools/zookeeper/power_monitor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/zookeeper/power_monitor.py b/tools/zookeeper/power_monitor.py index f88741813eab65..fa1f442bbc4ca2 100755 --- a/tools/zookeeper/power_monitor.py +++ b/tools/zookeeper/power_monitor.py @@ -27,8 +27,10 @@ while duration is None or time.monotonic() - start_time < duration: fltr.update(z.read_power()) if rk.frame % rate == 0: - print(f"{fltr.x:.2f} W") measurements.append(fltr.x) + t = datetime.timedelta(seconds=time.monotonic() - start_time) + avg = sum(measurements) / len(measurements) + print(f"Now: {fltr.x:.2f} W, Avg: {avg:.2f} W over {t}") rk.keep_time() except KeyboardInterrupt: pass From 88a30004e0bf96601e5b8fc4c084219c52a46e15 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 5 Jul 2022 16:40:47 -0700 Subject: [PATCH 238/436] Chrysler: prep for Ram port (#25040) * Chrysler: prep for Ram port * bump opendbc * opendbc master * bump panda --- opendbc | 2 +- panda | 2 +- release/files_common | 4 +--- selfdrive/car/chrysler/carstate.py | 20 ++++++++++---------- selfdrive/car/chrysler/chryslercan.py | 6 +++--- selfdrive/car/chrysler/values.py | 14 +++++++------- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/opendbc b/opendbc index 9c7248ceb26992..b2895650c744e2 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 9c7248ceb269928e3741103f978e9e4de2b38156 +Subproject commit b2895650c744e24d48cee2f13563dcd5b030a271 diff --git a/panda b/panda index 265245389208e1..6c0d0b43c239b8 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 265245389208e1e6ada86b169e879c0a2e30426c +Subproject commit 6c0d0b43c239b89baa83b4a1885d0ce21ab2335e diff --git a/release/files_common b/release/files_common index acf74e21375be4..e32277dfd8250e 100644 --- a/release/files_common +++ b/release/files_common @@ -35,7 +35,6 @@ common/filter_simple.py common/stat_live.py common/spinner.py common/text_window.py -common/SConscript common/kalman/.gitignore common/kalman/* @@ -217,7 +216,6 @@ selfdrive/locationd/generated/gps.h selfdrive/locationd/laikad.py selfdrive/locationd/laikad_helpers.py -selfdrive/locationd/locationd.cc selfdrive/locationd/locationd.h selfdrive/locationd/locationd.cc selfdrive/locationd/paramsd.py @@ -475,7 +473,7 @@ opendbc/can/parser_pyx.pyx opendbc/comma_body.dbc -opendbc/chrysler_pacifica_2017_hybrid.dbc +opendbc/chrysler_pacifica_2017_hybrid_generated.dbc opendbc/chrysler_pacifica_2017_hybrid_private_fusion.dbc opendbc/gm_global_a_powertrain_generated.dbc diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 444557191a1872..aa46ea0d9434f5 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -64,14 +64,14 @@ def update(self, cp, cp_cam): steer_state = cp.vl["EPS_2"]["LKAS_STATE"] ret.steerFaultPermanent = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) - ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]["HIGH_BEAM_FLASH"]) + ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]["HIGH_BEAM_PRESSED"]) if self.CP.enableBsm: - ret.leftBlindspot = cp.vl["BLIND_SPOT_WARNINGS"]["BLIND_SPOT_LEFT"] == 1 - ret.rightBlindspot = cp.vl["BLIND_SPOT_WARNINGS"]["BLIND_SPOT_RIGHT"] == 1 + ret.leftBlindspot = cp.vl["BSM_1"]["LEFT_STATUS"] == 1 + ret.rightBlindspot = cp.vl["BSM_1"]["RIGHT_STATUS"] == 1 self.lkas_counter = cp_cam.vl["LKAS_COMMAND"]["COUNTER"] - self.lkas_car_model = cp_cam.vl["LKAS_HUD"]["CAR_MODEL"] + self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"] self.lkas_status_ok = cp_cam.vl["LKAS_HEARTBIT"]["LKAS_STATUS_OK"] self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"] @@ -100,7 +100,7 @@ def get_can_parser(CP): ("ACC_AVAILABLE", "DAS_3"), ("ACC_ACTIVE", "DAS_3"), ("ACC_FAULTED", "DAS_3"), - ("HIGH_BEAM_FLASH", "STEERING_LEVERS"), + ("HIGH_BEAM_PRESSED", "STEERING_LEVERS"), ("ACC_SPEED_CONFIG_KPH", "DAS_4"), ("CRUISE_STATE", "DAS_4"), ("COLUMN_TORQUE", "EPS_2"), @@ -132,10 +132,10 @@ def get_can_parser(CP): if CP.enableBsm: signals += [ - ("BLIND_SPOT_RIGHT", "BLIND_SPOT_WARNINGS"), - ("BLIND_SPOT_LEFT", "BLIND_SPOT_WARNINGS"), + ("RIGHT_STATUS", "BSM_1"), + ("LEFT_STATUS", "BSM_1"), ] - checks.append(("BLIND_SPOT_WARNINGS", 2)) + checks.append(("BSM_1", 2)) return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 0) @@ -144,13 +144,13 @@ def get_cam_can_parser(CP): signals = [ # sig_name, sig_address ("COUNTER", "LKAS_COMMAND"), - ("CAR_MODEL", "LKAS_HUD"), + ("CAR_MODEL", "DAS_6"), ("LKAS_STATUS_OK", "LKAS_HEARTBIT") ] checks = [ ("LKAS_COMMAND", 100), ("LKAS_HEARTBIT", 10), - ("LKAS_HUD", 4), + ("DAS_6", 4), ] return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 2) diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index 53b79cab73e566..adcd411d317d07 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -35,14 +35,14 @@ def create_lkas_hud(packer, gear, lkas_active, hud_alert, hud_count, lkas_car_mo "LKAS_ALERTS": alerts, # byte 3, last 4 bits } - return packer.make_can_msg("LKAS_HUD", 0, values) # 0x2a6 + return packer.make_can_msg("DAS_6", 0, values) # 0x2a6 def create_lkas_command(packer, apply_steer, moving_fast, frame): # LKAS_COMMAND 0x292 (658) Lane-keeping signal to turn the wheel. values = { - "LKAS_STEERING_TORQUE": apply_steer, - "LKAS_HIGH_TORQUE": int(moving_fast), + "STEERING_TORQUE": apply_steer, + "LKAS_CONTROL_BIT": int(moving_fast), "COUNTER": frame % 0x10, } return packer.make_can_msg("LKAS_COMMAND", 0, values) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index d624bd27272fc7..5537b383d30f89 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -101,13 +101,13 @@ class ChryslerCarInfo(CarInfo): DBC = { - CAR.PACIFICA_2017_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.PACIFICA_2018: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.JEEP_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), - CAR.JEEP_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.PACIFICA_2017_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.PACIFICA_2018: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.PACIFICA_2020: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.PACIFICA_2018_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.JEEP_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.JEEP_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), } STEER_THRESHOLD = 120 From eaa8b08510057c089520f4ba410218be261c9158 Mon Sep 17 00:00:00 2001 From: martinl Date: Wed, 6 Jul 2022 02:42:53 +0300 Subject: [PATCH 239/436] Subaru: XV is supported (#25034) * Subaru: add XV to supported models * Update docs --- docs/CARS.md | 6 ++++-- selfdrive/car/subaru/values.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 25de6a2549272c..0cf384df183922 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -71,7 +71,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 67 cars +# Silver - 68 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -120,6 +120,7 @@ How We Rate The Cars |Subaru|Crosstrek 2020-21|EyeSight|||||| |Subaru|Forester 2019-21|All|||||| |Subaru|Impreza 2020-21|EyeSight|||||| +|Subaru|XV 2020-21|EyeSight|||||| |Toyota|Alphard 2019-20|All|||||| |Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| @@ -143,7 +144,7 @@ How We Rate The Cars |Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Polo 2020|Driver Assistance|||||| -# Bronze - 78 cars +# Bronze - 79 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -199,6 +200,7 @@ How We Rate The Cars |Mazda|CX-9 2021|All|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| +|Subaru|XV 2018-19|EyeSight|||||| |Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| |Škoda|Karoq 2019|Driver Assistance|||||| |Škoda|Kodiaq 2018-19|Driver Assistance|||||| diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 45358eb3a43cdd..ea923b1b5027ed 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -45,10 +45,12 @@ class SubaruCarInfo(CarInfo): CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), + SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), ], CAR.IMPREZA_2020: [ SubaruCarInfo("Subaru Impreza 2020-21"), SubaruCarInfo("Subaru Crosstrek 2020-21"), + SubaruCarInfo("Subaru XV 2020-21"), ], CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), From f21b56f25af5ee0930dc8f991aac4e9c438b3cda Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 5 Jul 2022 20:04:48 -0700 Subject: [PATCH 240/436] regen migration: use Panda safety parameters (#25043) no magic numbers --- selfdrive/test/process_replay/regen.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index d2f239c249b0b1..793e548705d8ee 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -15,6 +15,8 @@ from common.params import Params from common.realtime import Ratekeeper, DT_MDL, DT_DMON, sec_since_boot from common.transformations.camera import eon_f_frame_size, eon_d_frame_size, tici_f_frame_size, tici_d_frame_size +from panda.python import Panda +from selfdrive.car.toyota.values import EPS_SCALE from selfdrive.manager.process import ensure_running from selfdrive.manager.process_config import managed_processes from selfdrive.test.process_replay.process_replay import FAKEDATA, setup_env, check_enabled @@ -30,8 +32,8 @@ def replay_panda_states(s, msgs): # TODO: new safety params from flags, remove after getting new routes for Toyota safety_param_migration = { - "TOYOTA PRIUS 2017": 578, - "TOYOTA RAV4 2017": 329 + "TOYOTA PRIUS 2017": EPS_SCALE["TOYOTA PRIUS 2017"] | Panda.FLAG_TOYOTA_STOCK_LONGITUDINAL, + "TOYOTA RAV4 2017": EPS_SCALE["TOYOTA RAV4 2017"] | Panda.FLAG_TOYOTA_ALT_BRAKE, } # Migrate safety param base on carState From 972e24ee263d7c86b865632f4bb110bbde1b16ac Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Wed, 6 Jul 2022 00:06:25 -0500 Subject: [PATCH 241/436] Add new LEXUS_RX_TSS2 engine f/w (#25041) `@ibby1137#8978` 2022 Lexus RX350L AWD DongleID|route abc09032f402f271|2022-07-05--17-34-41 --- selfdrive/car/toyota/values.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 43d923338e0f3e..283c137c918791 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1761,13 +1761,14 @@ class ToyotaCarInfo(CarInfo): b'\x01896630EB0000\x00\x00\x00\x00', b'\x01896630EC9000\x00\x00\x00\x00', b'\x01896630ED0000\x00\x00\x00\x00', + b'\x01896630ED0100\x00\x00\x00\x00', b'\x01896630ED6000\x00\x00\x00\x00', b'\x018966348W5100\x00\x00\x00\x00', b'\x018966348W9000\x00\x00\x00\x00', b'\x01896634D12000\x00\x00\x00\x00', b'\x01896634D12100\x00\x00\x00\x00', b'\x01896634D43000\x00\x00\x00\x00', - b'\x01896630ED0100\x00\x00\x00\x00', + b'\x01896634D44000\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ b'\x01F15260E031\x00\x00\x00\x00\x00\x00', From 73a6348be7f20fd2c4e303ca7ff7490b71bff59f Mon Sep 17 00:00:00 2001 From: haram-KONA <88036668+haram-KONA@users.noreply.github.com> Date: Wed, 6 Jul 2022 14:08:36 +0900 Subject: [PATCH 242/436] car docs: add video for Hyundai Kona Hybrid 2020 (#25029) * Update values.py Added the following video link "https://www.youtube.com/watch?v=0dwpAHiZgFo" * Update values.py * Update selfdrive/car/hyundai/values.py * Update selfdrive/car/hyundai/values.py Co-authored-by: Shane Smiskol --- selfdrive/car/hyundai/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 0717ed1fbe4843..f6efedb2488f8d 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -105,7 +105,7 @@ class HyundaiCarInfo(CarInfo): CAR.IONIQ_PHEV: HyundaiCarInfo("Hyundai Ioniq Plug-in Hybrid 2020-21", harness=Harness.hyundai_h), CAR.KONA: HyundaiCarInfo("Hyundai Kona 2020", harness=Harness.hyundai_b), CAR.KONA_EV: HyundaiCarInfo("Hyundai Kona Electric 2018-21", harness=Harness.hyundai_g), - CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", harness=Harness.hyundai_i), + CAR.KONA_HEV: HyundaiCarInfo("Hyundai Kona Hybrid 2020", video_link="https://youtu.be/0dwpAHiZgFo", harness=Harness.hyundai_i), CAR.SANTA_FE: HyundaiCarInfo("Hyundai Santa Fe 2019-20", "All", harness=Harness.hyundai_d), CAR.SANTA_FE_2022: HyundaiCarInfo("Hyundai Santa Fe 2021-22", "All", harness=Harness.hyundai_l), CAR.SANTA_FE_HEV_2022: HyundaiCarInfo("Hyundai Santa Fe Hybrid 2022", "All", harness=Harness.hyundai_l), From 38427e6fbbe1eb09c8a335f8c9aa90460d6bbfde Mon Sep 17 00:00:00 2001 From: BirdZhang <0312birdzhang@gmail.com> Date: Wed, 6 Jul 2022 13:11:21 +0800 Subject: [PATCH 243/436] Toyota: add missing esp FW version for 2021 Toyota Corolla (#25026) 2021 Toyota Levin hybrid (aka Corolla) --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 283c137c918791..86283bc48f990e 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -816,6 +816,7 @@ class ToyotaCarInfo(CarInfo): b'F152676303\x00\x00\x00\x00\x00\x00', b'F152676304\x00\x00\x00\x00\x00\x00', b'F152612D00\x00\x00\x00\x00\x00\x00', + b'F152612842\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F3301100\x00\x00\x00\x00', From f797567ef8c574027eec8da72a2c76cfb2fdafe1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 5 Jul 2022 22:19:20 -0700 Subject: [PATCH 244/436] long planner: run when using stock longitudinal (#25017) * Squashed commit of the following: commit e27a5b4e2bfeab4e6a47440b1d4eb180ee4acf49 Author: Shane Smiskol Date: Fri Jul 1 14:10:06 2022 -0700 remove this test remove this test commit c3c10af82222ea4641d94c53a3a07b486cca8452 Author: Shane Smiskol Date: Fri Jul 1 14:08:15 2022 -0700 only planner changes commit 50e0f1d8704c15acfce8987faf3515c99e8af4f4 Merge: e088fde67 fcc5b3d70 Author: Shane Smiskol Date: Fri Jul 1 14:05:36 2022 -0700 Merge remote-tracking branch 'upstream/master' into enable-planner2 commit e088fde67edcc32ccfeea23b4ae9e44845240429 Author: Shane Smiskol Date: Thu Jun 30 13:58:38 2022 -0700 no walrus commit b7b425e530e949b9cc427833562473cc241d1367 Merge: f8634266b c49f997be Author: Shane Smiskol Date: Thu Jun 30 13:54:30 2022 -0700 Merge remote-tracking branch 'upstream/master' into enable-planner commit f8634266b49c3f692b255e6cfac66cccc438ae20 Author: Shane Smiskol Date: Wed Jun 29 16:07:35 2022 -0700 stricter test, speeds[-1] is 0.14 when starting here commit c9e446ad2183feba9d03ee39f9801091ab791c08 Merge: e6c4106ea 879a7c320 Author: Shane Smiskol Date: Wed Jun 29 16:01:32 2022 -0700 Merge remote-tracking branch 'upstream/master' into enable-planner commit e6c4106ea185c68a6c7b3d59d5bde664df8bdc9c Author: Shane Smiskol Date: Sat Jun 25 03:28:41 2022 -0700 fix test commit 0520c7f21613b57b804e08a8e8d10950ac059074 Author: Shane Smiskol Date: Sat Jun 25 03:26:16 2022 -0700 add test for resuming commit 04db5f80bff4a002f5241765a625d7cf57b74364 Merge: e23b37d3f d8bfe2f00 Author: Shane Smiskol Date: Wed Jun 22 20:15:50 2022 -0700 Merge remote-tracking branch 'upstream/master' into enable-planner commit e23b37d3fe8dd3dd07b46a32a4f0564fabade1aa Author: Shane Smiskol Date: Tue Jun 21 12:46:04 2022 -0700 0.1 should be pretty safe commit e7dc3960da3d713753f28732f50dbd25811fad28 Author: Shane Smiskol Date: Tue Jun 21 12:39:30 2022 -0700 try 0.2 commit ff0597ec92a0d2c52915316961ec123b0183c5cf Author: Shane Smiskol Date: Tue Jun 21 11:34:00 2022 -0700 Always run planner if not opLong commit 13997c55271f79fd3ca62d6db45ec3790b09aa60 Merge: d2f51ee55 95d8517a8 Author: Shane Smiskol Date: Tue Jun 21 11:29:22 2022 -0700 Merge remote-tracking branch 'upstream/master' into enable-planner commit d2f51ee55fd3bde38275371e76714d7741bc6f6b Author: Shane Smiskol Date: Tue Jun 21 11:27:45 2022 -0700 same for non-HDA2 commit 6a63bd60f09a0abd9185049cd173100d3ef6fefa Author: Shane Smiskol Date: Mon Jun 20 23:37:07 2022 -0700 mazda: ensure no resume if cancelling commit 5771cdecab7999765d9f5203c75a67f1555cf975 Author: Shane Smiskol Date: Mon Jun 20 23:27:58 2022 -0700 maintain original button msg rate commit 6c1fe0606fd0a0819ffeaac92526e43b3110f2f4 Author: Shane Smiskol Date: Wed Jun 15 23:45:26 2022 -0700 rename to resume commit 00b1df652f1679137c769f9db61eed7dd14e1542 Author: Shane Smiskol Date: Wed Jun 15 21:57:54 2022 -0700 remove comments commit 325ea9bbd5e0dd946961ede0cdcc446ad5e5bbdb Author: Shane Smiskol Date: Wed Jun 15 21:56:20 2022 -0700 vw commit 2c9061042b36fe1d6b029a4216655be69a980849 Author: Shane Smiskol Date: Wed Jun 15 21:54:37 2022 -0700 do rest but vw commit 3dc51f663dfdd4ea1fd72d239bcd5db8c7da4b47 Author: Shane Smiskol Date: Wed Jun 15 16:34:48 2022 -0700 only spam resume when future is > vEgoStarting commit 5f32cd1fcb402bee425d866a9dc76b6feea3d241 Author: Shane Smiskol Date: Wed Jun 15 16:09:43 2022 -0700 always log leads, we hide them in ui * reset when not CS.enabled remove comment * update refs --- selfdrive/controls/lib/longitudinal_planner.py | 4 ++-- selfdrive/controls/radard.py | 16 ++++++---------- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index d4a6aaef8f7a68..cf511367702902 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -66,11 +66,11 @@ def update(self, sm): v_cruise_kph = min(v_cruise_kph, V_CRUISE_MAX) v_cruise = v_cruise_kph * CV.KPH_TO_MS - long_control_state = sm['controlsState'].longControlState + long_control_off = sm['controlsState'].longControlState == LongCtrlState.off force_slow_decel = sm['controlsState'].forceDecel # Reset current state when not engaged, or user is controlling the speed - reset_state = long_control_state == LongCtrlState.off + reset_state = long_control_off if self.CP.openpilotLongitudinalControl else not sm['controlsState'].enabled # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) diff --git a/selfdrive/controls/radard.py b/selfdrive/controls/radard.py index b2c99144573b55..3d958139d6413c 100755 --- a/selfdrive/controls/radard.py +++ b/selfdrive/controls/radard.py @@ -102,7 +102,7 @@ def __init__(self, radar_ts, delay=0): self.ready = False - def update(self, sm, rr, enable_lead): + def update(self, sm, rr): self.current_time = 1e-9*max(sm.logMonoTime.values()) if sm.updated['carState']: @@ -169,11 +169,10 @@ def update(self, sm, rr, enable_lead): radarState.radarErrors = list(rr.errors) radarState.carStateMonoTime = sm.logMonoTime['carState'] - if enable_lead: - leads_v3 = sm['modelV2'].leadsV3 - if len(leads_v3) > 1: - radarState.leadOne = get_lead(self.v_ego, self.ready, clusters, leads_v3[0], low_speed_override=True) - radarState.leadTwo = get_lead(self.v_ego, self.ready, clusters, leads_v3[1], low_speed_override=False) + leads_v3 = sm['modelV2'].leadsV3 + if len(leads_v3) > 1: + radarState.leadOne = get_lead(self.v_ego, self.ready, clusters, leads_v3[0], low_speed_override=True) + radarState.leadTwo = get_lead(self.v_ego, self.ready, clusters, leads_v3[1], low_speed_override=False) return dat @@ -203,9 +202,6 @@ def radard_thread(sm=None, pm=None, can_sock=None): rk = Ratekeeper(1.0 / CP.radarTimeStep, print_delay_threshold=None) RD = RadarD(CP.radarTimeStep, RI.delay) - # TODO: always log leads once we can hide them conditionally - enable_lead = CP.openpilotLongitudinalControl or not CP.radarOffCan - while 1: can_strings = messaging.drain_sock_raw(can_sock, wait_for_one=True) rr = RI.update(can_strings) @@ -215,7 +211,7 @@ def radard_thread(sm=None, pm=None, can_sock=None): sm.update(0) - dat = RD.update(sm, rr, enable_lead) + dat = RD.update(sm, rr) dat.radarState.cumLagMs = -rk.remaining*1000. pm.send('radarState', dat) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 90444b1fa7950b..999081b4dfcf86 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -806984d4206056fb132625c5dad6c0ca1835a2d6 \ No newline at end of file +a57bbbffbee434e59e08b98b667dc13b6b505f08 \ No newline at end of file From 59c28611a4c385c487d5d0c8a219fe20eaeadeaf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 5 Jul 2022 23:01:57 -0700 Subject: [PATCH 245/436] bump opendbc --- opendbc | 2 +- selfdrive/car/chrysler/carstate.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/opendbc b/opendbc index b2895650c744e2..1e9693ce0916b8 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit b2895650c744e24d48cee2f13563dcd5b030a271 +Subproject commit 1e9693ce0916b896568dcd5558a670e67843c299 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index aa46ea0d9434f5..61fe1f7ec6dcee 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -51,9 +51,9 @@ def update(self, cp, cp_cam): ret.cruiseState.available = cp.vl["DAS_3"]["ACC_AVAILABLE"] == 1 # ACC is white ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_ACTIVE"] == 1 # ACC is green - ret.cruiseState.speed = cp.vl["DAS_4"]["ACC_SPEED_CONFIG_KPH"] * CV.KPH_TO_MS + ret.cruiseState.speed = cp.vl["DAS_4"]["ACC_SET_SPEED_KPH"] * CV.KPH_TO_MS # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too - ret.cruiseState.nonAdaptive = cp.vl["DAS_4"]["CRUISE_STATE"] in (1, 2) + ret.cruiseState.nonAdaptive = cp.vl["DAS_4"]["ACC_STATE"] in (1, 2) ret.accFaulted = cp.vl["DAS_3"]["ACC_FAULTED"] != 0 ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] @@ -101,8 +101,8 @@ def get_can_parser(CP): ("ACC_ACTIVE", "DAS_3"), ("ACC_FAULTED", "DAS_3"), ("HIGH_BEAM_PRESSED", "STEERING_LEVERS"), - ("ACC_SPEED_CONFIG_KPH", "DAS_4"), - ("CRUISE_STATE", "DAS_4"), + ("ACC_SET_SPEED_KPH", "DAS_4"), + ("ACC_STATE", "DAS_4"), ("COLUMN_TORQUE", "EPS_2"), ("EPS_TORQUE_MOTOR", "EPS_2"), ("LKAS_STATE", "EPS_2"), From 50434d612ee7becc09aef762a6c8fb8d8111af6d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 6 Jul 2022 12:08:51 +0200 Subject: [PATCH 246/436] casync: reuse requests session in RemoteChunkReader (#25045) --- system/hardware/tici/casync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py index b857c047952fde..d0d0da3c6a17a3 100755 --- a/system/hardware/tici/casync.py +++ b/system/hardware/tici/casync.py @@ -55,6 +55,7 @@ class RemoteChunkReader(ChunkReader): def __init__(self, url: str) -> None: super().__init__() self.url = url + self.session = requests.Session() def read(self, chunk: Chunk) -> bytes: sha_hex = chunk.sha.hex() @@ -62,7 +63,7 @@ def read(self, chunk: Chunk) -> bytes: for i in range(CHUNK_DOWNLOAD_RETRIES): try: - resp = requests.get(url, timeout=CHUNK_DOWNLOAD_TIMEOUT) + resp = self.session.get(url, timeout=CHUNK_DOWNLOAD_TIMEOUT) break except Exception: if i == CHUNK_DOWNLOAD_RETRIES - 1: From 4080f729bea6092813331fb6d92c80e2f4c4f928 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 6 Jul 2022 13:04:25 +0200 Subject: [PATCH 247/436] casync: compute seed caibx url on the fly (#25046) * compute seed hash on the fly * more logging * partition name in url * fix comment --- system/hardware/tici/agnos.py | 41 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index 527422aca75769..bd8ce2bf025c45 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -13,6 +13,7 @@ import system.hardware.tici.casync as casync SPARSE_CHUNK_FMT = struct.Struct('H2xI4x') +CAIBX_URL = "https://commadist.azureedge.net/agnosupdate/" class StreamingDecompressor: @@ -103,28 +104,37 @@ def get_partition_path(target_slot_number: int, partition: dict) -> str: return path +def get_raw_hash(path: str, partition_size: int) -> str: + raw_hash = hashlib.sha256() + pos, chunk_size = 0, 1024 * 1024 + + with open(path, 'rb+') as out: + while pos < partition_size: + n = min(chunk_size, partition_size - pos) + raw_hash.update(out.read(n)) + pos += n + + return raw_hash.hexdigest().lower() + + def verify_partition(target_slot_number: int, partition: Dict[str, Union[str, int]], force_full_check: bool = False) -> bool: full_check = partition['full_check'] or force_full_check path = get_partition_path(target_slot_number, partition) + if not isinstance(partition['size'], int): return False + partition_size: int = partition['size'] if not isinstance(partition['hash_raw'], str): return False - partition_hash: str = partition['hash_raw'] - with open(path, 'rb+') as out: - if full_check: - raw_hash = hashlib.sha256() - pos, chunk_size = 0, 1024 * 1024 - while pos < partition_size: - n = min(chunk_size, partition_size - pos) - raw_hash.update(out.read(n)) - pos += n + partition_hash: str = partition['hash_raw'] - return raw_hash.hexdigest().lower() == partition_hash.lower() - else: + if full_check: + return get_raw_hash(path, partition_size) == partition_hash.lower() + else: + with open(path, 'rb+') as out: out.seek(partition_size) return out.read(64) == partition_hash.lower().encode() @@ -177,8 +187,13 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] # First source is the current partition. Index file for current version is provided in the manifest - if 'casync_seed_caibx' in partition: - sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(partition['casync_seed_caibx'])))] + raw_hash = get_raw_hash(seed_path, partition['size']) + caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" + try: + cloudlog.info(f"casync fetching {caibx_url}") + sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))] + except requests.RequestException: + cloudlog.error(f"casync failed to load {caibx_url}") # Second source is the target partition, this allows for resuming sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] From 6065871ad504f1b590d8de053b25ce1cf01f29ba Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 6 Jul 2022 14:03:31 +0200 Subject: [PATCH 248/436] add casync to release files --- release/files_common | 1 + 1 file changed, 1 insertion(+) diff --git a/release/files_common b/release/files_common index e32277dfd8250e..b46266fa918d0a 100644 --- a/release/files_common +++ b/release/files_common @@ -196,6 +196,7 @@ system/hardware/tici/hardware.h system/hardware/tici/hardware.py system/hardware/tici/pins.py system/hardware/tici/agnos.py +system/hardware/tici/casync.py system/hardware/tici/agnos.json system/hardware/tici/amplifier.py system/hardware/tici/updater From e336f254b1744f87119059189711b326ce8b7884 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 6 Jul 2022 15:50:28 +0200 Subject: [PATCH 249/436] bump laika --- laika_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laika_repo b/laika_repo index 6e87f536dbe8cf..828612e1b8848c 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 6e87f536dbe8cf80040f724c89798e66ca17cf9d +Subproject commit 828612e1b8848ccf70072d5513c0b7977f1707da From b88d7c89fae448068aeaca65706d99aa145c8a74 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Wed, 6 Jul 2022 19:01:19 +0200 Subject: [PATCH 250/436] laikad: Filter unwanted pseudoranges (#25051) Filter unwanted pseudoranges --- selfdrive/locationd/laikad.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 0df48dd8935830..40519da7bc7019 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -103,6 +103,9 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) new_meas = read_raw_ublox(report) + # Filter measurements with unexpected pseudoranges for GPS and GLONASS satellites + new_meas = [m for m in new_meas if 1e7 < m.observables['C1C'] < 3e7] + processed_measurements = process_measurements(new_meas, self.astro_dog) est_pos = self.get_est_pos(t, processed_measurements) From 3e5e27f043bca856ff3f4aaa83355d964ca42fa5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 6 Jul 2022 18:51:51 -0700 Subject: [PATCH 251/436] Add missing 2019 RAV4 Hybrid engine FW version (#25057) add missing engine fw --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 86283bc48f990e..9324e6baf5066b 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1317,6 +1317,7 @@ class ToyotaCarInfo(CarInfo): b'\x018966342X6000\x00\x00\x00\x00', b'\x01896634A25000\x00\x00\x00\x00', b'\x018966342W5000\x00\x00\x00\x00', + b'\x018966342W7000\x00\x00\x00\x00', b'\x028966342W4001\x00\x00\x00\x00897CF1203001\x00\x00\x00\x00', b'\x02896634A13000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02896634A13001\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', From ea241bf3dc1d6dec2610bec9fceb1b8659014436 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 6 Jul 2022 19:42:58 -0700 Subject: [PATCH 252/436] FW fingerprinting: log all FW versions (#25042) * get_fw_versions returns all fw versions with request's brand * keep track of everything received * debug * need to regen or write a hack in build_fw_dict * to be safe, still replace old responses within same brands (hyundai responds to two queries, can fix later) to be safe, still replace old responses within same brands (hyundai responds to two queries, can fix later) * update test_fw_query_on_routes * clean up * better name * slightly cleaner * fix test_startup unit test del * fix imports * fix test_fw_fingerprint fix test_fw_fingerprint fix * fingerprint on all FW_VERSIONS, not just brands with requests * support old routes in test_fw_query_on_routes * regen and update refs * similar function style to before * better comment * space switch name * try to exact match first * useless else * fix debug script * simpler dictionary * bump cereal to master --- cereal | 2 +- selfdrive/car/fw_versions.py | 45 ++++++++++++------- selfdrive/car/tests/test_fw_fingerprint.py | 10 ++--- selfdrive/controls/tests/test_startup.py | 23 +++++----- selfdrive/debug/test_fw_query_on_routes.py | 43 +++++++++++------- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/process_replay/regen.py | 16 ++++++- .../test/process_replay/test_processes.py | 26 +++++------ 8 files changed, 101 insertions(+), 66 deletions(-) diff --git a/cereal b/cereal index df08568318da97..cda60ec9652c05 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit df08568318da97ed6f87747caee0a5b2c30086c4 +Subproject commit cda60ec9652c05de4ccfcad1fae7936e708434a3 diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 758485c393b384..b79f61d94daa1d 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -194,12 +194,13 @@ def chunks(l, n=128): yield l[i:i + n] -def build_fw_dict(fw_versions): +def build_fw_dict(fw_versions, filter_brand=None): fw_versions_dict = {} for fw in fw_versions: - addr = fw.address - sub_addr = fw.subAddress if fw.subAddress != 0 else None - fw_versions_dict[(addr, sub_addr)] = fw.fwVersion + if filter_brand is None or fw.brand == filter_brand: + addr = fw.address + sub_addr = fw.subAddress if fw.subAddress != 0 else None + fw_versions_dict[(addr, sub_addr)] = fw.fwVersion return fw_versions_dict @@ -284,18 +285,27 @@ def match_fw_to_car_exact(fw_versions_dict): def match_fw_to_car(fw_versions, allow_fuzzy=True): - fw_versions_dict = build_fw_dict(fw_versions) - matches = match_fw_to_car_exact(fw_versions_dict) + versions = get_interface_attr('FW_VERSIONS', ignore_none=True) + + # Try exact matching first + exact_matches = [True] + if allow_fuzzy: + exact_matches.append(False) + + for exact_match in exact_matches: + # For each brand, attempt to fingerprint using FW returned from its queries + for brand in versions.keys(): + fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand) - exact_match = True - if allow_fuzzy and len(matches) == 0: - matches = match_fw_to_car_fuzzy(fw_versions_dict) + if exact_match: + matches = match_fw_to_car_exact(fw_versions_dict) + else: + matches = match_fw_to_car_fuzzy(fw_versions_dict) - # Fuzzy match found - if len(matches) == 1: - exact_match = False + if len(matches) == 1: + return exact_match, matches - return exact_match, matches + return True, [] def get_present_ecus(logcan, sendcan): @@ -372,20 +382,21 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr if addrs: query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) t = 2 * timeout if i == 0 else timeout - fw_versions.update({addr: (version, r.request, r.rx_offset) for addr, version in query.get_data(t).items()}) + fw_versions.update({(r.brand, addr): (version, r) for addr, version in query.get_data(t).items()}) except Exception: cloudlog.warning(f"FW query exception: {traceback.format_exc()}") # Build capnp list to put into CarParams car_fw = [] - for addr, (version, request, rx_offset) in fw_versions.items(): + for (brand, addr), (version, request) in fw_versions.items(): f = car.CarParams.CarFw.new_message() f.ecu = ecu_types[addr] f.fwVersion = version f.address = addr[0] - f.responseAddress = uds.get_rx_addr_for_tx_addr(addr[0], rx_offset) - f.request = request + f.responseAddress = uds.get_rx_addr_for_tx_addr(addr[0], request.rx_offset) + f.request = request.request + f.brand = brand if addr[1] is not None: f.subAddress = addr[1] diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index ed7e420e1a38d2..49fa66d36d476e 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -12,6 +12,7 @@ Ecu = car.CarParams.Ecu ECU_NAME = {v: k for k, v in Ecu.schema.enumerants.items()} +VERSIONS = get_interface_attr("FW_VERSIONS", ignore_none=True) class TestFwFingerprint(unittest.TestCase): @@ -20,14 +21,14 @@ def assertFingerprints(self, candidates, expected): self.assertEqual(len(candidates), 1, f"got more than one candidate: {candidates}") self.assertEqual(candidates[0], expected) - @parameterized.expand([(k, v) for k, v in FW_VERSIONS.items()]) - def test_fw_fingerprint(self, car_model, ecus): + @parameterized.expand([(b, c, e[c]) for b, e in VERSIONS.items() for c in e]) + def test_fw_fingerprint(self, brand, car_model, ecus): CP = car.CarParams.new_message() for _ in range(200): fw = [] for ecu, fw_versions in ecus.items(): ecu_name, addr, sub_addr = ecu - fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), + fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) CP.carFw = fw _, matches = match_fw_to_car(CP.carFw) @@ -60,10 +61,9 @@ def test_blacklisted_ecus(self): def test_fw_request_ecu_whitelist(self): passed = True brands = set(r.brand for r in REQUESTS) - versions = get_interface_attr('FW_VERSIONS') for brand in brands: whitelisted_ecus = [ecu for r in REQUESTS for ecu in r.whitelist_ecus if r.brand == brand] - brand_ecus = set([fw[0] for car_fw in versions[brand].values() for fw in car_fw]) + brand_ecus = set([fw[0] for car_fw in VERSIONS[brand].values() for fw in car_fw]) # each ecu in brand's fw versions needs to be whitelisted at least once ecus_not_whitelisted = set(brand_ecus) - set(whitelisted_ecus) diff --git a/selfdrive/controls/tests/test_startup.py b/selfdrive/controls/tests/test_startup.py index 9d1345304503b2..a94311c8c75acc 100755 --- a/selfdrive/controls/tests/test_startup.py +++ b/selfdrive/controls/tests/test_startup.py @@ -42,27 +42,27 @@ class TestStartup(unittest.TestCase): # TODO: test EventName.startup for release branches # officially supported car - (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS), - (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS), + (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS, "toyota"), + (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS, "toyota"), # dashcamOnly car - (EventName.startupNoControl, MAZDA.CX5, CX5_FW_VERSIONS), - (EventName.startupNoControl, MAZDA.CX5, CX5_FW_VERSIONS), + (EventName.startupNoControl, MAZDA.CX5, CX5_FW_VERSIONS, "mazda"), + (EventName.startupNoControl, MAZDA.CX5, CX5_FW_VERSIONS, "mazda"), # unrecognized car with no fw - (EventName.startupNoFw, None, None), - (EventName.startupNoFw, None, None), + (EventName.startupNoFw, None, None, ""), + (EventName.startupNoFw, None, None, ""), # unrecognized car - (EventName.startupNoCar, None, COROLLA_FW_VERSIONS[:1]), - (EventName.startupNoCar, None, COROLLA_FW_VERSIONS[:1]), + (EventName.startupNoCar, None, COROLLA_FW_VERSIONS[:1], "toyota"), + (EventName.startupNoCar, None, COROLLA_FW_VERSIONS[:1], "toyota"), # fuzzy match - (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS_FUZZY), - (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS_FUZZY), + (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS_FUZZY, "toyota"), + (EventName.startupMaster, TOYOTA.COROLLA, COROLLA_FW_VERSIONS_FUZZY, "toyota"), ]) @with_processes(['controlsd']) - def test_startup_alert(self, expected_event, car_model, fw_versions): + def test_startup_alert(self, expected_event, car_model, fw_versions, brand): # TODO: this should be done without any real sockets controls_sock = messaging.sub_sock("controlsState") @@ -82,6 +82,7 @@ def test_startup_alert(self, expected_event, car_model, fw_versions): f.ecu = ecu f.address = addr f.fwVersion = version + f.brand = brand if subaddress is not None: f.subAddress = subaddress diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 011dd6c9a3bdd5..789baeca4b0983 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -8,24 +8,15 @@ from tqdm import tqdm from tools.lib.logreader import LogReader from tools.lib.route import Route +from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.car_helpers import interface_names from selfdrive.car.fw_versions import match_fw_to_car_exact, match_fw_to_car_fuzzy, build_fw_dict -from selfdrive.car.toyota.values import FW_VERSIONS as TOYOTA_FW_VERSIONS -from selfdrive.car.honda.values import FW_VERSIONS as HONDA_FW_VERSIONS -from selfdrive.car.hyundai.values import FW_VERSIONS as HYUNDAI_FW_VERSIONS -from selfdrive.car.volkswagen.values import FW_VERSIONS as VW_FW_VERSIONS -from selfdrive.car.mazda.values import FW_VERSIONS as MAZDA_FW_VERSIONS -from selfdrive.car.subaru.values import FW_VERSIONS as SUBARU_FW_VERSIONS NO_API = "NO_API" in os.environ -SUPPORTED_CARS = set(interface_names['toyota']) -SUPPORTED_CARS |= set(interface_names['honda']) -SUPPORTED_CARS |= set(interface_names['hyundai']) -SUPPORTED_CARS |= set(interface_names['volkswagen']) -SUPPORTED_CARS |= set(interface_names['mazda']) -SUPPORTED_CARS |= set(interface_names['subaru']) -SUPPORTED_CARS |= set(interface_names['nissan']) +VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) +SUPPORTED_BRANDS = VERSIONS.keys() +SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]] try: from xx.pipeline.c.CarState import migration @@ -97,9 +88,24 @@ print("not in supported cars") break - fw_versions_dict = build_fw_dict(car_fw) - exact_matches = match_fw_to_car_exact(fw_versions_dict) - fuzzy_matches = match_fw_to_car_fuzzy(fw_versions_dict) + # Older routes only have carFw from their brand + old_route = not any([len(fw.brand) for fw in car_fw]) + brands = SUPPORTED_BRANDS if not old_route else [None] + + # Exact match + exact_matches, fuzzy_matches = [], [] + for brand in brands: + fw_versions_dict = build_fw_dict(car_fw, filter_brand=brand) + exact_matches = match_fw_to_car_exact(fw_versions_dict) + if len(exact_matches) == 1: + break + + # Fuzzy match + for brand in brands: + fw_versions_dict = build_fw_dict(car_fw, filter_brand=brand) + fuzzy_matches = match_fw_to_car_fuzzy(fw_versions_dict) + if len(fuzzy_matches) == 1: + break if (len(exact_matches) == 1) and (list(exact_matches)[0] == live_fingerprint): good_exact += 1 @@ -126,12 +132,15 @@ print("Mismatches") found = False - for car_fws in [TOYOTA_FW_VERSIONS, HONDA_FW_VERSIONS, HYUNDAI_FW_VERSIONS, VW_FW_VERSIONS, MAZDA_FW_VERSIONS, SUBARU_FW_VERSIONS]: + for brand in SUPPORTED_BRANDS: + car_fws = VERSIONS[brand] if live_fingerprint in car_fws: found = True expected = car_fws[live_fingerprint] for (_, expected_addr, expected_sub_addr), v in expected.items(): for version in car_fw: + if version.brand != brand and len(version.brand): + continue sub_addr = None if version.subAddress == 0 else version.subAddress addr = version.address diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 999081b4dfcf86..0eeae1e3e3e671 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -a57bbbffbee434e59e08b98b667dc13b6b505f08 \ No newline at end of file +b904e52e9de4ff7b2bd7f6af8b19abaf4957e6cc \ No newline at end of file diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index 793e548705d8ee..1a2d436f1abce7 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -179,8 +179,22 @@ def replay_camera(s, stream, dt, vipc_server, frames, size, use_extra_client): return vs, p +def migrate_carparams(lr): + all_msgs = [] + for msg in lr: + if msg.which() == 'carParams': + CP = messaging.new_message('carParams') + CP.carParams = msg.carParams.as_builder() + for car_fw in CP.carParams.carFw: + car_fw.brand = CP.carParams.carName + msg = CP.as_reader() + all_msgs.append(msg) + + return all_msgs + + def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): - lr = list(lr) + lr = migrate_carparams(list(lr)) if frs is None: frs = dict() diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 4e7ba4a6ddcd19..9cbf4439acca88 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -35,19 +35,19 @@ ] segments = [ - ("BODY", "bd6a637565e91581|2022-04-04--22-05-08--0"), - ("HYUNDAI", "fakedata|2022-01-20--17-49-04--0"), - ("TOYOTA", "fakedata|2022-04-29--15-57-12--0"), - ("TOYOTA2", "fakedata|2022-04-29--16-08-01--0"), - ("TOYOTA3", "fakedata|2022-04-29--16-17-39--0"), - ("HONDA", "fakedata|2022-01-20--17-56-40--0"), - ("HONDA2", "fakedata|2022-04-29--16-31-55--0"), - ("CHRYSLER", "fakedata|2022-01-20--18-00-11--0"), - ("SUBARU", "fakedata|2022-01-20--18-01-57--0"), - ("GM", "fakedata|2022-01-20--18-03-41--0"), - ("NISSAN", "fakedata|2022-01-20--18-05-29--0"), - ("VOLKSWAGEN", "fakedata|2022-01-20--18-07-15--0"), - ("MAZDA", "fakedata|2022-01-20--18-09-32--0"), + ("BODY", "regen660D86654BA|2022-07-06--14-27-15--0"), + ("HYUNDAI", "regen657E25856BB|2022-07-06--14-26-51--0"), + ("TOYOTA", "regenBA97410FBEC|2022-07-06--14-26-49--0"), + ("TOYOTA2", "regenDEDB1D9C991|2022-07-06--14-54-08--0"), + ("TOYOTA3", "regenDDC1FE60734|2022-07-06--14-32-06--0"), + ("HONDA", "regen17B09D158B8|2022-07-06--14-31-46--0"), + ("HONDA2", "regen041739C3E9A|2022-07-06--15-08-02--0"), + ("CHRYSLER", "regenBB2F9C1425C|2022-07-06--14-31-41--0"), + ("SUBARU", "regen732B69F33B1|2022-07-06--14-36-18--0"), + ("GM", "regen01D09D915B5|2022-07-06--14-36-20--0"), + ("NISSAN", "regenEA6FB2773F5|2022-07-06--14-58-23--0"), + ("VOLKSWAGEN", "regen007098CA0EF|2022-07-06--15-01-26--0"), + ("MAZDA", "regen61BA413D53B|2022-07-06--14-39-42--0"), ] # dashcamOnly makes don't need to be tested until a full port is done From 479b66c992fb2898418d866a1c61d993a9217d3d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 6 Jul 2022 19:57:44 -0700 Subject: [PATCH 253/436] VW FPv2: reduce number of ECU queries (#24939) * only send valid/needed queries * just do volkswagen * clean up * add parameter name clean up * add test for whitelist * rename * Update selfdrive/car/fw_versions.py Co-authored-by: Jason Young <46612682+jyoung8607@users.noreply.github.com> * fix test * log response addresses * bump cereal * handle response pending with IsoTpParallelQuery * remove response pending stuff * temporarily disregard cache for easier testing * revert this Co-authored-by: Jason Young <46612682+jyoung8607@users.noreply.github.com> --- selfdrive/car/fw_versions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index b79f61d94daa1d..03dcece10c85c9 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -148,12 +148,14 @@ class Request: "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], + whitelist_ecus=[Ecu.srs, Ecu.eps, Ecu.fwdRadar], rx_offset=VOLKSWAGEN_RX_OFFSET, ), Request( "volkswagen", [VOLKSWAGEN_VERSION_REQUEST_MULTI], [VOLKSWAGEN_VERSION_RESPONSE], + whitelist_ecus=[Ecu.engine, Ecu.transmission], ), # Mazda Request( From 9b0acacf5e387593ce94dbad88000b5473511e22 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 6 Jul 2022 23:42:07 -0700 Subject: [PATCH 254/436] Ram 1500 (#24878) * RamInit * bump submodules * lil cleanup * clean up carstate formatting and platform grouping make tuple * give it a gold torque star (looks around 2.4 from rough data) * Dasm Fault * bump panda * more cleanup * cleanup car state * more cleanup * some fixes * remove more stuff * fix angle signal scaling and fix lkas control bit * bump panda * update those * same limits as pacifica * cleanup hud alert building * better fault logic * fix rate * set ahb * bring that back * update refs Co-authored-by: Jonathan Co-authored-by: Shane Smiskol Co-authored-by: Comma Device --- RELEASES.md | 1 + docs/CARS.md | 3 +- panda | 2 +- release/files_common | 1 + selfdrive/car/chrysler/carcontroller.py | 73 +++++------- selfdrive/car/chrysler/carstate.py | 135 +++++++++++++++------- selfdrive/car/chrysler/chryslercan.py | 79 +++++++------ selfdrive/car/chrysler/interface.py | 22 +++- selfdrive/car/chrysler/radar_interface.py | 9 +- selfdrive/car/chrysler/values.py | 36 +++--- selfdrive/car/tests/routes.py | 1 + selfdrive/car/torque_data/override.yaml | 3 +- selfdrive/test/process_replay/ref_commit | 2 +- 13 files changed, 226 insertions(+), 141 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5b71fc33752a17..fedff5dbff7b99 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -19,6 +19,7 @@ Version 0.8.15 (2022-07-XX) * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! +* Ram 1500 2019-21 support thanks to realfast! Version 0.8.14 (2022-06-01) ======================== diff --git a/docs/CARS.md b/docs/CARS.md index 0cf384df183922..a1e89efe121b95 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -144,7 +144,7 @@ How We Rate The Cars |Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| |Volkswagen|Polo 2020|Driver Assistance|||||| -# Bronze - 79 cars +# Bronze - 80 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -198,6 +198,7 @@ How We Rate The Cars |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| |Mazda|CX-5 2022|All|||||| |Mazda|CX-9 2021|All|||||| +|Ram|1500 2019-21|Adaptive Cruise|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| |Subaru|XV 2018-19|EyeSight|||||| diff --git a/panda b/panda index 6c0d0b43c239b8..fae3ee2e8161d3 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 6c0d0b43c239b89baa83b4a1885d0ce21ab2335e +Subproject commit fae3ee2e8161d34a7c1939503e583db9b85e5402 diff --git a/release/files_common b/release/files_common index b46266fa918d0a..260e37e29ac019 100644 --- a/release/files_common +++ b/release/files_common @@ -474,6 +474,7 @@ opendbc/can/parser_pyx.pyx opendbc/comma_body.dbc +opendbc/chrysler_ram_dt_generated.dbc opendbc/chrysler_pacifica_2017_hybrid_generated.dbc opendbc/chrysler_pacifica_2017_hybrid_private_fusion.dbc diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 49525646ca6297..606cb51176cbde 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -1,8 +1,7 @@ -from cereal import car from opendbc.can.packer import CANPacker from selfdrive.car import apply_toyota_steer_torque_limits from selfdrive.car.chrysler.chryslercan import create_lkas_hud, create_lkas_command, create_cruise_buttons -from selfdrive.car.chrysler.values import CAR, CarControllerParams +from selfdrive.car.chrysler.values import RAM_CARS, CarControllerParams class CarController: @@ -10,61 +9,51 @@ def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 self.frame = 0 - self.prev_lkas_frame = -1 - self.hud_count = 0 - self.car_fingerprint = CP.carFingerprint - self.gone_fast_yet = False self.steer_rate_limited = False - self.packer = CANPacker(dbc_name) - - def update(self, CC, CS): - # this seems needed to avoid steering faults and to force the sync with the EPS counter - if self.prev_lkas_frame == CS.lkas_counter: - return car.CarControl.Actuators.new_message(), [] - - actuators = CC.actuators - - # steer torque - new_steer = int(round(actuators.steer * CarControllerParams.STEER_MAX)) - apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, - CS.out.steeringTorqueEps, CarControllerParams) - self.steer_rate_limited = new_steer != apply_steer - - moving_fast = CS.out.vEgo > self.CP.minSteerSpeed # for status message - if CS.out.vEgo > (self.CP.minSteerSpeed - 0.5): # for command high bit - self.gone_fast_yet = True - elif self.car_fingerprint in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_CHEROKEE_2019): - if CS.out.vEgo < (self.CP.minSteerSpeed - 3.0): - self.gone_fast_yet = False # < 14.5m/s stock turns off this bit, but fine down to 13.5 - lkas_active = moving_fast and CC.enabled - - if not lkas_active: - apply_steer = 0 + self.hud_count = 0 + self.last_lkas_falling_edge = 0 + self.lkas_active_prev = False - self.apply_steer_last = apply_steer + self.packer = CANPacker(dbc_name) + def update(self, CC, CS, low_speed_alert): can_sends = [] + # EPS faults if LKAS re-enables too quickly + lkas_active = CC.latActive and not low_speed_alert and (self.frame - self.last_lkas_falling_edge > 200) + # *** control msgs *** if CC.cruiseControl.cancel: - can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, cancel=True)) + bus = 2 if self.CP.carFingerprint in RAM_CARS else 0 + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, bus, cancel=True)) - # LKAS_HEARTBIT is forwarded by Panda so no need to send it here. - # frame is 100Hz (0.01s period) - if self.frame % 25 == 0: # 0.25s period + # HUD alerts + if self.frame % 25 == 0: if CS.lkas_car_model != -1: - can_sends.append(create_lkas_hud(self.packer, CS.out.gearShifter, lkas_active, - CC.hudControl.visualAlert, self.hud_count, CS.lkas_car_model)) + can_sends.append(create_lkas_hud(self.packer, self.CP, lkas_active, CC.hudControl.visualAlert, self.hud_count, CS.lkas_car_model, CS.auto_high_beam)) self.hud_count += 1 - can_sends.append(create_lkas_command(self.packer, int(apply_steer), self.gone_fast_yet, CS.lkas_counter)) + # steering + if self.frame % 2 == 0: + # steer torque + new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX)) + apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorqueEps, CarControllerParams) + if not lkas_active: + apply_steer = 0 + self.steer_rate_limited = new_steer != apply_steer + self.apply_steer_last = apply_steer + + idx = self.frame // 2 + can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_active, idx)) self.frame += 1 - self.prev_lkas_frame = CS.lkas_counter + if not lkas_active and self.lkas_active_prev: + self.last_lkas_falling_edge = self.frame + self.lkas_active_prev = lkas_active - new_actuators = actuators.copy() - new_actuators.steer = apply_steer / CarControllerParams.STEER_MAX + new_actuators = CC.actuators.copy() + new_actuators.steer = self.apply_steer_last / CarControllerParams.STEER_MAX return new_actuators, can_sends diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 61fe1f7ec6dcee..71b7e34623312a 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -3,21 +3,29 @@ from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine from selfdrive.car.interfaces import CarStateBase -from selfdrive.car.chrysler.values import DBC, STEER_THRESHOLD +from selfdrive.car.chrysler.values import DBC, STEER_THRESHOLD, RAM_CARS class CarState(CarStateBase): def __init__(self, CP): super().__init__(CP) + self.CP = CP can_define = CANDefine(DBC[CP.carFingerprint]["pt"]) - self.shifter_values = can_define.dv["GEAR"]["PRNDL"] + + self.auto_high_beam = 0 + self.button_counter = 0 + self.lkas_car_model = -1 + + if CP.carFingerprint in RAM_CARS: + self.shifter_values = can_define.dv["Transmission_Status"]["Gear_State"] + else: + self.shifter_values = can_define.dv["GEAR"]["PRNDL"] def update(self, cp, cp_cam): ret = car.CarState.new_message() - self.frame = int(cp.vl["EPS_2"]["COUNTER"]) - + # lock info ret.doorOpen = any([cp.vl["BCM_1"]["DOOR_OPEN_FL"], cp.vl["BCM_1"]["DOOR_OPEN_FR"], cp.vl["BCM_1"]["DOOR_OPEN_RL"], @@ -32,8 +40,15 @@ def update(self, cp, cp_cam): ret.gas = cp.vl["ECM_5"]["Accelerator_Position"] ret.gasPressed = ret.gas > 1e-5 - ret.espDisabled = (cp.vl["TRACTION_BUTTON"]["TRACTION_OFF"] == 1) - + # car speed + if self.CP.carFingerprint in RAM_CARS: + ret.vEgoRaw = cp.vl["ESP_8"]["Vehicle_Speed"] * CV.KPH_TO_MS + ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["Transmission_Status"]["Gear_State"], None)) + else: + ret.vEgoRaw = (cp.vl["SPEED_1"]["SPEED_LEFT"] + cp.vl["SPEED_1"]["SPEED_RIGHT"]) / 2. + ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.standstill = not ret.vEgoRaw > 0.001 ret.wheelSpeeds = self.get_wheel_speeds( cp.vl["ESP_6"]["WHEEL_SPEED_FL"], cp.vl["ESP_6"]["WHEEL_SPEED_FR"], @@ -41,55 +56,73 @@ def update(self, cp, cp_cam): cp.vl["ESP_6"]["WHEEL_SPEED_RR"], unit=1, ) - ret.vEgoRaw = (cp.vl["SPEED_1"]["SPEED_LEFT"] + cp.vl["SPEED_1"]["SPEED_RIGHT"]) / 2. - ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) - ret.standstill = not ret.vEgoRaw > 0.001 + # button presses ret.leftBlinker = cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 1 ret.rightBlinker = cp.vl["STEERING_LEVERS"]["TURN_SIGNALS"] == 2 - ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl["GEAR"]["PRNDL"], None)) - - ret.cruiseState.available = cp.vl["DAS_3"]["ACC_AVAILABLE"] == 1 # ACC is white - ret.cruiseState.enabled = cp.vl["DAS_3"]["ACC_ACTIVE"] == 1 # ACC is green - ret.cruiseState.speed = cp.vl["DAS_4"]["ACC_SET_SPEED_KPH"] * CV.KPH_TO_MS - # CRUISE_STATE is a three bit msg, 0 is off, 1 and 2 are Non-ACC mode, 3 and 4 are ACC mode, find if there are other states too - ret.cruiseState.nonAdaptive = cp.vl["DAS_4"]["ACC_STATE"] in (1, 2) - ret.accFaulted = cp.vl["DAS_3"]["ACC_FAULTED"] != 0 + ret.genericToggle = cp.vl["STEERING_LEVERS"]["HIGH_BEAM_PRESSED"] == 1 + # steering wheel ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.steeringTorque = cp.vl["EPS_2"]["COLUMN_TORQUE"] ret.steeringTorqueEps = cp.vl["EPS_2"]["EPS_TORQUE_MOTOR"] ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD - steer_state = cp.vl["EPS_2"]["LKAS_STATE"] - ret.steerFaultPermanent = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) - ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]["HIGH_BEAM_PRESSED"]) + # cruise state + cp_cruise = cp_cam if self.CP.carFingerprint in RAM_CARS else cp + + ret.cruiseState.available = cp_cruise.vl["DAS_3"]["ACC_AVAILABLE"] == 1 + ret.cruiseState.enabled = cp_cruise.vl["DAS_3"]["ACC_ACTIVE"] == 1 + ret.cruiseState.speed = cp_cruise.vl["DAS_4"]["ACC_SET_SPEED_KPH"] * CV.KPH_TO_MS + ret.cruiseState.nonAdaptive = cp_cruise.vl["DAS_4"]["ACC_STATE"] in (1, 2) # 1 NormalCCOn and 2 NormalCCSet + ret.cruiseState.standstill = cp_cruise.vl["DAS_3"]["ACC_STANDSTILL"] == 1 + ret.accFaulted = cp_cruise.vl["DAS_3"]["ACC_FAULTED"] != 0 + + if self.CP.carFingerprint in RAM_CARS: + self.auto_high_beam = cp_cam.vl["DAS_6"]['AUTO_HIGH_BEAM_ON'] # Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message + ret.steerFaultTemporary = cp.vl["EPS_3"]["DASM_FAULT"] == 1 + else: + steer_state = cp.vl["EPS_2"]["LKAS_STATE"] + ret.steerFaultPermanent = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) + # blindspot sensors if self.CP.enableBsm: ret.leftBlindspot = cp.vl["BSM_1"]["LEFT_STATUS"] == 1 ret.rightBlindspot = cp.vl["BSM_1"]["RIGHT_STATUS"] == 1 - self.lkas_counter = cp_cam.vl["LKAS_COMMAND"]["COUNTER"] self.lkas_car_model = cp_cam.vl["DAS_6"]["CAR_MODEL"] - self.lkas_status_ok = cp_cam.vl["LKAS_HEARTBIT"]["LKAS_STATUS_OK"] self.button_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"] return ret + @staticmethod + def get_cruise_signals(): + signals = [ + ("ACC_AVAILABLE", "DAS_3"), + ("ACC_ACTIVE", "DAS_3"), + ("ACC_FAULTED", "DAS_3"), + ("ACC_STANDSTILL", "DAS_3"), + ("COUNTER", "DAS_3"), + ("ACC_SET_SPEED_KPH", "DAS_4"), + ("ACC_STATE", "DAS_4"), + ] + checks = [ + ("DAS_3", 50), + ("DAS_4", 50), + ] + return signals, checks + @staticmethod def get_can_parser(CP): signals = [ # sig_name, sig_address - ("PRNDL", "GEAR"), ("DOOR_OPEN_FL", "BCM_1"), ("DOOR_OPEN_FR", "BCM_1"), ("DOOR_OPEN_RL", "BCM_1"), ("DOOR_OPEN_RR", "BCM_1"), ("Brake_Pedal_State", "ESP_1"), ("Accelerator_Position", "ECM_5"), - ("SPEED_LEFT", "SPEED_1"), - ("SPEED_RIGHT", "SPEED_1"), ("WHEEL_SPEED_FL", "ESP_6"), ("WHEEL_SPEED_RR", "ESP_6"), ("WHEEL_SPEED_RL", "ESP_6"), @@ -97,18 +130,12 @@ def get_can_parser(CP): ("STEER_ANGLE", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), - ("ACC_AVAILABLE", "DAS_3"), - ("ACC_ACTIVE", "DAS_3"), - ("ACC_FAULTED", "DAS_3"), ("HIGH_BEAM_PRESSED", "STEERING_LEVERS"), - ("ACC_SET_SPEED_KPH", "DAS_4"), - ("ACC_STATE", "DAS_4"), + ("SEATBELT_DRIVER_UNLATCHED", "ORC_1"), + ("COUNTER", "EPS_2",), ("COLUMN_TORQUE", "EPS_2"), ("EPS_TORQUE_MOTOR", "EPS_2"), ("LKAS_STATE", "EPS_2"), - ("COUNTER", "EPS_2",), - ("TRACTION_OFF", "TRACTION_BUTTON"), - ("SEATBELT_DRIVER_UNLATCHED", "ORC_1"), ("COUNTER", "CRUISE_BUTTONS"), ] @@ -116,18 +143,13 @@ def get_can_parser(CP): # sig_address, frequency ("ESP_1", 50), ("EPS_2", 100), - ("SPEED_1", 100), ("ESP_6", 50), ("STEERING", 100), - ("DAS_3", 50), - ("GEAR", 50), ("ECM_5", 50), ("CRUISE_BUTTONS", 50), - ("DAS_4", 15), ("STEERING_LEVERS", 10), ("ORC_1", 2), ("BCM_1", 1), - ("TRACTION_BUTTON", 1), ] if CP.enableBsm: @@ -137,20 +159,47 @@ def get_can_parser(CP): ] checks.append(("BSM_1", 2)) + if CP.carFingerprint in RAM_CARS: + signals += [ + ("DASM_FAULT", "EPS_3"), + ("Vehicle_Speed", "ESP_8"), + ("Gear_State", "Transmission_Status"), + ] + checks += [ + ("ESP_8", 50), + ("EPS_3", 50), + ("Transmission_Status", 50), + ] + else: + signals += [ + ("PRNDL", "GEAR"), + ("SPEED_LEFT", "SPEED_1"), + ("SPEED_RIGHT", "SPEED_1"), + ] + checks += [ + ("GEAR", 50), + ("SPEED_1", 100), + ] + signals += CarState.get_cruise_signals()[0] + checks += CarState.get_cruise_signals()[1] + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 0) @staticmethod def get_cam_can_parser(CP): signals = [ - # sig_name, sig_address - ("COUNTER", "LKAS_COMMAND"), + # sig_name, sig_address, default ("CAR_MODEL", "DAS_6"), - ("LKAS_STATUS_OK", "LKAS_HEARTBIT") ] checks = [ - ("LKAS_COMMAND", 100), - ("LKAS_HEARTBIT", 10), ("DAS_6", 4), ] + if CP.carFingerprint in RAM_CARS: + signals += [ + ("AUTO_HIGH_BEAM_ON", "DAS_6"), + ] + signals += CarState.get_cruise_signals()[0] + checks += CarState.get_cruise_signals()[1] + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 2) diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index adcd411d317d07..e17e5d5b2a445f 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -1,56 +1,69 @@ from cereal import car -from selfdrive.car import make_can_msg - +from selfdrive.car.chrysler.values import RAM_CARS GearShifter = car.CarState.GearShifter VisualAlert = car.CarControl.HUDControl.VisualAlert -def create_lkas_hud(packer, gear, lkas_active, hud_alert, hud_count, lkas_car_model): - # LKAS_HUD 0x2a6 (678) Controls what lane-keeping icon is displayed. +def create_lkas_hud(packer, CP, lkas_active, hud_alert, hud_count, car_model, auto_high_beam): + # LKAS_HUD - Controls what lane-keeping icon is displayed + + # == Color == + # 0 hidden? + # 1 white + # 2 green + # 3 ldw + + # == Lines == + # 03 white Lines + # 04 grey lines + # 09 left lane close + # 0A right lane close + # 0B left Lane very close + # 0C right Lane very close + # 0D left cross cross + # 0E right lane cross - if hud_alert in (VisualAlert.steerRequired, VisualAlert.ldw): - msg = b'\x00\x00\x00\x03\x00\x00\x00\x00' - return make_can_msg(0x2a6, msg, 0) + # == Alerts == + # 7 Normal + # 6 lane departure place hands on wheel - color = 1 # default values are for park or neutral in 2017 are 0 0, but trying 1 1 for 2019 - lines = 1 - alerts = 0 + color = 2 if lkas_active else 1 + lines = 3 if lkas_active else 0 + alerts = 7 if lkas_active else 0 if hud_count < (1 * 4): # first 3 seconds, 4Hz alerts = 1 - # CAR.PACIFICA_2018_HYBRID and CAR.PACIFICA_2019_HYBRID - # had color = 1 and lines = 1 but trying 2017 hybrid style for now. - if gear in (GearShifter.drive, GearShifter.reverse, GearShifter.low): - if lkas_active: - color = 2 # control active, display green. - lines = 6 - else: - color = 1 # control off, display white. - lines = 1 + + if hud_alert in (VisualAlert.ldw, VisualAlert.steerRequired): + color = 4 + lines = 0 + alerts = 6 values = { - "LKAS_ICON_COLOR": color, # byte 0, last 2 bits - "CAR_MODEL": lkas_car_model, # byte 1 - "LKAS_LANE_LINES": lines, # byte 2, last 4 bits - "LKAS_ALERTS": alerts, # byte 3, last 4 bits - } + "LKAS_ICON_COLOR": color, + "CAR_MODEL": car_model, + "LKAS_LANE_LINES": lines, + "LKAS_ALERTS": alerts, + } + + if CP.carFingerprint in RAM_CARS: + values['AUTO_HIGH_BEAM_ON'] = auto_high_beam - return packer.make_can_msg("DAS_6", 0, values) # 0x2a6 + return packer.make_can_msg("DAS_6", 0, values) -def create_lkas_command(packer, apply_steer, moving_fast, frame): - # LKAS_COMMAND 0x292 (658) Lane-keeping signal to turn the wheel. +def create_lkas_command(packer, CP, apply_steer, lat_active, frame): + # LKAS_COMMAND Lane-keeping signal to turn the wheel + enabled_val = 2 if CP.carFingerprint in RAM_CARS else 1 values = { "STEERING_TORQUE": apply_steer, - "LKAS_CONTROL_BIT": int(moving_fast), - "COUNTER": frame % 0x10, + "LKAS_CONTROL_BIT": enabled_val if lat_active else 0, } - return packer.make_can_msg("LKAS_COMMAND", 0, values) + return packer.make_can_msg("LKAS_COMMAND", 0, values, frame % 0x10) -def create_cruise_buttons(packer, frame, cancel=False): +def create_cruise_buttons(packer, frame, bus, cancel=False): values = { "ACC_Cancel": cancel, - "COUNTER": frame % 0x10, } - return packer.make_can_msg("CRUISE_BUTTONS", 0, values) + return packer.make_can_msg("CRUISE_BUTTONS", bus, values, frame % 0x10) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 8ebcb6b1266005..af202cdc463962 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from cereal import car -from selfdrive.car.chrysler.values import CAR +from panda import Panda from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config +from selfdrive.car.chrysler.values import CAR, RAM_CARS from selfdrive.car.interfaces import CarInterfaceBase @@ -10,7 +11,9 @@ class CarInterface(CarInterfaceBase): def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disable_radar=False): ret = CarInterfaceBase.get_std_params(candidate, fingerprint) ret.carName = "chrysler" - ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.chrysler)] + + param = Panda.FLAG_CHRYSLER_RAM_DT if candidate in RAM_CARS else None + ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.chrysler, param)] ret.steerActuatorDelay = 0.1 ret.steerLimitTimer = 0.4 @@ -39,6 +42,15 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.15, 0.30], [0.03, 0.05]] ret.lateralTuning.pid.kf = 0.00006 + # Ram + elif candidate == CAR.RAM_1500: + ret.wheelbase = 3.88 + ret.steerRatio = 16.3 + ret.mass = 2493. + STD_CARGO_KG + ret.maxLateralAccel = 2.4 + ret.minSteerSpeed = 14.5 + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + else: raise ValueError(f"Unsupported car: {candidate}") @@ -64,9 +76,9 @@ def _update(self, c): events = self.create_common_events(ret, extra_gears=[car.CarState.GearShifter.low]) # Low speed steer alert hysteresis logic - if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 1.): + if self.CP.minSteerSpeed > 0. and ret.vEgo < (self.CP.minSteerSpeed + 0.5): self.low_speed_alert = True - elif ret.vEgo > (self.CP.minSteerSpeed + 2.): + elif ret.vEgo > (self.CP.minSteerSpeed + 1.): self.low_speed_alert = False if self.low_speed_alert: events.add(car.CarEvent.EventName.belowSteerSpeed) @@ -76,4 +88,4 @@ def _update(self, c): return ret def apply(self, c): - return self.CC.update(c, self.CS) + return self.CC.update(c, self.CS, self.low_speed_alert) diff --git a/selfdrive/car/chrysler/radar_interface.py b/selfdrive/car/chrysler/radar_interface.py index 8882dc2d91baf0..348e3c3632dfb4 100755 --- a/selfdrive/car/chrysler/radar_interface.py +++ b/selfdrive/car/chrysler/radar_interface.py @@ -10,6 +10,10 @@ NUMBER_MSGS = len(RADAR_MSGS_C) + len(RADAR_MSGS_D) def _create_radar_can_parser(car_fingerprint): + dbc = DBC[car_fingerprint]['radar'] + if dbc is None: + return None + msg_n = len(RADAR_MSGS_C) # list of [(signal name, message name or number), (...)] # [('RADAR_STATE', 1024), @@ -46,6 +50,9 @@ def __init__(self, CP): self.trigger_msg = LAST_MSG def update(self, can_strings): + if self.rcp is None: + return super().update(None) + vls = self.rcp.update_strings(can_strings) self.updated_messages.update(vls) @@ -81,4 +88,4 @@ def update(self, can_strings): ret.points = [x for x in self.pts.values() if x.dRel != 0] self.updated_messages.clear() - return ret + return ret \ No newline at end of file diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 5537b383d30f89..40210e68e66985 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -2,19 +2,12 @@ from enum import Enum from typing import Dict, List, Optional, Union +from cereal import car from selfdrive.car import dbc_dict from selfdrive.car.docs_definitions import CarInfo, Harness -from cereal import car Ecu = car.CarParams.Ecu -class CarControllerParams: - STEER_MAX = 261 # 262 faults - STEER_DELTA_UP = 3 # 3 is stock. 100 is fine. 200 is too much it seems - STEER_DELTA_DOWN = 3 # no faults on the way down it seems - STEER_ERROR_MAX = 80 - - class CAR: # Chrysler PACIFICA_2017_HYBRID = "CHRYSLER PACIFICA HYBRID 2017" @@ -24,16 +17,28 @@ class CAR: PACIFICA_2020 = "CHRYSLER PACIFICA 2020" # Jeep - JEEP_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk - JEEP_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk + JEEP_CHEROKEE = "JEEP GRAND CHEROKEE V6 2018" # includes 2017 Trailhawk + JEEP_CHEROKEE_2019 = "JEEP GRAND CHEROKEE 2019" # includes 2020 Trailhawk + + # Ram + RAM_1500 = "RAM 1500 5TH GEN" + +class CarControllerParams: + STEER_MAX = 261 # higher than this faults the EPS on Chrysler/Jeep. Ram DT allows more + STEER_DELTA_UP = 3 + STEER_DELTA_DOWN = 3 + STEER_ERROR_MAX = 80 + +STEER_THRESHOLD = 120 + +RAM_CARS = {CAR.RAM_1500, } @dataclass class ChryslerCarInfo(CarInfo): package: str = "Adaptive Cruise" harness: Enum = Harness.fca - CAR_INFO: Dict[str, Optional[Union[ChryslerCarInfo, List[ChryslerCarInfo]]]] = { CAR.PACIFICA_2017_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2017-18"), CAR.PACIFICA_2018_HYBRID: None, # same platforms @@ -42,6 +47,7 @@ class ChryslerCarInfo(CarInfo): CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2019-20"), CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-20", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), + CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-21"), } # Unique CAN messages: @@ -97,6 +103,11 @@ class ChryslerCarInfo(CarInfo): # Jeep Grand Cherokee 2019, including most 2020 models 55: 8, 168: 8, 179: 8, 181: 8, 256: 4, 257: 5, 258: 8, 264: 8, 268: 8, 272: 6, 273: 6, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 292: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 341: 8, 344: 8, 352: 8, 362: 8, 368: 8, 376: 3, 384: 8, 388: 4, 416: 7, 448: 6, 456: 4, 464: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 530: 8, 532: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 618: 8, 624: 8, 625: 8, 632: 8, 639: 8, 640: 1, 656: 4, 658: 6, 660: 8, 671: 8, 672: 8, 676: 8, 678: 8, 680: 8, 683: 8, 684: 8, 703: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 761: 8, 764: 8, 766: 8, 773: 8, 776: 8, 779: 8, 782: 8, 783: 8, 784: 8, 785: 8, 792: 8, 799: 8, 800: 8, 804: 8, 806: 2, 808: 8, 810: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 831: 6, 832: 8, 838: 2, 840: 8, 844: 5, 847: 1, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 882: 8, 897: 8, 906: 8, 924: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 960: 4, 968: 8, 969: 4, 970: 8, 973: 8, 974: 5, 976: 8, 977: 4, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8, 1216: 8, 1218: 8, 1220: 8, 1223: 8, 1225: 8, 1227: 8, 1235: 8, 1242: 8, 1250: 8, 1251: 8, 1252: 8, 1254: 8, 1264: 8, 1284: 8, 1536: 8, 1537: 8, 1543: 8, 1545: 8, 1562: 8, 1568: 8, 1570: 8, 1572: 8, 1593: 8, 1856: 8, 1858: 8, 1860: 8, 1863: 8, 1865: 8, 1867: 8, 1875: 8, 1882: 8, 1890: 8, 1891: 8, 1892: 8, 1894: 8, 1896: 8, 1904: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8 }], + CAR.RAM_1500: [ + {35: 8, 37: 8, 39: 8, 41: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 278: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 298: 8, 299: 8, 305: 8, 307: 8, 311: 8, 315: 8, 317: 8, 319: 8, 323: 8, 333: 8, 334: 8, 341: 8, 343: 8, 345: 8, 347: 8, 409: 6, 421: 8, 448: 6, 456: 4, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 505: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 611: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 659: 5, 663: 8, 664: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 752: 2, 754: 8, 773: 8, 788: 3, 792: 8, 808: 8, 818: 8, 819: 8, 822: 8, 823: 8, 825: 2, 838: 2, 840: 8, 848: 8, 856: 4, 860: 6, 862: 8, 875: 2, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 956: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8}, + {35: 8, 37: 8, 39: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 276: 8, 277: 8, 278: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 299: 8, 301: 8, 302: 8, 305: 8, 307: 8, 311: 8, 317: 8, 319: 8, 323: 8, 327: 8, 333: 8, 334: 8, 341: 8, 343: 8, 345: 8, 347: 8, 421: 8, 448: 6, 456: 4, 457: 8, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 663: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 738: 8, 752: 2, 754: 8, 773: 8, 792: 8, 808: 8, 812: 8, 813: 8, 814: 8, 818: 8, 819: 8, 821: 8, 822: 8, 823: 8, 825: 2, 838: 2, 840: 8, 847: 1, 848: 8, 856: 4, 860: 6, 862: 8, 874: 2, 876: 8, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 975: 8, 976: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1030: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1098: 8, 1100: 8}, + {35: 8, 37: 8, 39: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 299: 8, 301: 8, 302: 8, 305: 8, 307: 8, 311: 8, 317: 8, 319: 8, 323: 8, 334: 8, 337: 8, 343: 8, 347: 8, 409: 6, 421: 8, 448: 6, 456: 4, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 659: 5, 663: 8, 664: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 752: 2, 754: 8, 773: 8, 788: 3, 792: 8, 808: 8, 812: 8, 813: 8, 814: 8, 818: 8, 819: 8, 821: 8, 822: 8, 825: 2, 838: 2, 840: 8, 847: 1, 848: 8, 856: 4, 860: 6, 862: 8, 876: 8, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 956: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1030: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8}, + ], } @@ -108,6 +119,5 @@ class ChryslerCarInfo(CarInfo): CAR.PACIFICA_2019_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_CHEROKEE: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), CAR.JEEP_CHEROKEE_2019: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), + CAR.RAM_1500: dbc_dict('chrysler_ram_dt_generated', None), } - -STEER_THRESHOLD = 120 diff --git a/selfdrive/car/tests/routes.py b/selfdrive/car/tests/routes.py index 19b1a05c239723..96eb5f67b9ec8b 100644 --- a/selfdrive/car/tests/routes.py +++ b/selfdrive/car/tests/routes.py @@ -38,6 +38,7 @@ TestRoute("378472f830ee7395|2021-05-28--07-38-43", CHRYSLER.PACIFICA_2018_HYBRID), TestRoute("8190c7275a24557b|2020-01-29--08-33-58", CHRYSLER.PACIFICA_2019_HYBRID), TestRoute("3d84727705fecd04|2021-05-25--08-38-56", CHRYSLER.PACIFICA_2020), + TestRoute("221c253375af4ee9|2022-06-15--18-38-24", CHRYSLER.RAM_1500), #TestRoute("f1b4c567731f4a1b|2018-04-30--10-15-35", FORD.FUSION), diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index a2200926c031aa..be81af260641bd 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -19,8 +19,9 @@ FORD FOCUS 4TH GEN: [.nan, 1.5, .nan] # No steering wheel COMMA BODY: [.nan, 1000, .nan] -# Totally new car +# Totally new cars KIA EV6 2022: [3.0, 2.5, 0.0] +RAM 1500 5TH GEN: [2.0, 2.0, 0.05] # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 0eeae1e3e3e671..b0136da88e5112 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -b904e52e9de4ff7b2bd7f6af8b19abaf4957e6cc \ No newline at end of file +ebe7f1285ec60f522179606d483a198535c0e83a \ No newline at end of file From fd2de54172b4a76f2ab8ac5d8f8eca5c41739351 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 7 Jul 2022 00:24:03 -0700 Subject: [PATCH 255/436] Stock longitudinal: spam resume button when lead starts moving (#24873) * always log leads, we hide them in ui * only spam resume when future is > vEgoStarting * do rest but vw * vw * remove comments * rename to resume * maintain original button msg rate * mazda: ensure no resume if cancelling * same for non-HDA2 * Always run planner if not opLong * try 0.2 * 0.1 should be pretty safe * add test for resuming * fix test * stricter test, speeds[-1] is 0.14 when starting here * no walrus * fixup mazda cc * remove extra import --- selfdrive/car/honda/carcontroller.py | 2 +- selfdrive/car/hyundai/carcontroller.py | 4 ++-- selfdrive/car/mazda/carcontroller.py | 11 ++++------- selfdrive/car/volkswagen/carcontroller.py | 5 ++--- selfdrive/controls/controlsd.py | 4 ++++ selfdrive/test/longitudinal_maneuvers/maneuver.py | 5 +++++ selfdrive/test/longitudinal_maneuvers/plant.py | 3 +++ .../test/longitudinal_maneuvers/test_longitudinal.py | 12 ++++++++++++ 8 files changed, 33 insertions(+), 13 deletions(-) diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index 14049c9997f071..d47caaa9ad1664 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -195,7 +195,7 @@ def update(self, CC, CS): # If using stock ACC, spam cancel command to kill gas when OP disengages. if pcm_cancel_cmd: can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.CANCEL, idx, self.CP.carFingerprint)) - elif CS.out.cruiseState.standstill: + elif CC.cruiseControl.resume: can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.RES_ACCEL, idx, self.CP.carFingerprint)) else: diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 4fbb5ce0e1c08e..f3066bda03aa2f 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -78,7 +78,7 @@ def update(self, CC, CS): self.last_button_frame = self.frame # cruise standstill resume - elif CC.enabled and CS.out.cruiseState.standstill: + elif CC.cruiseControl.resume: can_sends.append(hda2can.create_buttons(self.packer, CS.buttons_counter+1, False, True)) self.last_button_frame = self.frame else: @@ -96,7 +96,7 @@ def update(self, CC, CS): if not self.CP.openpilotLongitudinalControl: if CC.cruiseControl.cancel: can_sends.append(hyundaican.create_clu11(self.packer, self.frame, CS.clu11, Buttons.CANCEL)) - elif CS.out.cruiseState.standstill: + elif CC.cruiseControl.resume: # send resume at a max freq of 10Hz if (self.frame - self.last_button_frame) * DT_CTRL > 0.1: # send 25 messages at a time to increases the likelihood of resume being accepted diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py index 0e43a11ceb5d07..a83cef508a0c9a 100644 --- a/selfdrive/car/mazda/carcontroller.py +++ b/selfdrive/car/mazda/carcontroller.py @@ -29,13 +29,6 @@ def update(self, CC, CS): CS.out.steeringTorque, CarControllerParams) self.steer_rate_limited = new_steer != apply_steer - if CC.enabled: - if CS.out.standstill and self.frame % 5 == 0: - # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds - # Send Resume button at 20hz if we're engaged at standstill to support full stop and go! - # TODO: improve the resume trigger logic by looking at actual radar data - can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.RESUME)) - if CC.cruiseControl.cancel: # If brake is pressed, let us wait >70ms before trying to disable crz to avoid # a race condition with the stock system, where the second cancel from openpilot @@ -48,6 +41,10 @@ def update(self, CC, CS): can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.CANCEL)) else: self.brake_counter = 0 + if CC.cruiseControl.resume and self.frame % 5 == 0: + # Mazda Stop and Go requires a RES button (or gas) press if the car stops more than 3 seconds + # Send Resume button when planner wants car to move + can_sends.append(mazdacan.create_button_cmd(self.packer, self.CP.carFingerprint, CS.crz_btns_counter, Buttons.RESUME)) self.apply_steer_last = apply_steer diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 4614463c6e93f7..1643fbe9b6d1b0 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -96,9 +96,8 @@ def update(self, CC, CS, ext_bus): # Cancel ACC if it's engaged with OP disengaged. self.graButtonStatesToSend = BUTTON_STATES.copy() self.graButtonStatesToSend["cancel"] = True - elif CC.enabled and CS.out.cruiseState.standstill: - # Blip the Resume button if we're engaged at standstill. - # FIXME: This is a naive implementation, improve with visiond or radar input. + elif CC.cruiseControl.resume: + # Send Resume button when planner wants car to move self.graButtonStatesToSend = BUTTON_STATES.copy() self.graButtonStatesToSend["resumeCruise"] = True diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 9e3af9eb636972..6f0c9c2ae6c3a7 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -658,6 +658,10 @@ def publish_logs(self, CS, start_time, CC, lac_log): if self.joystick_mode and self.sm.rcv_frame['testJoystick'] > 0 and self.sm['testJoystick'].buttons[0]: CC.cruiseControl.cancel = True + speeds = self.sm['longitudinalPlan'].speeds + if len(speeds): + CC.cruiseControl.resume = self.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 + hudControl = CC.hudControl hudControl.setSpeed = float(self.v_cruise_kph * CV.KPH_TO_MS) hudControl.speedVisible = self.enabled diff --git a/selfdrive/test/longitudinal_maneuvers/maneuver.py b/selfdrive/test/longitudinal_maneuvers/maneuver.py index 9b4d01643068e1..0d605a5fc7e8cc 100644 --- a/selfdrive/test/longitudinal_maneuvers/maneuver.py +++ b/selfdrive/test/longitudinal_maneuvers/maneuver.py @@ -16,6 +16,7 @@ def __init__(self, title, duration, **kwargs): self.only_lead2 = kwargs.get("only_lead2", False) self.only_radar = kwargs.get("only_radar", False) + self.ensure_start = kwargs.get("ensure_start", False) self.duration = duration self.title = title @@ -52,5 +53,9 @@ def evaluate(self): print("Crashed!!!!") valid = False + if self.ensure_start and log['v_rel'] > 0 and log['speeds'][-1] <= 0.1: + print('Planner not starting!') + valid = False + print("maneuver end", valid) return valid, np.array(logs) diff --git a/selfdrive/test/longitudinal_maneuvers/plant.py b/selfdrive/test/longitudinal_maneuvers/plant.py index 13025a9f03b38d..3bd50ebcfaa68c 100755 --- a/selfdrive/test/longitudinal_maneuvers/plant.py +++ b/selfdrive/test/longitudinal_maneuvers/plant.py @@ -28,6 +28,7 @@ def __init__(self, lead_relevancy=False, speed=0.0, distance_lead=2.0, self.distance = 0. self.speed = speed self.acceleration = 0.0 + self.speeds = [] # lead car self.distance_lead = distance_lead @@ -98,6 +99,7 @@ def step(self, v_lead=0.0, prob=1.0, v_cruise=50.): self.planner.update(sm) self.speed = self.planner.v_desired_filter.x self.acceleration = self.planner.a_desired + self.speeds = self.planner.v_desired_trajectory.tolist() fcw = self.planner.fcw self.distance_lead = self.distance_lead + v_lead * self.ts @@ -129,6 +131,7 @@ def step(self, v_lead=0.0, prob=1.0, v_cruise=50.): "distance": self.distance, "speed": self.speed, "acceleration": self.acceleration, + "speeds": self.speeds, "distance_lead": self.distance_lead, "fcw": fcw, } diff --git a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py index 698877dd3ad29e..ec698d88fa1fcc 100755 --- a/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py +++ b/selfdrive/test/longitudinal_maneuvers/test_longitudinal.py @@ -3,6 +3,7 @@ import unittest from common.params import Params +from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import STOP_DISTANCE from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver @@ -106,6 +107,17 @@ breakpoints=[1., 1.01, 11.], cruise_values=[float("nan"), 15., 15.], ), + # controls relies on planner commanding to move for stock-ACC resume spamming + Maneuver( + "resume from a stop", + duration=20., + initial_speed=0., + lead_relevancy=True, + initial_distance_lead=STOP_DISTANCE, + speed_lead_values=[0., 0., 2.], + breakpoints=[1., 10., 15.], + ensure_start=True, + ), ] From 30cb9ac962395a86464fdfc079b5f0030c7b9b9d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 7 Jul 2022 00:28:21 -0700 Subject: [PATCH 256/436] FW query debug script: print version brand (#25058) * test_fw_query_on_routes: print brand * dynamic paddign --- selfdrive/debug/test_fw_query_on_routes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 789baeca4b0983..8c8c631c38fd3c 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -126,9 +126,10 @@ print("New style (exact):", exact_matches) print("New style (fuzzy):", fuzzy_matches) - for version in car_fw: + padding = max([len(fw.brand) for fw in car_fw]) + for version in sorted(car_fw, key=lambda fw: fw.brand): subaddr = None if version.subAddress == 0 else hex(version.subAddress) - print(f" (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") + print(f" Brand: {version.brand:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") print("Mismatches") found = False From edf170103ed0d244e0483794feee6c3b4023ccc2 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 7 Jul 2022 11:14:31 +0200 Subject: [PATCH 257/436] Process replay: Fix subtest diff (#25054) Fix subtest diff --- selfdrive/test/process_replay/test_processes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 9cbf4439acca88..04bf51e2e9bc0b 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -71,7 +71,7 @@ def run_test_process(data): assert os.path.exists(cur_log_fn), f"Cannot find log to upload: {cur_log_fn}" upload_file(cur_log_fn, os.path.basename(cur_log_fn)) os.remove(cur_log_fn) - return (segment, cfg.proc_name, res) + return (segment, cfg.proc_name, cfg.subtest_name, res) def get_log_data(segment): @@ -212,9 +212,9 @@ def format_diff(results, ref_commit): results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) - for (segment, proc, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): + for (segment, proc, subtest_name, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): if isinstance(result, list): - results[segment][proc] = result + results[segment][proc + subtest_name] = result diff1, diff2, failed = format_diff(results, ref_commit) if not upload: From a3a9a0685c63ea57dc936e496cd5e6be5a71512e Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 7 Jul 2022 12:02:31 +0200 Subject: [PATCH 258/436] onroad.cc: fix mutcd sign width for metric speed limit --- selfdrive/ui/qt/onroad.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 604d3c09a945f7..ca39a89ae45740 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -296,7 +296,7 @@ void NvgWindow::drawHud(QPainter &p) { // US/Canada (MUTCD style) sign if (has_us_speed_limit) { const int border_width = 6; - const int sign_width = (speedLimitStr.size() >= 3) ? 199 : 148; + const int sign_width = rect_width - 24; const int sign_height = 186; // White outer square From eaf7eb42784732136e52cd031035e9c5ab520e6e Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 7 Jul 2022 13:06:51 +0200 Subject: [PATCH 259/436] Laikad: Use clocks for faster fetching orbits (#25060) * Use clocks msg to for first fetch of orbits. Which is sent earlier than ublox msgs * refactor last_fetch_orbits * Add comment. Add test * increase timeout * Add clocks to process replay --- selfdrive/locationd/laikad.py | 39 ++++++++++++------- selfdrive/locationd/test/test_laikad.py | 26 ++++++++++++- .../test/process_replay/process_replay.py | 4 +- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 40519da7bc7019..e262407e024ad8 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -5,6 +5,7 @@ import time from collections import defaultdict from concurrent.futures import Future, ProcessPoolExecutor +from datetime import datetime from enum import IntEnum from typing import List, Optional @@ -13,7 +14,7 @@ from cereal import log, messaging from common.params import Params, put_nonblocking from laika import AstroDog -from laika.constants import SECS_IN_MIN +from laika.constants import SECS_IN_HR, SECS_IN_MIN from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId @@ -30,7 +31,8 @@ class Laikad: - def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_update=False, valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), + def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_update=False, + valid_ephem_types=(EphemerisType.ULTRA_RAPID_ORBIT, EphemerisType.NAV), save_ephemeris=False): """ valid_const: GNSS constellation which can be used @@ -47,6 +49,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_ self.orbit_fetch_future: Optional[Future] = None self.last_fetch_orbits_t = None + self.got_first_ublox_msg = False self.last_cached_t = None self.save_ephemeris = save_ephemeris self.load_cache() @@ -72,8 +75,9 @@ def load_cache(self): except json.decoder.JSONDecodeError: cloudlog.exception("Error parsing cache") timestamp = self.last_fetch_orbits_t.as_datetime() if self.last_fetch_orbits_t is not None else 'Nan' - cloudlog.debug(f"Loaded nav and orbits cache with timestamp: {timestamp}. Unique orbit and nav sats: {list(cache['orbits'].keys())} {list(cache['nav'].keys())} " + - f"Total: {sum([len(v) for v in cache['orbits']])} and {sum([len(v) for v in cache['nav']])}") + cloudlog.debug( + f"Loaded nav and orbits cache with timestamp: {timestamp}. Unique orbit and nav sats: {list(cache['orbits'].keys())} {list(cache['nav'].keys())} " + + f"Total: {sum([len(v) for v in cache['orbits']])} and {sum([len(v) for v in cache['nav']])}") def cache_ephemeris(self, t: GPSTime): if self.save_ephemeris and (self.last_cached_t is None or t - self.last_cached_t > SECS_IN_MIN): @@ -98,9 +102,10 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): t = ublox_mono_time * 1e-9 report = ublox_msg.measurementReport if report.gpsWeek > 0: + self.got_first_ublox_msg = True latest_msg_t = GPSTime(report.gpsWeek, report.rcvTow) if self.auto_fetch_orbits: - self.fetch_orbits(latest_msg_t + SECS_IN_MIN, block) + self.fetch_orbits(latest_msg_t, block) new_meas = read_raw_ublox(report) # Filter measurements with unexpected pseudoranges for GPS and GLONASS satellites @@ -174,24 +179,26 @@ def init_gnss_localizer(self, est_pos): self.gnss_kf.init_state(x_initial, covs_diag=p_initial_diag) def fetch_orbits(self, t: GPSTime, block): - if t not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or t - self.last_fetch_orbits_t > SECS_IN_MIN): + # Download new orbits if 1 hour of orbits data left + if t + SECS_IN_HR not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or abs(t - self.last_fetch_orbits_t) > SECS_IN_MIN): astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types - ret = None - if block: + if block: # Used for testing purposes ret = get_orbit_data(t, *astro_dog_vars) elif self.orbit_fetch_future is None: self.orbit_fetch_executor = ProcessPoolExecutor(max_workers=1) self.orbit_fetch_future = self.orbit_fetch_executor.submit(get_orbit_data, t, *astro_dog_vars) elif self.orbit_fetch_future.done(): - self.last_fetch_orbits_t = t ret = self.orbit_fetch_future.result() self.orbit_fetch_executor = self.orbit_fetch_future = None if ret is not None: - self.astro_dog.orbits, self.astro_dog.orbit_fetched_times = ret - self.cache_ephemeris(t=t) + if ret[0] is None: + self.last_fetch_orbits_t = ret[2] + else: + self.astro_dog.orbits, self.astro_dog.orbit_fetched_times, self.last_fetch_orbits_t = ret + self.cache_ephemeris(t=t) def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): @@ -201,9 +208,10 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): try: astro_dog.get_orbit_data(t, only_predictions=True) cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") - return astro_dog.orbits, astro_dog.orbit_fetched_times + return astro_dog.orbits, astro_dog.orbit_fetched_times, t except (RuntimeError, ValueError, IOError) as e: cloudlog.warning(f"No orbit data found or parsing failure: {e}") + return None, None, t def create_measurement_msg(meas: GNSSMeasurement): @@ -284,7 +292,7 @@ class EphemerisSourceType(IntEnum): def main(sm=None, pm=None): if sm is None: - sm = messaging.SubMaster(['ubloxGnss']) + sm = messaging.SubMaster(['ubloxGnss', 'clocks']) if pm is None: pm = messaging.PubMaster(['gnssMeasurements']) @@ -299,6 +307,11 @@ def main(sm=None, pm=None): msg = laikad.process_ublox_msg(ublox_msg, sm.logMonoTime['ubloxGnss'], block=replay) if msg is not None: pm.send('gnssMeasurements', msg) + if not laikad.got_first_ublox_msg and sm.updated['clocks']: + clocks_msg = sm['clocks'] + t = GPSTime.from_datetime(datetime.utcfromtimestamp(clocks_msg.wallTimeNanos * 1E-9)) + if laikad.auto_fetch_orbits: + laikad.fetch_orbits(t, block=replay) if __name__ == "__main__": diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 26c1d288203b4a..3a7c073b55c493 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -67,7 +67,7 @@ def test_fetch_orbits_non_blocking(self): gpstime = GPSTime.from_datetime(datetime(2021, month=3, day=1)) laikad = Laikad() laikad.fetch_orbits(gpstime, block=False) - laikad.orbit_fetch_future.result(5) + laikad.orbit_fetch_future.result(30) # Get results and save orbits to laikad: laikad.fetch_orbits(gpstime, block=False) @@ -75,7 +75,7 @@ def test_fetch_orbits_non_blocking(self): self.assertIsNotNone(ephem) laikad.fetch_orbits(gpstime+2*SECS_IN_DAY, block=False) - laikad.orbit_fetch_future.result(5) + laikad.orbit_fetch_future.result(30) # Get results and save orbits to laikad: laikad.fetch_orbits(gpstime + 2 * SECS_IN_DAY, block=False) @@ -83,6 +83,28 @@ def test_fetch_orbits_non_blocking(self): self.assertIsNotNone(ephem) self.assertNotEqual(ephem, ephem2) + def test_fetch_orbits_with_wrong_clocks(self): + laikad = Laikad() + + def check_has_orbits(): + self.assertGreater(len(laikad.astro_dog.orbits), 0) + ephem = laikad.astro_dog.orbits['G01'][0] + self.assertIsNotNone(ephem) + real_current_time = GPSTime.from_datetime(datetime(2021, month=3, day=1)) + wrong_future_clock_time = real_current_time + SECS_IN_DAY + + laikad.fetch_orbits(wrong_future_clock_time, block=True) + check_has_orbits() + self.assertEqual(laikad.last_fetch_orbits_t, wrong_future_clock_time) + + # Test fetching orbits with earlier time + assert real_current_time < laikad.last_fetch_orbits_t + + laikad.astro_dog.orbits = {} + laikad.fetch_orbits(real_current_time, block=True) + check_has_orbits() + self.assertEqual(laikad.last_fetch_orbits_t, real_current_time) + def test_ephemeris_source_in_msg(self): data_mock = defaultdict(str) data_mock['sv_id'] = 1 diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 9c45281b0c7c8a..c667aa388713c6 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -239,7 +239,7 @@ def ublox_rcv_callback(msg): def laika_rcv_callback(msg, CP, cfg, fsm): - if msg.ubloxGnss.which() == "measurementReport": + if msg.which() == 'ubloxGnss' and msg.ubloxGnss.which() == "measurementReport": return ["gnssMeasurements"], True else: return [], True @@ -352,6 +352,7 @@ def laika_rcv_callback(msg, CP, cfg, fsm): subtest_name="Offline", pub_sub={ "ubloxGnss": ["gnssMeasurements"], + "clocks": [] }, ignore=["logMonoTime"], init_callback=get_car_params, @@ -364,6 +365,7 @@ def laika_rcv_callback(msg, CP, cfg, fsm): proc_name="laikad", pub_sub={ "ubloxGnss": ["gnssMeasurements"], + "clocks": [] }, ignore=["logMonoTime"], init_callback=get_car_params, From 356190f6712b68218237147deddf90fbe6268b6e Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 7 Jul 2022 16:13:05 +0200 Subject: [PATCH 260/436] fix MacOS buid: replay frameworks (#25061) * replay: fix macos build * here too * keep original frameworks --- tools/replay/SConscript | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 4a85f46d612b03..d3967708fa29be 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -2,8 +2,14 @@ import os Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'transformations') +base_frameworks = qt_env['FRAMEWORKS'] base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', - 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] + 'capnp', 'kj', 'm', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] + +if arch == "Darwin": + base_frameworks.append('OpenCL') +else: + base_libs.append('OpenCL') qt_libs = ['qt_util'] + base_libs if arch in ['x86_64', 'Darwin'] or GetOption('extras'): @@ -11,9 +17,9 @@ if arch in ['x86_64', 'Darwin'] or GetOption('extras'): replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", "route.cc", "util.cc"] - replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs) + replay_lib = qt_env.Library("qt_replay", replay_lib_src, LIBS=qt_libs, FRAMEWORKS=base_frameworks) replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'curl', 'yuv', 'ncurses'] + qt_libs - qt_env.Program("replay", ["main.cc"], LIBS=replay_libs) + qt_env.Program("replay", ["main.cc"], LIBS=replay_libs, FRAMEWORKS=base_frameworks) if GetOption('test'): qt_env.Program('tests/test_replay', ['tests/test_runner.cc', 'tests/test_replay.cc'], LIBS=[replay_libs]) From 7e187426c7f02b8d63bce394f08219307e5900ca Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 7 Jul 2022 18:00:07 +0200 Subject: [PATCH 261/436] athena: skip duplicate upload requests (#25062) * athena: skip duplicate upload requests * cleanup * keep simple * just ignore --- selfdrive/athena/athenad.py | 5 +++++ selfdrive/athena/tests/test_athenad.py | 23 +++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/selfdrive/athena/athenad.py b/selfdrive/athena/athenad.py index b0e138c49527c7..6ccd6c3de135b2 100755 --- a/selfdrive/athena/athenad.py +++ b/selfdrive/athena/athenad.py @@ -364,6 +364,11 @@ def uploadFilesToUrls(files_data): failed.append(fn) continue + # Skip item if already in queue + url = file['url'].split('?')[0] + if any(url == item['url'].split('?')[0] for item in listUploadQueue()): + continue + item = UploadItem( path=path, url=file['url'], diff --git a/selfdrive/athena/tests/test_athenad.py b/selfdrive/athena/tests/test_athenad.py index 382b549c1bbf5a..7f511eecf686fe 100755 --- a/selfdrive/athena/tests/test_athenad.py +++ b/selfdrive/athena/tests/test_athenad.py @@ -124,7 +124,7 @@ def test_strip_bz2_extension(self): fn = os.path.join(athenad.ROOT, 'qlog.bz2') Path(fn).touch() if fn.endswith('.bz2'): - self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4]) + self.assertEqual(athenad.strip_bz2_extension(fn), fn[:-4]) @with_http_server @@ -142,9 +142,6 @@ def test_do_upload(self, host): @with_http_server def test_uploadFileToUrl(self, host): - not_exists_resp = dispatcher["uploadFileToUrl"]("does_not_exist.bz2", "http://localhost:1238", {}) - self.assertEqual(not_exists_resp, {'enqueued': 0, 'items': [], 'failed': ['does_not_exist.bz2']}) - fn = os.path.join(athenad.ROOT, 'qlog.bz2') Path(fn).touch() @@ -155,6 +152,24 @@ def test_uploadFileToUrl(self, host): self.assertIsNotNone(resp['items'][0].get('id')) self.assertEqual(athenad.upload_queue.qsize(), 1) + @with_http_server + def test_uploadFileToUrl_duplicate(self, host): + fn = os.path.join(athenad.ROOT, 'qlog.bz2') + Path(fn).touch() + + url1 = f"{host}/qlog.bz2?sig=sig1" + dispatcher["uploadFileToUrl"]("qlog.bz2", url1, {}) + + # Upload same file again, but with different signature + url2 = f"{host}/qlog.bz2?sig=sig2" + resp = dispatcher["uploadFileToUrl"]("qlog.bz2", url2, {}) + self.assertEqual(resp, {'enqueued': 0, 'items': []}) + + @with_http_server + def test_uploadFileToUrl_does_not_exist(self, host): + not_exists_resp = dispatcher["uploadFileToUrl"]("does_not_exist.bz2", "http://localhost:1238", {}) + self.assertEqual(not_exists_resp, {'enqueued': 0, 'items': [], 'failed': ['does_not_exist.bz2']}) + @with_http_server def test_upload_handler(self, host): fn = os.path.join(athenad.ROOT, 'qlog.bz2') From ea80ee0845b619cc20309228611c01ed4c717fd1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 09:40:04 -0700 Subject: [PATCH 262/436] Chrysler: resume from cruise standstill (#25009) * Chrysler: resume from cruise standstill * bump panda * resume isn't set yet --- panda | 2 +- selfdrive/car/chrysler/carcontroller.py | 6 ++++-- selfdrive/car/chrysler/chryslercan.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/panda b/panda index fae3ee2e8161d3..53466f09344c8f 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit fae3ee2e8161d34a7c1939503e583db9b85e5402 +Subproject commit 53466f09344c8ff6cdce3b19df76b5bca79e1327 diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 606cb51176cbde..e0eb979e6ab61c 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -25,9 +25,11 @@ def update(self, CC, CS, low_speed_alert): # *** control msgs *** + das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0 if CC.cruiseControl.cancel: - bus = 2 if self.CP.carFingerprint in RAM_CARS else 0 - can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, bus, cancel=True)) + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True)) + elif CC.enabled and CS.out.cruiseState.standstill: + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True)) # HUD alerts if self.frame % 25 == 0: diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index e17e5d5b2a445f..632c0d2bcf6c7d 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -62,8 +62,9 @@ def create_lkas_command(packer, CP, apply_steer, lat_active, frame): return packer.make_can_msg("LKAS_COMMAND", 0, values, frame % 0x10) -def create_cruise_buttons(packer, frame, bus, cancel=False): +def create_cruise_buttons(packer, frame, bus, cancel=False, resume=False): values = { "ACC_Cancel": cancel, + "ACC_Resume": resume, } return packer.make_can_msg("CRUISE_BUTTONS", bus, values, frame % 0x10) From 8d98d8c6578830e28ea8f39fce3844a7e493c019 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 10:28:55 -0700 Subject: [PATCH 263/436] process replay: add Ram route (#25063) --- selfdrive/car/chrysler/interface.py | 4 +++- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/process_replay/test_processes.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index af202cdc463962..920000b271850f 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -2,7 +2,7 @@ from cereal import car from panda import Panda from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config -from selfdrive.car.chrysler.values import CAR, RAM_CARS +from selfdrive.car.chrysler.values import CAR, DBC, RAM_CARS from selfdrive.car.interfaces import CarInterfaceBase @@ -12,6 +12,8 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret = CarInterfaceBase.get_std_params(candidate, fingerprint) ret.carName = "chrysler" + ret.radarOffCan = DBC[candidate]['radar'] is None + param = Panda.FLAG_CHRYSLER_RAM_DT if candidate in RAM_CARS else None ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.chrysler, param)] diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b0136da88e5112..0521fd829504d9 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -ebe7f1285ec60f522179606d483a198535c0e83a \ No newline at end of file +cf46781e405a01c96307d30d1266d46e0fa92255 \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 04bf51e2e9bc0b..96d1014004deb6 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -24,6 +24,7 @@ ("HONDA", "eb140f119469d9ab|2021-06-12--10-46-24--27"), # HONDA.CIVIC (NIDEC) ("HONDA2", "7d2244f34d1bbcda|2021-06-25--12-25-37--26"), # HONDA.ACCORD (BOSCH) ("CHRYSLER", "4deb27de11bee626|2021-02-20--11-28-55--8"), # CHRYSLER.PACIFICA + ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--2"), # CHRYSLER.RAM_1500 ("SUBARU", "4d70bc5e608678be|2021-01-15--17-02-04--5"), # SUBARU.IMPREZA ("GM", "0c58b6a25109da2b|2021-02-23--16-35-50--11"), # GM.VOLT ("NISSAN", "35336926920f3571|2021-02-12--18-38-48--46"), # NISSAN.XTRAIL @@ -43,6 +44,7 @@ ("HONDA", "regen17B09D158B8|2022-07-06--14-31-46--0"), ("HONDA2", "regen041739C3E9A|2022-07-06--15-08-02--0"), ("CHRYSLER", "regenBB2F9C1425C|2022-07-06--14-31-41--0"), + ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--2"), ("SUBARU", "regen732B69F33B1|2022-07-06--14-36-18--0"), ("GM", "regen01D09D915B5|2022-07-06--14-36-20--0"), ("NISSAN", "regenEA6FB2773F5|2022-07-06--14-58-23--0"), From 568cc0f892d650bd5906b59e1ab169158a7a6bf7 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 8 Jul 2022 01:54:56 +0800 Subject: [PATCH 264/436] loggerd: remove 'enable' from struct LogCameraInfo (#25052) remove enalbe --- selfdrive/loggerd/encoderd.cc | 6 ++---- selfdrive/loggerd/loggerd.cc | 6 ++---- selfdrive/loggerd/loggerd.h | 5 ----- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/selfdrive/loggerd/encoderd.cc b/selfdrive/loggerd/encoderd.cc index 87cf4a492f464b..9bd8e2f1d4422b 100644 --- a/selfdrive/loggerd/encoderd.cc +++ b/selfdrive/loggerd/encoderd.cc @@ -124,10 +124,8 @@ void encoderd_thread() { std::vector encoder_threads; for (const auto &cam : cameras_logged) { - if (cam.enable) { - encoder_threads.push_back(std::thread(encoder_thread, &s, cam)); - s.max_waiting++; - } + encoder_threads.push_back(std::thread(encoder_thread, &s, cam)); + s.max_waiting++; } for (auto &t : encoder_threads) t.join(); } diff --git a/selfdrive/loggerd/loggerd.cc b/selfdrive/loggerd/loggerd.cc index a75ab2c92b2b5c..e0892e68b42ed4 100644 --- a/selfdrive/loggerd/loggerd.cc +++ b/selfdrive/loggerd/loggerd.cc @@ -204,10 +204,8 @@ void loggerd_thread() { // init encoders s.last_camera_seen_tms = millis_since_boot(); for (const auto &cam : cameras_logged) { - if (cam.enable) { - s.max_waiting++; - if (cam.has_qcamera) { s.max_waiting++; } - } + s.max_waiting++; + if (cam.has_qcamera) { s.max_waiting++; } } uint64_t msg_count = 0, bytes_count = 0; diff --git a/selfdrive/loggerd/loggerd.h b/selfdrive/loggerd/loggerd.h index 2c4990086a2109..6eafbe08d0aa67 100644 --- a/selfdrive/loggerd/loggerd.h +++ b/selfdrive/loggerd/loggerd.h @@ -50,7 +50,6 @@ struct LogCameraInfo { int bitrate; bool is_h265; bool has_qcamera; - bool enable; bool record; }; @@ -63,7 +62,6 @@ const LogCameraInfo cameras_logged[] = { .bitrate = MAIN_BITRATE, .is_h265 = true, .has_qcamera = true, - .enable = true, .record = true, .frame_width = 1928, .frame_height = 1208, @@ -76,7 +74,6 @@ const LogCameraInfo cameras_logged[] = { .bitrate = DCAM_BITRATE, .is_h265 = true, .has_qcamera = false, - .enable = true, .record = Params().getBool("RecordFront"), .frame_width = 1928, .frame_height = 1208, @@ -89,7 +86,6 @@ const LogCameraInfo cameras_logged[] = { .bitrate = MAIN_BITRATE, .is_h265 = true, .has_qcamera = false, - .enable = true, .record = true, .frame_width = 1928, .frame_height = 1208, @@ -100,7 +96,6 @@ const LogCameraInfo qcam_info = { .fps = MAIN_FPS, .bitrate = 256000, .is_h265 = false, - .enable = true, .record = true, .frame_width = 526, .frame_height = 330, From 836e2a4d98b587b7ae083bb710190ed1dfa9dccf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 13:20:42 -0700 Subject: [PATCH 265/436] Chrysler: fix steer fault detection (#25068) --- selfdrive/car/chrysler/carstate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 71b7e34623312a..47b28f9e05dbc2 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -83,8 +83,7 @@ def update(self, cp, cp_cam): self.auto_high_beam = cp_cam.vl["DAS_6"]['AUTO_HIGH_BEAM_ON'] # Auto High Beam isn't Located in this message on chrysler or jeep currently located in 729 message ret.steerFaultTemporary = cp.vl["EPS_3"]["DASM_FAULT"] == 1 else: - steer_state = cp.vl["EPS_2"]["LKAS_STATE"] - ret.steerFaultPermanent = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) + ret.steerFaultPermanent = cp.vl["EPS_2"]["LKAS_STATE"] == 4 # blindspot sensors if self.CP.enableBsm: From bd2ea158977f5c26658bed8ac683b72c2c592d06 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 7 Jul 2022 14:19:30 -0700 Subject: [PATCH 266/436] Add Chinese (traditional) translations (#25064) * Add Chinese translations * wrap these * add to languages.json * fix tests * use tmp dir for tests (doesn't change translation files in git repo) * defaultdict not used * update main_zh.ts (test outdated QM file) * test outdated QM file (prev commit tests missing) * update qm file * add sidebar translations * no need for function --- selfdrive/ui/qt/sidebar.h | 14 +- selfdrive/ui/tests/test_translations.py | 46 +- selfdrive/ui/translations/languages.json | 3 +- selfdrive/ui/translations/main_zh.qm | Bin 0 -> 17617 bytes selfdrive/ui/translations/main_zh.ts | 1212 ++++++++++++++++++++++ selfdrive/ui/update_translations.py | 4 +- 6 files changed, 1254 insertions(+), 25 deletions(-) create mode 100644 selfdrive/ui/translations/main_zh.qm create mode 100644 selfdrive/ui/translations/main_zh.ts diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 98ae6564d6ba5b..4c6d8f47e5bc35 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -34,13 +34,13 @@ public slots: QPixmap home_img, settings_img; const QMap network_type = { - {cereal::DeviceState::NetworkType::NONE, "--"}, - {cereal::DeviceState::NetworkType::WIFI, "Wi-Fi"}, - {cereal::DeviceState::NetworkType::ETHERNET, "ETH"}, - {cereal::DeviceState::NetworkType::CELL2_G, "2G"}, - {cereal::DeviceState::NetworkType::CELL3_G, "3G"}, - {cereal::DeviceState::NetworkType::CELL4_G, "LTE"}, - {cereal::DeviceState::NetworkType::CELL5_G, "5G"} + {cereal::DeviceState::NetworkType::NONE, tr("--")}, + {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, + {cereal::DeviceState::NetworkType::ETHERNET, tr("ETH")}, + {cereal::DeviceState::NetworkType::CELL2_G, tr("2G")}, + {cereal::DeviceState::NetworkType::CELL3_G, tr("3G")}, + {cereal::DeviceState::NetworkType::CELL4_G, tr("LTE")}, + {cereal::DeviceState::NetworkType::CELL5_G, tr("5G")} }; const QRect settings_btn = QRect(50, 35, 200, 117); diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index ccea748e24054e..2dedf3d78561f2 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 import json import os +import shutil import unittest from selfdrive.ui.update_translations import TRANSLATIONS_DIR, LANGUAGES_FILE, update_translations +TMP_TRANSLATIONS_DIR = os.path.join(TRANSLATIONS_DIR, "tmp") + class TestTranslations(unittest.TestCase): @classmethod @@ -12,11 +15,25 @@ def setUpClass(cls): with open(LANGUAGES_FILE, "r") as f: cls.translation_files = json.load(f) + # Set up temp directory + shutil.copytree(TRANSLATIONS_DIR, TMP_TRANSLATIONS_DIR, dirs_exist_ok=True) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(TMP_TRANSLATIONS_DIR, ignore_errors=True) + + @staticmethod + def _read_translation_file(path, file, file_ext): + tr_file = os.path.join(path, f"{file}.{file_ext}") + with open(tr_file, "rb") as f: + # fix relative path depth + return f.read().replace(b"filename=\"../../", b"filename=\"../") + def test_missing_translation_files(self): for name, file in self.translation_files.items(): with self.subTest(name=name, file=file): if not len(file): - self.skipTest(f"{name} translation has no file") + self.skipTest(f"{name} translation has no defined file") self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")), f"{name} has no XML translation file, run selfdrive/ui/update_translations.py") @@ -24,26 +41,25 @@ def test_missing_translation_files(self): f"{name} has no compiled QM translation file, run selfdrive/ui/update_translations.py --release") def test_translations_updated(self): - suffix = "_test" - update_translations(suffix=suffix) + update_translations(release=True, translations_dir=TMP_TRANSLATIONS_DIR) for name, file in self.translation_files.items(): with self.subTest(name=name, file=file): - cur_tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}.ts") - new_tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}{suffix}.ts") - if not len(file): - self.skipTest(f"{name} translation has no file") - elif not os.path.exists(cur_tr_file): - self.skipTest(f"{name} missing translation file") # caught by test_missing_translation_files + self.skipTest(f"{name} translation has no defined file") + + for file_ext in ["ts", "qm"]: + with self.subTest(file_ext=file_ext): + + # caught by test_missing_translation_files + if not os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.{file_ext}")): + self.skipTest(f"{name} missing translation file") - with open(cur_tr_file, "r") as f: - cur_translations = f.read() - with open(new_tr_file, "r") as f: - new_translations = f.read() + cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file, file_ext) + new_translations = self._read_translation_file(TMP_TRANSLATIONS_DIR, file, file_ext) - self.assertEqual(cur_translations, new_translations, - f"{name} translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") + self.assertEqual(cur_translations, new_translations, + f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py --release to update the translation files") if __name__ == "__main__": diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index f2f9400d6412f5..e62de24a1e83a5 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -1,3 +1,4 @@ { - "English": "" + "English": "", + "中文(简体)": "main_zh" } diff --git a/selfdrive/ui/translations/main_zh.qm b/selfdrive/ui/translations/main_zh.qm new file mode 100644 index 0000000000000000000000000000000000000000..627f6afc204dba0b2c2a87f13cad266e9edcbd4f GIT binary patch literal 17617 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 ZKPoWEh4C}~ZXVfh^r2{cIZ?p!{{qG<2A}`{ literal 0 HcmV?d00001 diff --git a/selfdrive/ui/translations/main_zh.ts b/selfdrive/ui/translations/main_zh.ts new file mode 100644 index 00000000000000..7bcee2c23abe7f --- /dev/null +++ b/selfdrive/ui/translations/main_zh.ts @@ -0,0 +1,1212 @@ + + + + + AbstractAlert + + + Close + + + + + Snooze Update + 暂停更新 + + + + Reboot and Update + 重启和更新 + + + + AdvancedNetworking + + + Back + 后退 + + + + Enable Tethering + 启用网络共享 + + + + Tethering Password + 网络共享密码 + + + + + EDIT + 编辑 + + + + Enter new tethering password + 输入新的网络共享密码 + + + + IP Address + IP地址 + + + + Enable Roaming + 启用漫游 + + + + APN Setting + APN 设置 + + + + Enter APN + 输入 APN + + + + leave blank for automatic configuration + 为自动配置留空 + + + + ConfirmationDialog + + + + Ok + 好的 + + + + Cancel + 取消 + + + + DeclinePage + + + You must accept the Terms and Conditions in order to use openpilot. + 您必须接受条款和条件才能使用 openpilot。 + + + + Back + 后退 + + + + Decline, uninstall %1 + 拒绝,卸载 %1 + + + + DevicePanel + + + Dongle ID + 加密狗 ID + + + + N/A + 不适用 + + + + Serial + 串行 + + + + Driver Camera + 司机摄像头 + + + + PREVIEW + 预习 + + + + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) + 预览面向驾驶员的摄像头,以帮助优化设备安装位置以获得最佳驾驶员监控体验。 (车辆必须关闭) + + + + Reset Calibration + 重置校准 + + + + RESET + 重置 + + + + Are you sure you want to reset calibration? + 您确定要重置校准吗? + + + + Review Training Guide + 查看培训指南 + + + + REVIEW + 审查 + + + + Review the rules, features, and limitations of openpilot + 查看 openpilot 的规则、功能和限制 + + + + Are you sure you want to review the training guide? + 您确定要查看培训指南吗? + + + + Regulatory + 监管 + + + + VIEW + 看法 + + + Change Language + 改变语言 + + + CHANGE + 改变 + + + Select a language + 选择语言 + + + + Reboot + 重启 + + + + Power Off + 关机 + + + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot 要求设备安装在左或右 4° 以内,上 5° 或下 8° 以内。 openpilot 会持续校准,很少需要重置。 + + + + Your device is pointed %1° %2 and %3° %4. + 您的设备指向 %1° %2 和 %3° %4。 + + + + down + + + + + up + 向上 + + + + left + 剩下 + + + + right + 向右 + + + + Are you sure you want to reboot? + 您确定要重新启动吗? + + + + Disengage to Reboot + 脱离以重新启动 + + + + Are you sure you want to power off? + 您确定要关闭电源吗? + + + + Disengage to Power Off + 脱离以关闭电源 + + + + DriveStats + + + Drives + 驱动器 + + + + Hours + 小时 + + + + ALL TIME + 整天 + + + + PAST WEEK + 上周 + + + + KM + 千米 + + + + Miles + 迈尔斯 + + + + DriverViewScene + + + camera starting + 相机启动 + + + + InputDialog + + + Cancel + 取消 + + + + Need at least + 需要至少 + + + + characters! + 字符! + + + + Installer + + + Installing... + 安装... + + + + Receiving objects: + 接收物体: + + + + Resolving deltas: + 解决增量: + + + + Updating files: + 更新文件: + + + + MapPanel + + + Current Destination + 当前目的地 + + + + CLEAR + CLEAR + + + + Recent Destinations + 近期目的地 + + + + Try the Navigation Beta + 试用导航测试版 + + + + Get turn-by-turn directions displayed and more with a comma +prime subscription. Sign up now: https://connect.comma.ai + 使用逗号获取显示的详细路线和更多信息 +主要订阅。 立即注册:https://connect.comma.ai + + + + No home +location set + 没有家 +位置集 + + + + No work +location set + 没有工作 +位置集 + + + + no recent destinations + 没有最近的目的地 + + + + MultiOptionDialog + + Select + 选择 + + + Cancel + 取消 + + + + Networking + + + Advanced + 先进的 + + + + Enter password + 先进的 + + + + + for " + 为了 " + + + + Wrong password + Wrong password + + + + NvgWindow + + + km/h + 公里/小时 + + + + mph + 英里/小时 + + + + + MAX + 最大限度 + + + + + SPEED + 速度 + + + + + LIMIT + 限制 + + + + OffroadHome + + + UPDATE + 更新 + + + + ALERTS + 个警报 + + + + ALERT + 个警报 + + + + PairingPopup + + + Pair your device to your comma account + 将您的设备与您的逗号账户配对 + + + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> + <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> + <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> + </ol> + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>在手机上访问 https://connect.comma.ai</li> + <li style='margin-bottom: 50px;'>点击“添加新设备”,扫描右侧二维码</li> + <li style='margin-bottom: 50px;'>将 connect.comma.ai 收藏到您的主屏幕,以便像应用程序一样使用它</li> + </ol> + + + + + PrimeAdWidget + + + Upgrade Now + 现在升级 + + + + Become a comma prime member at connect.comma.ai + 成为 connect.comma.ai 的逗号主要会员 + + + + PRIME FEATURES: + 主要特点: + + + + Remote access + 远程访问 + + + + 1 year of storage + 1年存储 + + + + Developer perks + 开发者福利 + + + + PrimeUserWidget + + + ✓ SUBSCRIBED + ✓ 订阅 + + + + comma prime + 逗号素数 + + + + CONNECT.COMMA.AI + CONNECT.COMMA.AI + + + + COMMA POINTS + 逗号分 + + + + QObject + + + Reboot + 重启 + + + + Exit + 出口 + + + + dashcam + 行车记录器 + + + + openpilot + 开放式飞行员 + + + + %1 minute%2 ago + %1 分钟%2 前 + + + + %1 hour%2 ago + %1 小时%2 前 + + + + %1 day%2 ago + %1 天%2 前 + + + + Reset + + + Reset failed. Reboot to try again. + 重置失败。 重新启动以重试。 + + + + Are you sure you want to reset your device? + 您确定要重置您的设备吗? + + + + Resetting device... + 正在重置设备... + + + + System Reset + 系统重置 + + + + System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + 触发系统重置。 按确认删除所有内容和设置。 按取消恢复启动。 + + + + Cancel + 取消 + + + + Reboot + 重启 + + + + Confirm + 确认 + + + + Unable to mount data partition. Press confirm to reset your device. + 无法挂载数据分区。 按确认重置您的设备。 + + + + RichTextDialog + + + Ok + 好的 + + + + SettingsWindow + + + × + × + + + + Device + 设备 + + + + + Network + 网络 + + + + Toggles + 切换 + + + + Software + 软件 + + + + Navigation + 导航 + + + + Setup + + + WARNING: Low Voltage + 警告:低电压 + + + + Power your device in a car with a harness or proceed at your own risk. + 使用安全带在汽车中为您的设备供电或自行承担风险。 + + + + Power off + 关机 + + + + + + Continue + 继续 + + + + Getting Started + 入门 + + + + Before we get on the road, let’s finish installation and cover some details. + 在我们上路之前,让我们完成安装并介绍一些细节。 + + + + Connect to Wi-Fi + 连接到无线网络 + + + + + Back + 后退 + + + + Continue without Wi-Fi + 在没有 Wi-Fi 的情况下继续 + + + + Waiting for internet + 等待上网 + + + + Choose Software to Install + 选择要安装的软件 + + + + Dashcam + 行车记录器 + + + + Custom Software + 定制的软件 + + + + Enter URL + 输入网址 + + + + for Custom Software + 定制软件 + + + + Downloading... + 正在下载... + + + + Download Failed + 下载失败 + + + + Ensure the entered URL is valid, and the device’s internet connection is good. + 确保输入的 URL 有效,并且设备的互联网连接良好。 + + + + Reboot device + 重启设备 + + + + Start over + 重来 + + + + SetupWidget + + + Finish Setup + 完成设置 + + + + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. + 将您的设备与 comma connect (connect.comma.ai) 配对并领取您的 comma prime 优惠。 + + + + Pair device + 配对设备 + + + + Sidebar + + + + CONNECT + 连接 + + + + OFFLINE + 离线 + + + + + ONLINE + 在线的 + + + + ERROR + 错误 + + + + + + TEMP + 温度 + + + + HIGH + 高的 + + + + GOOD + 好的 + + + + OK + 好的 + + + + VEHICLE + 车辆 + + + + NO + + + + + PANDA + 熊猫 + + + + GPS + GPS + + + + SEARCH + 搜索 + + + + -- + -- + + + + Wi-Fi + Wi-Fi + + + + ETH + 以太網 + + + + 2G + 2G + + + + 3G + 3G + + + + LTE + LTE + + + + 5G + 5G + + + + SoftwarePanel + + + Git Branch + Git 分支 + + + + Git Commit + Git 提交 + + + + OS Version + 操作系统版本 + + + + Version + 版本 + + + + Last Update Check + 最后更新检查 + + + + The last time openpilot successfully checked for an update. The updater only runs while the car is off. + 上次 openpilot 成功检查更新的时间。 更新程序仅在汽车关闭时运行。 + + + + Check for Update + 检查更新 + + + + CHECKING + 正在检查 + + + + Uninstall + 卸载 + + + + UNINSTALL + 卸载 + + + + Are you sure you want to uninstall? + 您确定要卸载吗? + + + + failed to fetch update + 未能获取更新 + + + + + CHECK + 查看 + + + + SshControl + + + SSH Keys + SSH 密钥 + + + + Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. + 警告:这将授予对 GitHub 设置中所有公钥的 SSH 访问权限。 切勿输入您自己以外的 GitHub 用户名。 逗号员工永远不会要求您添加他们的 GitHub 用户名。 + + + + + ADD + 添加 + + + + Enter your GitHub username + 输入你的 GitHub 用户名 + + + + LOADING + 正在加载 + + + + REMOVE + 消除 + + + + Username '%1' has no keys on GitHub + 用户名“%1”在 GitHub 上没有密钥 + + + + Request timed out + 请求超时 + + + + Username '%1' doesn't exist on GitHub + GitHub 上不存在用户名“%1” + + + + SshToggle + + + Enable SSH + 启用 SSH + + + + TermsPage + + + Terms & Conditions + 条款和条件 + + + + Decline + 衰退 + + + + Scroll to accept + 滚动接受 + + + + Agree + 同意 + + + + TogglesPanel + + + Enable openpilot + 启用 openpilot + + + + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 使用 openpilot 系统进行自适应巡航控制和车道保持驾驶员辅助。 任何时候都需要您注意使用此功能。 更改此设置在汽车断电时生效。 + + + + Enable Lane Departure Warnings + 启用车道偏离警告 + + + + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). + 当您的车辆在以超过 31 英里/小时(50 公里/小时)的速度行驶时在检测到的车道线上漂移而没有激活转向信号时,接收提醒以返回车道。 + + + + Enable Right-Hand Drive + 启用右手驱动 + + + + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. + 允许 openpilot 遵守左侧交通惯例并在右侧驾驶座上执行驾驶员监控。 + + + + Use Metric System + 使用公制 + + + + Display speed in km/h instead of mph. + 以公里/小时而不是英里/小时显示速度。 + + + + Record and Upload Driver Camera + 记录和上传司机摄像头 + + + + Upload data from the driver facing camera and help improve the driver monitoring algorithm. + 从面向驾驶员的摄像头上传数据,帮助改进驾驶员监控算法。 + + + + Disengage On Accelerator Pedal + 松开加速踏板 + + + + When enabled, pressing the accelerator pedal will disengage openpilot. + 启用后,踩下油门踏板将解除 openpilot。 + + + + Show ETA in 24h format + 以 24 小时格式显示 ETA + + + + Use 24h format instead of am/pm + 使用 24 小时制代替上午/下午 + + + + openpilot Longitudinal Control + openpilot 纵向控制 + + + + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + openpilot 将禁用汽车的雷达并接管油门和刹车的控制。 警告:这会禁用 AEB! + + + + Updater + + + Update Required + 需要更新 + + + + An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. + 需要操作系统更新。 将您的设备连接到 Wi-Fi,以获得最快的更新体验。 下载大小约为 1GB。 + + + + Connect to Wi-Fi + 连接到无线网络 + + + + Install + 安装 + + + + Back + 后退 + + + + Loading... + 正在加载... + + + + Reboot + 重启 + + + + Update failed + 更新失败 + + + + WifiUI + + + + Scanning for networks... + 正在扫描网络... + + + + CONNECTING... + 正在连接... + + + + FORGET + 忘记 + + + + Forget Wi-Fi Network " + 忘记 Wi-Fi 网络" + + + diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index 5d57fa39d245bc..263eb5e6703a4c 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -10,7 +10,7 @@ LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") -def update_translations(release=False, suffix=""): +def update_translations(release=False, translations_dir=TRANSLATIONS_DIR): with open(LANGUAGES_FILE, "r") as f: translation_files = json.load(f) @@ -19,7 +19,7 @@ def update_translations(release=False, suffix=""): print(f"{name} has no translation file, skipping...") continue - tr_file = os.path.join(TRANSLATIONS_DIR, f"{file}{suffix}.ts") + tr_file = os.path.join(translations_dir, f"{file}.ts") ret = os.system(f"lupdate -recursive {UI_DIR} -ts {tr_file}") assert ret == 0 From b035b538ec5dc4ae837fe26ab900a34828525647 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 14:54:14 -0700 Subject: [PATCH 267/436] chill ram tune (#25071) --- selfdrive/car/chrysler/interface.py | 5 ++++- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 920000b271850f..697fb9b83adc00 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -51,7 +51,10 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.mass = 2493. + STD_CARGO_KG ret.maxLateralAccel = 2.4 ret.minSteerSpeed = 14.5 - CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + + ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[0.], [0.]] + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.1], [0.02]] + ret.lateralTuning.pid.kf = 0.00003 else: raise ValueError(f"Unsupported car: {candidate}") diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 0521fd829504d9..4908b8618296b9 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -cf46781e405a01c96307d30d1266d46e0fa92255 \ No newline at end of file +bd2ea158977f5c26658bed8ac683b72c2c592d06 \ No newline at end of file From dc3d94d662654a6bb2bf40560c2a17ed2303fa80 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 8 Jul 2022 06:01:02 +0800 Subject: [PATCH 268/436] UI: fix unable to scroll on 'Regulatory' page (#25014) * fix unable to scroll on 'Regulatory' page deleteLater after hide * remove alert * override exec * set MousePressEventDelay to 0 * set to 0.01 * cleanup * check event->pos() --- selfdrive/ui/qt/widgets/controls.cc | 4 +++- selfdrive/ui/qt/widgets/controls.h | 6 +++++- selfdrive/ui/qt/widgets/scrollview.cc | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 89c95843fb9c8a..a86c05a3c445b4 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -125,7 +125,9 @@ void ElidedLabel::paintEvent(QPaintEvent *event) { ClickableWidget::ClickableWidget(QWidget *parent) : QWidget(parent) { } void ClickableWidget::mouseReleaseEvent(QMouseEvent *event) { - emit clicked(); + if (rect().contains(event->pos())) { + emit clicked(); + } } // Fix stylesheets diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index b6684e28b29373..aed99edae8da5a 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -24,7 +24,11 @@ class ElidedLabel : public QLabel { protected: void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent* event) override; - void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + void mouseReleaseEvent(QMouseEvent *event) override { + if (rect().contains(event->pos())) { + emit clicked(); + } + } QString lastText_, elidedText_; }; diff --git a/selfdrive/ui/qt/widgets/scrollview.cc b/selfdrive/ui/qt/widgets/scrollview.cc index 1aa05b4157a1f6..bd4309d8d06049 100644 --- a/selfdrive/ui/qt/widgets/scrollview.cc +++ b/selfdrive/ui/qt/widgets/scrollview.cc @@ -37,7 +37,7 @@ ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) { sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue(QScrollerProperties::OvershootAlwaysOff)); - + sp.setScrollMetric(QScrollerProperties::MousePressEventDelay, 0.01); scroller->grabGesture(this->viewport(), QScroller::LeftMouseButtonGesture); scroller->setScrollerProperties(sp); } From ff3f6de149475ce995c21f8308da0bbc08922c54 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 16:53:34 -0700 Subject: [PATCH 269/436] UI: fix reset calibration description --- selfdrive/ui/qt/widgets/controls.cc | 30 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index a86c05a3c445b4..3264fd3aac76c4 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -45,21 +45,23 @@ AbstractControl::AbstractControl(const QString &title, const QString &desc, cons main_layout->addLayout(hlayout); // description - if (!desc.isEmpty()) { - description = new QLabel(desc); - description->setContentsMargins(40, 20, 40, 20); - description->setStyleSheet("font-size: 40px; color: grey"); - description->setWordWrap(true); - description->setVisible(false); - main_layout->addWidget(description); - - connect(title_label, &QPushButton::clicked, [=]() { - if (!description->isVisible()) { - emit showDescription(); - } + description = new QLabel(desc); + description->setContentsMargins(40, 20, 40, 20); + description->setStyleSheet("font-size: 40px; color: grey"); + description->setWordWrap(true); + description->setVisible(false); + main_layout->addWidget(description); + + connect(title_label, &QPushButton::clicked, [=]() { + if (!description->isVisible()) { + emit showDescription(); + } + + if (!description->text().isEmpty()) { description->setVisible(!description->isVisible()); - }); - } + } + }); + main_layout->addStretch(); } From a4c90765813e1cc3ab7afbe58cf23b392ec8181c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 17:09:36 -0700 Subject: [PATCH 270/436] Hyundai: limit Kona torque (#25074) --- selfdrive/car/hyundai/values.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index f6efedb2488f8d..c55a0dc63949bb 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -32,6 +32,12 @@ def __init__(self, CP): CAR.IONIQ_EV_LTD, CAR.SANTA_FE_PHEV_2022, CAR.SONATA_LF, CAR.KIA_FORTE, CAR.KIA_NIRO_HEV, CAR.KIA_OPTIMA_H, CAR.KIA_SORENTO, CAR.KIA_STINGER): self.STEER_MAX = 255 + + # These cars have significantly more torque than most HKG. Limit to 70% of max. + elif CP.carFingerprint in (CAR.KONA, CAR.KONA_EV, CAR.KONA_HEV): + self.STEER_MAX = 270 + + # Default for most HKG else: self.STEER_MAX = 384 From bd432eb76bf4c2d99a0f66b3a1df0067adb81a1a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 17:27:59 -0700 Subject: [PATCH 271/436] move kona limit to car controller --- selfdrive/car/hyundai/carcontroller.py | 7 ++++++- selfdrive/car/hyundai/values.py | 4 ---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index f3066bda03aa2f..73635375ad5810 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -52,7 +52,12 @@ def update(self, CC, CS): hud_control = CC.hudControl # Steering Torque - new_steer = int(round(actuators.steer * self.params.STEER_MAX)) + + # These cars have significantly more torque than most HKG. Limit to 70% of max. + steer = actuators.steer + if self.CP.carFingerprint in (CAR.KONA, CAR.KONA_EV, CAR.KONA_HEV): + steer = clip(steer, -0.7, 0.7) + new_steer = int(round(steer * self.params.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params) self.steer_rate_limited = new_steer != apply_steer diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index c55a0dc63949bb..4b3acf3f2703c1 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -33,10 +33,6 @@ def __init__(self, CP): CAR.KIA_OPTIMA_H, CAR.KIA_SORENTO, CAR.KIA_STINGER): self.STEER_MAX = 255 - # These cars have significantly more torque than most HKG. Limit to 70% of max. - elif CP.carFingerprint in (CAR.KONA, CAR.KONA_EV, CAR.KONA_HEV): - self.STEER_MAX = 270 - # Default for most HKG else: self.STEER_MAX = 384 From 1382e28aa0b32923a44efa28c5b25d9b02c41d68 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 7 Jul 2022 20:02:07 -0700 Subject: [PATCH 272/436] add pandas and tabulate packages (#25076) * add pandas and tabulate packages * remove that --- Pipfile | 4 +- Pipfile.lock | 1226 ++++++++++++++++++---------------- selfdrive/debug/can_table.py | 2 +- 3 files changed, 647 insertions(+), 585 deletions(-) diff --git a/Pipfile b/Pipfile index b8545b1a2190ad..81669e4807fe57 100644 --- a/Pipfile +++ b/Pipfile @@ -41,6 +41,8 @@ tenacity = "*" mpld3 = "*" carla = {version = "==0.9.13", markers="platform_system != 'Darwin'"} ft4222 = "*" +pandas = "*" +tabulate = "*" [packages] atomicwrites = "*" @@ -86,8 +88,6 @@ urllib3 = "*" utm = "*" websocket_client = "*" hatanaka = "==2.4" -PyQt5 = "==5.15.4" -PyQt5-sip = "==12.9.0" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index cc347a60f45c67..0612d0ff396133 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2629168f477b3a14f68f26e3b63ea9797b20599c066b2aa23a027bcee2ca40a" + "sha256": "c92514c0e6968af008916446514f41b4e004aa7aa4a2951cc1e9e258ac072111" }, "pipfile-spec": 6, "requires": { @@ -79,75 +79,89 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], "markers": "python_version >= '3.6'", - "version": "==2022.5.18.1" + "version": "==2022.6.15" }, "cffi": { "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "index": "pypi", - "version": "==1.15.0" + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "index": "pypi", + "version": "==1.15.1" }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" ], - "markers": "python_full_version >= '3.5.0'", - "version": "==2.0.12" + "markers": "python_version >= '3.6'", + "version": "==2.1.0" }, "click": { "hashes": [ @@ -169,31 +183,31 @@ }, "cryptography": { "hashes": [ - "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804", - "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178", - "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717", - "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982", - "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004", - "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe", - "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452", - "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336", - "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4", - "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15", - "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d", - "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c", - "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0", - "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06", - "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9", - "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1", - "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023", - "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de", - "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f", - "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181", - "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e", - "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a" - ], - "index": "pypi", - "version": "==37.0.2" + "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", + "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", + "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", + "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", + "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", + "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", + "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", + "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", + "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", + "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", + "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", + "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", + "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", + "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", + "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", + "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", + "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", + "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", + "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", + "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", + "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", + "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" + ], + "index": "pypi", + "version": "==37.0.4" }, "cython": { "hashes": [ @@ -347,31 +361,31 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_full_version >= '3.5.0'", + "markers": "python_version >= '3.5'", "version": "==3.3" }, "importlib-metadata": { "hashes": [ - "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", - "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" + "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", + "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" ], "markers": "python_version < '3.10'", - "version": "==4.11.4" + "version": "==4.12.0" }, "importlib-resources": { "hashes": [ - "sha256:b6062987dfc51f0fcb809187cffbd60f35df7acb4589091f154214af6d0d49d3", - "sha256:e447dc01619b1e951286f3929be820029d48c75eb25d265c28b92a16548212b8" + "sha256:568c9f16cb204f9decc8d6d24a572eeea27dacbb4cee9e6b03a8025736769751", + "sha256:7952325ffd516c05a8ad0858c74dff2c3343f136fe66a6002b2623dd1d43f223" ], "markers": "python_version >= '3.7'", - "version": "==5.7.1" + "version": "==5.8.0" }, "isort": { "hashes": [ "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.1'", + "markers": "python_version < '4' and python_full_version >= '3.6.1'", "version": "==5.10.1" }, "itsdangerous": { @@ -563,62 +577,58 @@ }, "numpy": { "hashes": [ - "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207", - "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887", - "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e", - "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802", - "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077", - "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af", - "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74", - "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5", - "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1", - "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0", - "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0", - "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e", - "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c", - "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c", - "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3", - "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72", - "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd", - "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6", - "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76", - "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32", - "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa", - "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba" - ], - "index": "pypi", - "version": "==1.22.4" + "sha256:092f5e6025813e64ad6d1b52b519165d08c730d099c114a9247c9bb635a2a450", + "sha256:196cd074c3f97c4121601790955f915187736f9cf458d3ee1f1b46aff2b1ade0", + "sha256:1c29b44905af288b3919803aceb6ec7fec77406d8b08aaa2e8b9e63d0fe2f160", + "sha256:2b2da66582f3a69c8ce25ed7921dcd8010d05e59ac8d89d126a299be60421171", + "sha256:5043bcd71fcc458dfb8a0fc5509bbc979da0131b9d08e3d5f50fb0bbb36f169a", + "sha256:58bfd40eb478f54ff7a5710dd61c8097e169bc36cc68333d00a9bcd8def53b38", + "sha256:79a506cacf2be3a74ead5467aee97b81fca00c9c4c8b3ba16dbab488cd99ba10", + "sha256:94b170b4fa0168cd6be4becf37cb5b127bd12a795123984385b8cd4aca9857e5", + "sha256:97a76604d9b0e79f59baeca16593c711fddb44936e40310f78bfef79ee9a835f", + "sha256:98e8e0d8d69ff4d3fa63e6c61e8cfe2d03c29b16b58dbef1f9baa175bbed7860", + "sha256:ac86f407873b952679f5f9e6c0612687e51547af0e14ddea1eedfcb22466babd", + "sha256:ae8adff4172692ce56233db04b7ce5792186f179c415c37d539c25de7298d25d", + "sha256:bd3fa4fe2e38533d5336e1272fc4e765cabbbde144309ccee8675509d5cd7b05", + "sha256:d0d2094e8f4d760500394d77b383a1b06d3663e8892cdf5df3c592f55f3bff66", + "sha256:d54b3b828d618a19779a84c3ad952e96e2c2311b16384e973e671aa5be1f6187", + "sha256:d6ca8dabe696c2785d0c8c9b0d8a9b6e5fdbe4f922bde70d57fa1a2848134f95", + "sha256:d8cc87bed09de55477dba9da370c1679bd534df9baa171dd01accbb09687dac3", + "sha256:f0f18804df7370571fb65db9b98bf1378172bd4e962482b857e612d1fec0f53e", + "sha256:f1d88ef79e0a7fa631bb2c3dda1ea46b32b1fe614e10fedd611d3d5398447f2f", + "sha256:f9c3fc2adf67762c9fe1849c859942d23f8d3e0bee7b5ed3d4a9c3eeb50a2f07", + "sha256:fc431493df245f3c627c0c05c2bd134535e7929dbe2e602b80e42bf52ff760bc", + "sha256:fe8b9683eb26d2c4d5db32cd29b38fdcf8381324ab48313b5b69088e0e355379" + ], + "index": "pypi", + "version": "==1.23.0" }, "onnx": { "hashes": [ - "sha256:0cf47c205b376b3763beef92a6de4152f3b1552d6f640d93044938500baf5958", - "sha256:3403884c482859f8cf2e0c276da84bd9ac2235d266726f4ddc9625d3fd263218", - "sha256:43b32a2f20c94aa98866deae9e4218faf0495144ad05402e918fa279674b6df9", - "sha256:4454906de80a351de6929b0896ad605d106c324c3112c92249240e531f68fbba", - "sha256:4aa899f74acd4c5543f0efed8bfe98a3b701df75c5ffa179212e3088c51971bb", - "sha256:58d4873ec587ac14c44227d8027787edc88cd61596e646e3417f2a826a920898", - "sha256:593ca9e11f15afa26b3aaf2d170bb803d4bd86dbd560aa7be4e5f535d03f83d5", - "sha256:67c6d2654c1c203e5c839a47900b51f588fd0de71bbd497fb193d30a0b3ec1e9", - "sha256:7924d9baa13dbbf335737229f6d068f380d153679f357e495da60007b61cf56d", - "sha256:7a2f5d6998fe79aed80fad9d4522140d02c4d29513047e335d5c5355c1ebda5e", - "sha256:82221a07707b1ccf71fb18c6abb77f2566517a55d5185809775b5ff008bfb35c", - "sha256:89420e5b824d7e182846fe2aa09190ddb41162b261465c6ca928174bc2ac10b7", - "sha256:997d91ffd7b7ae7aee09c6d652a896d906be430d425865c759b51a8de5df9fe0", - "sha256:9b9f58ea01c1b20b057f55f628df4fc0403bbc160b7282a56e3bb4df5c7fb96f", - "sha256:a6e9135f1d02539ca7573f699fb0d31d3c43d10fac1d2d2239a9a1c553506c29", - "sha256:ae74bf8fa343b64e2b7fe205091b7f3728887c018ae061d161dd86ec95eb66a8", - "sha256:b2de0b117ad77689d308824a0c9eb89539ec28a799b4e2e05b3bb977b0da0b45", - "sha256:c3d3503110f2cab2c818f4a7b2bc8abc3bc79649daa39e70d5fb504b208ddb1e", - "sha256:d6581dd2122525549d1d8b431b8bf375298993c77bddb8fd0bf0d92611df76a1", - "sha256:d6ddbe89e32f885db736d36fcb132784e368331a18c3b6168ac9f561eb462057", - "sha256:df85666ab2b88fd9cf9b2504bcb551da39422eab65a143926a8db58f81b09164", - "sha256:ea06dbf57a287657b6dc4e189918e4cb451450308589d482117216194d6f83d6", - "sha256:eb46f31f12bb0bfdcfb68497d10b20447cf8fa6c4f693120c013e052645357b8", - "sha256:eca224c7c2c8ee4072a0743e4898a84a9bdf8297b5e5910a2632e4c4182ffb2a", - "sha256:f335d982b8ed201cf767459b993630acfd20c32b100529f70af9f28a26e72167" - ], - "index": "pypi", - "version": "==1.11.0" + "sha256:13b3e77d27523b9dbf4f30dfc9c959455859d5e34e921c44f712d69b8369eff9", + "sha256:213e73610173f6b2e99f99a4b0636f80b379c417312079d603806e48ada4ca8b", + "sha256:23781594bb8b7ee985de1005b3c601648d5b0568a81e01365c48f91d1f5648e4", + "sha256:2d9a7db54e75529160337232282a4816cc50667dc7dc34be178fd6f6b79d4705", + "sha256:341c7016e23273e9ffa9b6e301eee95b8c37d0f04df7cedbdb169d2c39524c96", + "sha256:3c6e6bcffc3f5c1e148df3837dc667fa4c51999788c1b76b0b8fbba607e02da8", + "sha256:5578b93dc6c918cec4dee7fb7d9dd3b09d338301ee64ca8b4f28bc217ed42dca", + "sha256:56ceb7e094c43882b723cfaa107d85ad673cfdf91faeb28d7dcadacca4f43a07", + "sha256:81a3555fd67be2518bf86096299b48fb9154652596219890abfe90bd43a9ec13", + "sha256:8a7aa61aea339bd28f310f4af4f52ce6c4b876386228760b16308efd58f95059", + "sha256:9fd2f4e23078df197bb76a59b9cd8f5a43a6ad2edc035edb3ecfb9042093e05a", + "sha256:af90427ca04c6b7b8107c2021e1273227a3ef1a7a01f3073039cae7855a59833", + "sha256:b3629e8258db15d4e2c9b7f1be91a3186719dd94661c218c6f5fde3cc7de3d4d", + "sha256:bdbd2578424c70836f4d0f9dda16c21868ddb07cc8192f9e8a176908b43d694b", + "sha256:c11162ffc487167da140f1112f49c4f82d815824f06e58bc3095407699f05863", + "sha256:c39a7a0352c856f1df30dccf527eb6cb4909052e5eaf6fa2772a637324c526aa", + "sha256:c7a9b3ea02c30efc1d2662337e280266aca491a8e86be0d8a657f874b7cccd1e", + "sha256:f66d2996e65f490a57b3ae952e4e9189b53cc9fe3f75e601d50d4db2dc1b1cd9", + "sha256:f8800f28c746ab06e51ef8449fd1215621f4ddba91be3ffc264658937d38a2af", + "sha256:fab13feb4d94342aae6d357d480f2e47d41b9f4e584367542b21ca6defda9e0a", + "sha256:fea5156a03398fe0e23248042d8651c1eaac5f6637d4dd683b4c1f1320b9f7b4" + ], + "index": "pypi", + "version": "==1.12.0" }, "onnxruntime-gpu": { "hashes": [ @@ -635,47 +645,67 @@ }, "pillow": { "hashes": [ - "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f", - "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d", - "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b", - "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c", - "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9", - "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546", - "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578", - "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1", - "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe", - "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098", - "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2", - "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a", - "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45", - "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530", - "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108", - "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1", - "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd", - "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0", - "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6", - "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c", - "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf", - "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4", - "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d", - "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765", - "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602", - "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340", - "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c", - "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b", - "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84", - "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8", - "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92", - "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54", - "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601", - "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a", - "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf", - "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251", - "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a", - "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e" - ], - "index": "pypi", - "version": "==9.1.1" + "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", + "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", + "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", + "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", + "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", + "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", + "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", + "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", + "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", + "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", + "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", + "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", + "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", + "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", + "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", + "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", + "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", + "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8", + "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", + "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", + "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", + "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", + "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", + "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", + "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", + "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", + "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9", + "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", + "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", + "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", + "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", + "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", + "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", + "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", + "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", + "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", + "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", + "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", + "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", + "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", + "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", + "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", + "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", + "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", + "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", + "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", + "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", + "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", + "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", + "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", + "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", + "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", + "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", + "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", + "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", + "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" + ], + "index": "pypi", + "version": "==9.2.0" }, "platformdirs": { "hashes": [ @@ -803,39 +833,39 @@ }, "pycryptodome": { "hashes": [ - "sha256:028dcbf62d128b4335b61c9fbb7dd8c376594db607ef36d5721ee659719935d5", - "sha256:12ef157eb1e01a157ca43eda275fa68f8db0dd2792bc4fe00479ab8f0e6ae075", - "sha256:2562de213960693b6d657098505fd4493c45f3429304da67efcbeb61f0edfe89", - "sha256:27e92c1293afcb8d2639baf7eb43f4baada86e4de0f1fb22312bfc989b95dae2", - "sha256:36e3242c4792e54ed906c53f5d840712793dc68b726ec6baefd8d978c5282d30", - "sha256:50a5346af703330944bea503106cd50c9c2212174cfcb9939db4deb5305a8367", - "sha256:53dedbd2a6a0b02924718b520a723e88bcf22e37076191eb9b91b79934fb2192", - "sha256:69f05aaa90c99ac2f2af72d8d7f185f729721ad7c4be89e9e3d0ab101b0ee875", - "sha256:75a3a364fee153e77ed889c957f6f94ec6d234b82e7195b117180dcc9fc16f96", - "sha256:766a8e9832128c70012e0c2b263049506cbf334fb21ff7224e2704102b6ef59e", - "sha256:7fb90a5000cc9c9ff34b4d99f7f039e9c3477700e309ff234eafca7b7471afc0", - "sha256:893f32210de74b9f8ac869ed66c97d04e7d351182d6d39ebd3b36d3db8bda65d", - "sha256:8b5c28058102e2974b9868d72ae5144128485d466ba8739abd674b77971454cc", - "sha256:924b6aad5386fb54f2645f22658cb0398b1f25bc1e714a6d1522c75d527deaa5", - "sha256:9924248d6920b59c260adcae3ee231cd5af404ac706ad30aa4cd87051bf09c50", - "sha256:9ec761a35dbac4a99dcbc5cd557e6e57432ddf3e17af8c3c86b44af9da0189c0", - "sha256:a36ab51674b014ba03da7f98b675fcb8eabd709a2d8e18219f784aba2db73b72", - "sha256:aae395f79fa549fb1f6e3dc85cf277f0351e15a22e6547250056c7f0c990d6a5", - "sha256:c880a98376939165b7dc504559f60abe234b99e294523a273847f9e7756f4132", - "sha256:ce7a875694cd6ccd8682017a7c06c6483600f151d8916f2b25cf7a439e600263", - "sha256:d1b7739b68a032ad14c5e51f7e4e1a5f92f3628bba024a2bda1f30c481fc85d8", - "sha256:dcd65355acba9a1d0fc9b923875da35ed50506e339b35436277703d7ace3e222", - "sha256:e04e40a7f8c1669195536a37979dd87da2c32dbdc73d6fe35f0077b0c17c803b", - "sha256:e0c04c41e9ade19fbc0eff6aacea40b831bfcb2c91c266137bcdfd0d7b2f33ba", - "sha256:e24d4ec4b029611359566c52f31af45c5aecde7ef90bf8f31620fd44c438efe7", - "sha256:e64738207a02a83590df35f59d708bf1e7ea0d6adce712a777be2967e5f7043c", - "sha256:ea56a35fd0d13121417d39a83f291017551fa2c62d6daa6b04af6ece7ed30d84", - "sha256:f2772af1c3ef8025c85335f8b828d0193fa1e43256621f613280e2c81bfad423", - "sha256:f403a3e297a59d94121cb3ee4b1cf41f844332940a62d71f9e4a009cc3533493", - "sha256:f572a3ff7b6029dd9b904d6be4e0ce9e309dcb847b03e3ac8698d9d23bb36525" - ], - "index": "pypi", - "version": "==3.14.1" + "sha256:045d75527241d17e6ef13636d845a12e54660aa82e823b3b3341bcf5af03fa79", + "sha256:0926f7cc3735033061ef3cf27ed16faad6544b14666410727b31fea85a5b16eb", + "sha256:092a26e78b73f2530b8bd6b3898e7453ab2f36e42fd85097d705d6aba2ec3e5e", + "sha256:1b22bcd9ec55e9c74927f6b1f69843cb256fb5a465088ce62837f793d9ffea88", + "sha256:2aa55aae81f935a08d5a3c2042eb81741a43e044bd8a81ea7239448ad751f763", + "sha256:2ea63d46157386c5053cfebcdd9bd8e0c8b7b0ac4a0507a027f5174929403884", + "sha256:2ec709b0a58b539a4f9d33fb8508264c3678d7edb33a68b8906ba914f71e8c13", + "sha256:2ffd8b31561455453ca9f62cb4c24e6b8d119d6d531087af5f14b64bee2c23e6", + "sha256:4b52cb18b0ad46087caeb37a15e08040f3b4c2d444d58371b6f5d786d95534c2", + "sha256:4c3ccad74eeb7b001f3538643c4225eac398c77d617ebb3e57571a897943c667", + "sha256:5099c9ca345b2f252f0c28e96904643153bae9258647585e5e6f649bb7a1844a", + "sha256:57f565acd2f0cf6fb3e1ba553d0cb1f33405ec1f9c5ded9b9a0a5320f2c0bd3d", + "sha256:60b4faae330c3624cc5a546ba9cfd7b8273995a15de94ee4538130d74953ec2e", + "sha256:7c9ed8aa31c146bef65d89a1b655f5f4eab5e1120f55fc297713c89c9e56ff0b", + "sha256:7e3a8f6ee405b3bd1c4da371b93c31f7027944b2bcce0697022801db93120d83", + "sha256:9135dddad504592bcc18b0d2d95ce86c3a5ea87ec6447ef25cfedea12d6018b8", + "sha256:9c772c485b27967514d0df1458b56875f4b6d025566bf27399d0c239ff1b369f", + "sha256:9eaadc058106344a566dc51d3d3a758ab07f8edde013712bc8d22032a86b264f", + "sha256:9ee40e2168f1348ae476676a2e938ca80a2f57b14a249d8fe0d3cdf803e5a676", + "sha256:a8f06611e691c2ce45ca09bbf983e2ff2f8f4f87313609d80c125aff9fad6e7f", + "sha256:b9c5b1a1977491533dfd31e01550ee36ae0249d78aae7f632590db833a5012b8", + "sha256:b9cc96e274b253e47ad33ae1fccc36ea386f5251a823ccb50593a935db47fdd2", + "sha256:c3640deff4197fa064295aaac10ab49a0d55ef3d6a54ae1499c40d646655c89f", + "sha256:c77126899c4b9c9827ddf50565e93955cb3996813c18900c16b2ea0474e130e9", + "sha256:d2a39a66057ab191e5c27211a7daf8f0737f23acbf6b3562b25a62df65ffcb7b", + "sha256:e244ab85c422260de91cda6379e8e986405b4f13dc97d2876497178707f87fc1", + "sha256:ecaaef2d21b365d9c5ca8427ffc10cebed9d9102749fd502218c23cb9a05feb5", + "sha256:fd2184aae6ee2a944aaa49113e6f5787cdc5e4db1eb8edb1aea914bd75f33a0c", + "sha256:ff287bcba9fbeb4f1cccc1f2e90a08d691480735a611ee83c80a7d74ad72b9d9", + "sha256:ff7ae90e36c1715a54446e7872b76102baa5c63aa980917f4aa45e8c78d1a3ec" + ], + "index": "pypi", + "version": "==3.15.0" }, "pyflakes": { "hashes": [ @@ -855,101 +885,54 @@ }, "pylint": { "hashes": [ - "sha256:549261e0762c3466cc001024c4419c08252cb8c8d40f5c2c6966fea690e7fe2a", - "sha256:bb71e6d169506de585edea997e48d9ff20c0dc0e2fbc1d166bad6b640120326b" + "sha256:47705453aa9dce520e123a7d51843d5f0032cbfa06870f89f00927aa1f735a4a", + "sha256:89b61867db16eefb7b3c5b84afc94081edaf11544189e2b238154677529ad69f" ], "index": "pypi", - "version": "==2.14.1" + "version": "==2.14.4" }, "pyopencl": { "hashes": [ - "sha256:01030054c201b021715deb3d6f1355844f9795429dfa0591b59b6f8000ec2d38", - "sha256:02997935ac164f519be65c371f9dd2267a2b7532247dc0a2ef43f435cf76cf4b", - "sha256:07482df440e1246cba6dc46ef70d3ebf1a6c8157a3c6456091026c7f9e4d18d2", - "sha256:0b179591c60b4446846fbea035cb3d1acd2685b0226ba91724109882dc59af2c", - "sha256:15ebc3f3eb2df1d196a7dcefd68d0e9ffa11e275f8a6c57a1145a1d0ff36c382", - "sha256:1a5fb7dc32cf24cdeab1205bc075710d7112656720c2bf9972bebe906e28ec4b", - "sha256:1b649637d608e8dabdec0e0f85392f727fdf622463b425cf7587bdd313b4d9eb", - "sha256:22eed49903178bc686287192a8319ce763129b4e5d42a9dfb5d8f763ba5d6bd6", - "sha256:2deef59d73d0bdd11ba40613ab0798c767214a669a1a5a672500787fb7da63d3", - "sha256:3736bfdc946068be66fe4b5c680926c84366b724b3c4b649b2a1940f7bd6afde", - "sha256:3dd0b5ff24d12ad4c13446d8e5439e63914496dfcf9e23a26baeaa65ec3c7039", - "sha256:5430b938e9391309be2ffaefb6269a0a3c016af5d729121cf8a5fce62a5146c2", - "sha256:5e89596e7f18824fc1f84e2cb0ae059fbfe187d1e2e3919ab0cd701cc634eb03", - "sha256:65e406603fbe47ca72298e022a3c3855b2e1732cb9d04ecbb411025050d0bc57", - "sha256:6f9f91594358af6a9728908c31c5ed4bec3fe1a0d25c6292e37e40c92903fe36", - "sha256:77a70b76789aac85566cb0e3ff6b60c4c00729bbd7f0edd24ac4b3b43e4627e2", - "sha256:799355c27463bf801260e3398643c3c9359627fa9e6ac621cfb5dc1d6e77d859", - "sha256:7ae4825562f7c5956b8926cb99882df1631c5e28aa1310d896c22a8471cf8f56", - "sha256:7b17906a4821a30aa1ce7a9d783bba2564230ea6a55ff31eb3f0e2a4aa5b80af", - "sha256:7d4bf4c858554e9e3af9e7f18b06e8d6c39b25d7a80c28db6e5dd412dc457aee", - "sha256:8981a9274796272508158b08a3cb1a5711318cf32b5f0e4829edecb1a9efcf93", - "sha256:8da3ef5a03cfd0a9859a5ebe623f3c43037e9f0dffd2b658e944bbc381beb529", - "sha256:94c744997f4aff86e68fa3a5d383dc8b5f1e529a360156b82c7583a757eddaa5", - "sha256:9a7fb5769bce7ec09a2d264a233ec9c730b15b391c830d04a381df3fc85bdaca", - "sha256:a6ce276a42caedd3a9a7be00031cfe6bf5d1796efdac40e47f1b707846c96d86", - "sha256:a84310ae508f998ed31825b6e3ab888098cb69a2c627bf5970706620a8d4b127", - "sha256:ad08e37cdeda5d38ac3ca9820400da62ce3a67aab76a5eefa5d089ef3a4877c9", - "sha256:b6e426b5fdce61051b112825da20df4cb78429967e491223bfedaf95c025273a", - "sha256:bb363f9993013b04c0b146e269a73b3d5ebef30f78d5fa542f317cc2440e15b6", - "sha256:c58f05b050ae4ac3b0584d97738ae7ac4381e611567b9d67fe7cf4210c0a7b62", - "sha256:c84ef85cf6b83dbcef4e034390fc1ed6bd8eadf5260b5ae89515d3b9744ef207", - "sha256:c9a841b80ef4c332a6133377fc295fe5376f90f8f2e7c63d36903b07b8ea7262", - "sha256:cd5871aff617d3c9d338fd94c9187382390db82452be0868055b8c519a73445d", - "sha256:cf45c232bf818ef54ee831eb41f4edbd5dfe4c67d894b1e65fc17a690a63c81e", - "sha256:e90bd1ed69cca2a750ffabafc70b4f9eb4d109299e986c3c8fdc4c40fee36ef2", - "sha256:ea5b6ef0e4ad23a3ccbdb382f7cccadbb200a47ceb6ff3e965a3c6c46360b4c2", - "sha256:f433ddd7bfd688b591ea95b6971e5e6cb00f8d5f2dc5db833e528e5ede6909d6" - ], - "index": "pypi", - "version": "==2022.1.5" - }, - "pyqt5": { - "hashes": [ - "sha256:213bebd51821ed89b4d5b35bb10dbe67564228b3568f463a351a08e8b1677025", - "sha256:2a69597e0dd11caabe75fae133feca66387819fc9bc050f547e5551bce97e5be", - "sha256:883a549382fc22d29a0568f3ef20b38c8e7ab633a59498ac4eb63a3bf36d3fd3", - "sha256:8c0848ba790a895801d5bfd171da31cad3e551dbcc4e59677a3b622de2ceca98", - "sha256:a88526a271e846e44779bb9ad7a738c6d3c4a9d01e15a128ecfc6dd4696393b7" - ], - "index": "pypi", - "version": "==5.15.4" - }, - "pyqt5-qt5": { - "hashes": [ - "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", - "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962", - "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", - "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327" - ], - "version": "==5.15.2" - }, - "pyqt5-sip": { - "hashes": [ - "sha256:055581c6fed44ba4302b70eeb82e979ff70400037358908f251cd85cbb3dbd93", - "sha256:0fc9aefacf502696710b36cdc9fa2a61487f55ee883dbcf2c2a6477e261546f7", - "sha256:42274a501ab4806d2c31659170db14c282b8313d2255458064666d9e70d96206", - "sha256:4347bd81d30c8e3181e553b3734f91658cfbdd8f1a19f254777f906870974e6d", - "sha256:485972daff2fb0311013f471998f8ec8262ea381bded244f9d14edaad5f54271", - "sha256:4f8e05fe01d54275877c59018d8e82dcdd0bc5696053a8b830eecea3ce806121", - "sha256:69a3ad4259172e2b1aa9060de211efac39ddd734a517b1924d9c6c0cc4f55f96", - "sha256:6a8701892a01a5a2a4720872361197cc80fdd5f49c8482d488ddf38c9c84f055", - "sha256:6d5bca2fc222d58e8093ee8a81a6e3437067bb22bc3f86d06ec8be721e15e90a", - "sha256:83c3220b1ca36eb8623ba2eb3766637b19eb0ce9f42336ad8253656d32750c0a", - "sha256:a25b9843c7da6a1608f310879c38e6434331aab1dc2fe6cb65c14f1ecf33780e", - "sha256:ac57d796c78117eb39edd1d1d1aea90354651efac9d3590aac67fa4983f99f1f", - "sha256:b09f4cd36a4831229fb77c424d89635fa937d97765ec90685e2f257e56a2685a", - "sha256:c446971c360a0a1030282a69375a08c78e8a61d568bfd6dab3dcc5cf8817f644", - "sha256:c5216403d4d8d857ec4a61f631d3945e44fa248aa2415e9ee9369ab7c8a4d0c7", - "sha256:d3e4489d7c2b0ece9d203ae66e573939f7f60d4d29e089c9f11daa17cfeaae32", - "sha256:d59af63120d1475b2bf94fe8062610720a9be1e8940ea146c7f42bb449d49067", - "sha256:d85002238b5180bce4b245c13d6face848faa1a7a9e5c6e292025004f2fd619a", - "sha256:d8b2bdff7bbf45bc975c113a03b14fd669dc0c73e1327f02706666a7dd51a197", - "sha256:dd05c768c2b55ffe56a9d49ce6cc77cdf3d53dbfad935258a9e347cbfd9a5850", - "sha256:fc43f2d7c438517ee33e929e8ae77132749c15909afab6aeece5fcf4147ffdb5" - ], - "index": "pypi", - "version": "==12.9.0" + "sha256:069e7eb1a223d88c13eafa54d6ae896fa892e75ba3d56ff2135a26107ef1142b", + "sha256:1490e6cdeaecba42854013c273685d65fd9102ee6dc6bc3bcb814e9e2b8179e5", + "sha256:15f7b3d29c9359e1e440e4f52f70de031f8d0d8d0f8de53a3bc01501b89360c0", + "sha256:1a2029b7fda6709eca077f618f997372c3d6f2780ad45512632b0d056e6305f9", + "sha256:25e87b4ccc0cc53487d445bea07ce9bdb478a335725df16986aead2ff65b68a4", + "sha256:2c9ad1cbc3f540afc52038851be8e06640aacfece051c89408bc3aece605a7ee", + "sha256:2df01c95ea9ae3dd66b277f0df47144cf7535a27b48a8d49fdd98e0583e368ce", + "sha256:316f59d0c40bfce4f6c160dbaf6501883b33880370bb1819f360dad747e52dfe", + "sha256:4836bc4619be967d6c28627adac151223037fdca056c4ab54da16b591f719347", + "sha256:4b53f7f3ed85ab671c8bfc61a0bbc5476725a7a5f51a94bba5512c3962b2d609", + "sha256:5304cb336af7316ae0650abb7467c076032635bfe4710b8df191612d245dca28", + "sha256:55e9302b8f0b1964c87b0fdab7b853aa2b2f10b4188f5b4618782d4380448c11", + "sha256:6032bef8a35f6df727a0b66e3c9faedb3f560318052848b28d2f72622cfbeace", + "sha256:6ec55934057e99461f684ccd293d87db59a452f5834c13ae36b19d31dfe38599", + "sha256:7176f96728be9b43024bd71704f60849cbfcf0fafd20270181b68ea4730ceb2d", + "sha256:730901d409d8251cd6e9dc59e6c518dff5cdb20a3a0b728344bfd2c707f28b64", + "sha256:75be43c7f33fb86f9d18b7b6f8e9081d8bd5b6331a90aec0d2cad3e81e72bc8f", + "sha256:7bef8e8bcfff574b481565390113ea0a37cf33fd2587ade7f2980f15e73f7b08", + "sha256:7ca9597877e1f8bdb4a49810988230f538b2d7aac389c33418a21cf4358f2fd4", + "sha256:814389b3eb9e6930cf43b984283c94a955edf20ec286402da5acfa503d3ae790", + "sha256:8efc3467454ce8c644f09029a3308496f9cb6e93ca5e8c08f6b79e7825da72c5", + "sha256:98bad7035f27b6de5c9268f52c1e10bffe3a2874994e862468a1792b699a4884", + "sha256:9bbfe94bb6e9d0458693183334e73c973e2fcba01568f42db15b453b926fb816", + "sha256:9d112a4426f5b356641c1312bf1004247dc4019e649502589b86333557203c01", + "sha256:a845779f505ed57b83f279307ae6307d886f3e41fb24dcf7889da27daa726118", + "sha256:aca3581f1a7f6b809b8cdc78b0e66587848b38b143bf2983e91ff8fb9a41bc8f", + "sha256:af5664b98140a29966c5fb12e9d29b85b6c6310efa97d82aee58310774917e8f", + "sha256:b85fa5ba1678dd40713587fd437787b6aa940000c2ddffa360884431be21723a", + "sha256:bcabfb5217ca8f8770f9c69298f79576080bb994b1883a99494b4c2668b04836", + "sha256:c00989bed1e7e5b32ad498fec3deb1c93403ab802cd99b7c78b9c692bd0910ef", + "sha256:d0ddc3b74ad1804eb3fe238dfa3b844b997e88b1ca5164a717c16b362b4f34c3", + "sha256:d8bb2eea4e960917e0a6132dedd34c8ec0b7a384f22713f775d50dbce154263a", + "sha256:db833ebb1e756969a8f851f15486598eb9e3fb27b0535c2a8193cc1c71455016", + "sha256:dc2d78cb5da0081ada1c263aaa773fd5479b3da5e2c421547bf7f3258d3239a5", + "sha256:dd2728e59ae088c900ed68f68d953476d0ff07189f182f917b74de2ac7b3972e", + "sha256:ea4eff6b922fa4ad2077ef90b3254d78597d050ada09bfbe74c22dd22d10c6ac", + "sha256:f8887d54e654598f3854472540b2eb228ac56b56a2491b95bdfac8f15be1c943" + ], + "index": "pypi", + "version": "==2022.1.6" }, "pyserial": { "hashes": [ @@ -969,10 +952,10 @@ }, "pytools": { "hashes": [ - "sha256:3393d25029982080e3fb94c47bf627a1e553ccd174fe2edef6c1c5ec723918ff" + "sha256:4d62875e9a2ab2a24e393a9a8b799492f1a721bffa840af3807bfd42871dd1f4" ], "markers": "python_version ~= '3.6'", - "version": "==2022.1.9" + "version": "==2022.1.12" }, "pyyaml": { "hashes": [ @@ -1015,74 +998,74 @@ }, "pyzmq": { "hashes": [ - "sha256:057176dd3f5ccf5aad4abd662d76b6a39bbf799baaf2f39cd4fdaf2eab326e43", - "sha256:05ec90a8da618f2398f9d1aa20b18a9ef332992c6ac23e8c866099faad6ef0d6", - "sha256:154de02b15422af28b53d29a02de72121ba503634955017255573fc1f995143d", - "sha256:16b832adb5d8716f46051da5533c480250bf126984ce86804db6137a3a7f931b", - "sha256:1df26aa854bdd3a8341bf199064dd6aa6e240f2eaa3c9fa8d217e5d8b868c73e", - "sha256:28f9164fb2658b7b414fa0894c75b1a9c61375774cdc1bdb7298beb042a2cd87", - "sha256:2951c29b8649f3672af9dca8ff61d86310d3664d9629788b1c66422fb13b1239", - "sha256:2b08774057ae7ce8a2eb4e7d54db05358234440706ce43a85814500c5d7bd22e", - "sha256:2e2ac40f7a91c740ec68d6db07ae19ea9259c959333c68bee56ab2c799a67d66", - "sha256:312e56799410c34797417a4060a8bd37d4db1f06d1ec0c54f7c8fd81e0d90376", - "sha256:38f778a74e3889392e949326cfd0e9b2eb37dcbb2980d98fad2c51703d523db2", - "sha256:3955dd5bbbe02f454655296ee36a66c334c7102a29b8458223d168c0380edfd5", - "sha256:425ba851a6f9892bde1da2024d82e2fe6796bd77e3391fb96665c50fe9d4c6a5", - "sha256:48bbc2db041ab28eeee4a3e8ada0ed336640946dd5a8e53dbd3805f9dbdcf0dc", - "sha256:4fbcd657cda75574fd1315a4c44bd322bc2e219039fb09f146bbe6f8aef039e9", - "sha256:523ba7fd4d8fe75ad09c1e574a648892b75a97d0cfc8005727681053ac19555b", - "sha256:53b2c1326c2e484d450932d2be739f064b7cb572faabec38386098a28516a529", - "sha256:540d7146c3cdc9bbffab039ea067f494eba24d1abe5bd33eb9f963c01e3305d4", - "sha256:563d4281c4dbdf647d93114420151d33f895afc4c46b7115a67a0aa5347e6624", - "sha256:67a049bcf967a39993858beed873ed3405536019820922d4efacfe35ab3da51a", - "sha256:67ec63ae3c9c1fa2e077fcb42e77035e2121a04f987464bdf9945a28535d30ad", - "sha256:68e22c5d3be451e87d47f956b397a7823bfbde2176341bc902fba30f96831d7e", - "sha256:6ab4b6108e69f63c917cd7ef7217c5727955b1ac90600e44a13ed5312019a014", - "sha256:6bd7f18bd4cf51ea8d7e54825902cf36f9d2f35cc51ef618373988d5398b8dd0", - "sha256:6cd53e861bccc0bdc4620f68fb4a91d5bcfe9f4213cf8e200fa498044d33a6dc", - "sha256:6d346e551fa64b89d57a4ac74b9bc66703413f02f50093e089e861999ec5cccc", - "sha256:6ff8708fabc9f9bc2949f457d39b4088c9656c4c9ac15fbbbbaafce8f6d07833", - "sha256:7626e8384275a7dea6f3d1f749fb5e00299042e9c895fc3dbe24cb154909c242", - "sha256:7e7346b2b33dcd4a2171dd8a9870ae283eec8f6231dcbcf237a0f41e74751a50", - "sha256:81623c67cb71b93b5f7e06c9107f3781738ae86866db830c950223d87af2a235", - "sha256:83f1c76068faf62c32a36dd62dc4db642c2027bbbd960f8f6345b59e9d4dc472", - "sha256:8679bb1dd723ecbea03b1f96c98972815775fd8ec756c440a14f289c436c472e", - "sha256:86fb683cb9a9c0bb7476988b7957393ecdd22777d87d804442c66e62c99197f9", - "sha256:8757c62f7960cd26122f7aaaf86eda1e016fa85734c3777b8054dd334d7dea4d", - "sha256:894be7d17228e7328cc188096c0162697211ec91761f6812fff12790cbe11c66", - "sha256:8a0f240bf43c29be1bd82d77e602a61c798e9de02e5f8bb7bb414cb814f43236", - "sha256:8c3abf7eab5b76ae162c4fbb16d514a947fc57fd995b64e5ea8ef8ba3b888a69", - "sha256:93332c6972e4c91522c4810e907f3aea067424338071161b39cacded022559df", - "sha256:97d6c676dc97d593625d9fc48154f2ffeabb619a1e6fe8d2a5b53f97e3e9bdee", - "sha256:99dd85f0ca1db8d17a01a25c2bbb7784d25a2d39497c6beddbe96bff74194e04", - "sha256:9c7fb691fb07ec7ab99fd173bb0e7e0248d31bf83d484a87b917a342f63812c9", - "sha256:b3bc3cf200aab74f3d758586ac50295214eda496ac6a6636e0c881c5958d9123", - "sha256:bba54f97578943f48f621b4a7afb8eb022370da26a88b88ccc9fee9f3ef7ce45", - "sha256:bd2a13a0f8367e50347cbac87ae230ae1953935443240238f956bf10668bead6", - "sha256:cbc1184349ca6e5112898aa7fc3efa1b1bbae24ab1edc774cfd09cbfd3b091d7", - "sha256:cd82cca9c489e441574804dbda2dd8e114cf3be7935b03de11dade2c9478aea6", - "sha256:ce8ba5ed8b0a7a203922d61cff45ee6001a41a9359f04f00d055a4e988755569", - "sha256:cfee22e072a382b92ee0709dbb8203dabd52d54258051e770d9d2a81b162530b", - "sha256:d977df6f7c4109ed1d96ffb6795f6af77114be606ae4556efbfc9cac725db65d", - "sha256:da72a384a1d7e87490ca71182f3ab469ed21d847adc16b70c34faac5a3b12801", - "sha256:ddf4ad1d651e6c9234945061e1a31fe27a4be0dea21c498b87b186fadf8f5919", - "sha256:eb0ae5dfda83bbce660179d7b41c1c38fd833a54d2e6d9b258c644f3b75ef94d", - "sha256:f4c7d370badc60ac94a554bc571a46d03e39d8aacfba8006b334512e184aed59", - "sha256:f6c378b435a26fda8996579c0e324b108d2ca0d01b4661503a75634e5155559f", - "sha256:f6c9d30888503f2f5f87d6d41f016301352dd98da4a861bd10663c3a2d99d3b5", - "sha256:fab8a7877275060f7b303e1f91c218069a2814a616b6a5ee2d8a3737deb15915", - "sha256:fc32e7d7f98cac3d8d5153ed2cb583158ae3d446a6efb8e28ccb1c54a09f4169" - ], - "index": "pypi", - "version": "==23.1.0" + "sha256:004a431dfa0459123e6f4660d7e3c4ac19217d134ca38bacfffb2e78716fe944", + "sha256:057b154471e096e2dda147f7b057041acc303bb7ca4aa24c3b88c6cecdd78717", + "sha256:0e08671dc202a1880fa522f921f35ca5925ba30da8bc96228d74a8f0643ead9c", + "sha256:1b2a21f595f8cc549abd6c8de1fcd34c83441e35fb24b8a59bf161889c62a486", + "sha256:21552624ce69e69f7924f413b802b1fb554f4c0497f837810e429faa1cd4f163", + "sha256:22ac0243a41798e3eb5d5714b28c2f28e3d10792dffbc8a5fca092f975fdeceb", + "sha256:2b054525c9f7e240562185bf21671ca16d56bde92e9bd0f822c07dec7626b704", + "sha256:30c365e60c39c53f8eea042b37ea28304ffa6558fb7241cf278745095a5757da", + "sha256:3a4d87342c2737fbb9eee5c33c792db27b36b04957b4e6b7edd73a5b239a2a13", + "sha256:420b9abd1a7330687a095373b8280a20cdee04342fbc8ccb3b56d9ec8efd4e62", + "sha256:444f7d615d5f686d0ef508b9edfa8a286e6d89f449a1ba37b60ef69d869220a3", + "sha256:558f5f636e3e65f261b64925e8b190e8689e334911595394572cc7523879006d", + "sha256:5592fb4316f895922b1cacb91b04a0fa09d6f6f19bbab4442b4d0a0825177b93", + "sha256:59928dfebe93cf1e203e3cb0fd5d5dd384da56b99c8305f2e1b0a933751710f6", + "sha256:5cb642e94337b0c76c9c8cb9bfb0f8a78654575847d080d3e1504f312d691fc3", + "sha256:5d57542429df6acff02ff022067aa75b677603cee70e3abb9742787545eec966", + "sha256:5d92e7cbeab7f70b08cc0f27255b0bb2500afc30f31075bca0b1cb87735d186c", + "sha256:602835e5672ca9ca1d78e6c148fb28c4f91b748ebc41fbd2f479d8763d58bc9b", + "sha256:60746a7e8558655420a69441c0a1d47ed225ed3ac355920b96a96d0554ef7e6b", + "sha256:61b97f624da42813f74977425a3a6144d604ea21cf065616d36ea3a866d92c1c", + "sha256:693c96ae4d975eb8efa1639670e9b1fac0c3f98b7845b65c0f369141fb4bb21f", + "sha256:814e5aaf0c3be9991a59066eafb2d6e117aed6b413e3e7e9be45d4e55f5e2748", + "sha256:83005d8928f8a5cebcfb33af3bfb84b1ad65d882b899141a331cc5d07d89f093", + "sha256:831da96ba3f36cc892f0afbb4fb89b28b61b387261676e55d55a682addbd29f7", + "sha256:8355744fdbdeac5cfadfa4f38b82029b5f2b8cab7472a33453a217a7f3a9dce2", + "sha256:8496a2a5efd055c61ac2c6a18116c768a25c644b6747dcfde43e91620ab3453c", + "sha256:859059caf564f0c9398c9005278055ed3d37af4d73de6b1597821193b04ca09b", + "sha256:8c0f4d6f8c985bab83792be26ff3233940ba42e22237610ac50cbcfc10a5c235", + "sha256:8c2d8b69a2bf239ae3d987537bf3fbc2b044a405394cf4c258fc684971dd48b2", + "sha256:984b232802eddf9f0be264a4d57a10b3a1fd7319df14ee6fc7b41c6d155a3e6c", + "sha256:99cedf38eaddf263cf7e2a50e405f12c02cedf6d9df00a0d9c5d7b9417b57f76", + "sha256:a3dc339f7bc185d5fd0fd976242a5baf35de404d467e056484def8a4dd95868b", + "sha256:a51f12a8719aad9dcfb55d456022f16b90abc8dde7d3ca93ce3120b40e3fa169", + "sha256:bbabd1df23bf63ae829e81200034c0e433499275a6ed29ca1a912ea7629426d9", + "sha256:bcc6953e47bcfc9028ddf9ab2a321a3c51d7cc969db65edec092019bb837959f", + "sha256:c0a5f987d73fd9b46c3d180891f829afda714ab6bab30a1218724d4a0a63afd8", + "sha256:c223a13555444707a0a7ebc6f9ee63053147c8c082bd1a31fd1207a03e8b0500", + "sha256:c616893a577e9d6773a3836732fd7e2a729157a108b8fccd31c87512fa01671a", + "sha256:c882f1d4f96fbd807e92c334251d8ebd159a1ef89059ccd386ddea83fdb91bd8", + "sha256:c8dec8a2f3f0bb462e6439df436cd8c7ec37968e90b4209ac621e7fbc0ed3b00", + "sha256:c9638e0057e3f1a8b7c5ce33c7575349d9183a033a19b5676ad55096ae36820b", + "sha256:ce4f71e17fa849de41a06109030d3f6815fcc33338bf98dd0dde6d456d33c929", + "sha256:ced12075cdf3c7332ecc1960f77f7439d5ebb8ea20bbd3c34c8299e694f1b0a1", + "sha256:d11628212fd731b8986f1561d9bb3f8c38d9c15b330c3d8a88963519fbcd553b", + "sha256:d1610260cc672975723fcf7705c69a95f3b88802a594c9867781bedd9b13422c", + "sha256:d4651de7316ec8560afe430fb042c0782ed8ac54c0be43a515944d7c78fddac8", + "sha256:da338e2728410d74ddeb1479ec67cfba73311607037455a40f92b6f5c62bf11d", + "sha256:de727ea906033b30527b4a99498f19aca3f4d1073230a958679a5b726e2784e0", + "sha256:e2e2db5c6ef376e97c912733dfc24406f5949474d03e800d5f07b6aca4d870af", + "sha256:e669913cb2179507628419ec4f0e453e48ce6f924de5884d396f18c31836089c", + "sha256:eb4a573a8499685d62545e806d8fd143c84ac8b3439f925cd92c8763f0ed9bd7", + "sha256:f146648941cadaaaf01254a75651a23c08159d009d36c5af42a7cc200a5e53ec", + "sha256:f3ff6abde52e702397949054cb5b06c1c75b5d6542f6a2ce029e46f71ffbbbf2", + "sha256:f5aa9da520e4bb8cee8189f2f541701405e7690745094ded7a37b425d60527ea", + "sha256:f5fdb00d65ec44b10cc6b9b6318ef1363b81647a4aa3270ca39565eadb2d1201", + "sha256:f685003d836ad0e5d4f08d1e024ee3ac7816eb2f873b2266306eef858f058133", + "sha256:fee86542dc4ee8229e023003e3939b4d58cc2453922cf127778b69505fc9064b" + ], + "index": "pypi", + "version": "==23.2.0" }, "requests": { "hashes": [ - "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", - "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.28.0" + "version": "==2.28.1" }, "scons": { "hashes": [ @@ -1094,11 +1077,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863", - "sha256:778b53f0a6c83b1ee43d3b7886318ba86d975e686cb2c7906ccc35b334360be1" + "sha256:b82ad57306d5546713f15d5d70daea0408cf7f998c7566db16e0e6257e51e561", + "sha256:ddbd191b6f4e696b7845b4d87389898ae1207981faf114f968a57363aa6be03c" ], "index": "pypi", - "version": "==1.5.12" + "version": "==1.6.0" }, "setproctitle": { "hashes": [ @@ -1179,11 +1162,11 @@ }, "setuptools": { "hashes": [ - "sha256:5a844ad6e190dccc67d6d7411d119c5152ce01f7c76be4d8a1eaa314501bba77", - "sha256:bf8a748ac98b09d32c9a64a995a6b25921c96cc5743c1efa82763ba80ff54e91" + "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", + "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" ], "markers": "python_version >= '3.7'", - "version": "==62.4.0" + "version": "==63.1.0" }, "six": { "hashes": [ @@ -1227,11 +1210,11 @@ }, "tomlkit": { "hashes": [ - "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1", - "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd" + "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5", + "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==0.11.0" + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==0.11.1" }, "tqdm": { "hashes": [ @@ -1243,19 +1226,19 @@ }, "typing-extensions": { "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" ], - "markers": "python_version < '3.10'", - "version": "==4.2.0" + "markers": "python_version >= '3.7'", + "version": "==4.3.0" }, "urllib3": { "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", + "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" ], "index": "pypi", - "version": "==1.26.9" + "version": "==1.26.10" }, "utm": { "hashes": [ @@ -1266,11 +1249,11 @@ }, "websocket-client": { "hashes": [ - "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6", - "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef" + "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877", + "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1" ], "index": "pypi", - "version": "==1.3.2" + "version": "==1.3.3" }, "werkzeug": { "hashes": [ @@ -1443,11 +1426,11 @@ }, "babel": { "hashes": [ - "sha256:7aed055f0c04c9e7f51a2f75261e41e1c804efa724cb65b60a970dd4448d469d", - "sha256:81a3beca4d0cd40a9cfb9e2adb2cf39261c2f959b92e7a74750befe5d79afd7b" + "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51", + "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb" ], "markers": "python_version >= '3.6'", - "version": "==2.10.2" + "version": "==2.10.3" }, "bcrypt": { "hashes": [ @@ -1468,11 +1451,11 @@ }, "breathe": { "hashes": [ - "sha256:553aeffb00efc2cf96c4c9ed388d6ee8036ecd6d1bd9bd0c656fc25ca271bd3c", - "sha256:c4b9ff4d5298fd91518d336ede28b6a2d8cacc685d0eae17eb20e760e06bb904" + "sha256:48804dcf0e607a89fb6ad88c729ef12743a42db03ae9489be4ef8f7c4011774a", + "sha256:ac0768a5e84addad3e632028fe67749c567aba2b29088493b64c2c1634bcdba1" ], "index": "pypi", - "version": "==4.33.1" + "version": "==4.34.0" }, "carla": { "hashes": [ @@ -1490,67 +1473,81 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d", + "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412" ], "markers": "python_version >= '3.6'", - "version": "==2022.5.18.1" + "version": "==2022.6.15" }, "cffi": { "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "index": "pypi", - "version": "==1.15.0" + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "index": "pypi", + "version": "==1.15.1" }, "cfgv": { "hashes": [ @@ -1562,11 +1559,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" + "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5", + "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413" ], - "markers": "python_full_version >= '3.5.0'", - "version": "==2.0.12" + "markers": "python_version >= '3.6'", + "version": "==2.1.0" }, "control": { "hashes": [ @@ -1624,31 +1621,31 @@ }, "cryptography": { "hashes": [ - "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804", - "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178", - "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717", - "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982", - "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004", - "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe", - "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452", - "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336", - "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4", - "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15", - "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d", - "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c", - "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0", - "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06", - "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9", - "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1", - "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023", - "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de", - "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f", - "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181", - "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e", - "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a" - ], - "index": "pypi", - "version": "==37.0.2" + "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59", + "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596", + "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3", + "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5", + "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab", + "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884", + "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82", + "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b", + "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441", + "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa", + "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d", + "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b", + "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a", + "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6", + "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157", + "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280", + "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282", + "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67", + "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8", + "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046", + "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327", + "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" + ], + "index": "pypi", + "version": "==37.0.4" }, "cycler": { "hashes": [ @@ -1742,11 +1739,11 @@ }, "fonttools": { "hashes": [ - "sha256:c0fdcfa8ceebd7c1b2021240bd46ef77aa8e7408cf10434be55df52384865f8e", - "sha256:f829c579a8678fa939a1d9e9894d01941db869de44390adb49ce67055a06cc2a" + "sha256:9a1c52488045cd6c6491fd07711a380f932466e317cb8e016fc4e99dc7eac2f0", + "sha256:d73f25b283cd8033367451122aa868a23de0734757a01984e4b30b18b9050c72" ], "markers": "python_version >= '3.7'", - "version": "==4.33.3" + "version": "==4.34.4" }, "ft4222": { "hashes": [ @@ -1805,24 +1802,24 @@ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], - "markers": "python_full_version >= '3.5.0'", + "markers": "python_version >= '3.5'", "version": "==3.3" }, "imagesize": { "hashes": [ - "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", - "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" + "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", + "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.0" + "version": "==1.4.1" }, "importlib-metadata": { "hashes": [ - "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700", - "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec" + "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", + "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" ], "markers": "python_version < '3.10'", - "version": "==4.11.4" + "version": "==4.12.0" }, "iniconfig": { "hashes": [ @@ -2076,38 +2073,39 @@ }, "nodeenv": { "hashes": [ - "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b", - "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7" + "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", + "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" ], - "version": "==1.6.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.7.0" }, "numpy": { "hashes": [ - "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207", - "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887", - "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e", - "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802", - "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077", - "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af", - "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74", - "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5", - "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1", - "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0", - "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0", - "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e", - "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c", - "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c", - "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3", - "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72", - "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd", - "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6", - "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76", - "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32", - "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa", - "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba" - ], - "index": "pypi", - "version": "==1.22.4" + "sha256:092f5e6025813e64ad6d1b52b519165d08c730d099c114a9247c9bb635a2a450", + "sha256:196cd074c3f97c4121601790955f915187736f9cf458d3ee1f1b46aff2b1ade0", + "sha256:1c29b44905af288b3919803aceb6ec7fec77406d8b08aaa2e8b9e63d0fe2f160", + "sha256:2b2da66582f3a69c8ce25ed7921dcd8010d05e59ac8d89d126a299be60421171", + "sha256:5043bcd71fcc458dfb8a0fc5509bbc979da0131b9d08e3d5f50fb0bbb36f169a", + "sha256:58bfd40eb478f54ff7a5710dd61c8097e169bc36cc68333d00a9bcd8def53b38", + "sha256:79a506cacf2be3a74ead5467aee97b81fca00c9c4c8b3ba16dbab488cd99ba10", + "sha256:94b170b4fa0168cd6be4becf37cb5b127bd12a795123984385b8cd4aca9857e5", + "sha256:97a76604d9b0e79f59baeca16593c711fddb44936e40310f78bfef79ee9a835f", + "sha256:98e8e0d8d69ff4d3fa63e6c61e8cfe2d03c29b16b58dbef1f9baa175bbed7860", + "sha256:ac86f407873b952679f5f9e6c0612687e51547af0e14ddea1eedfcb22466babd", + "sha256:ae8adff4172692ce56233db04b7ce5792186f179c415c37d539c25de7298d25d", + "sha256:bd3fa4fe2e38533d5336e1272fc4e765cabbbde144309ccee8675509d5cd7b05", + "sha256:d0d2094e8f4d760500394d77b383a1b06d3663e8892cdf5df3c592f55f3bff66", + "sha256:d54b3b828d618a19779a84c3ad952e96e2c2311b16384e973e671aa5be1f6187", + "sha256:d6ca8dabe696c2785d0c8c9b0d8a9b6e5fdbe4f922bde70d57fa1a2848134f95", + "sha256:d8cc87bed09de55477dba9da370c1679bd534df9baa171dd01accbb09687dac3", + "sha256:f0f18804df7370571fb65db9b98bf1378172bd4e962482b857e612d1fec0f53e", + "sha256:f1d88ef79e0a7fa631bb2c3dda1ea46b32b1fe614e10fedd611d3d5398447f2f", + "sha256:f9c3fc2adf67762c9fe1849c859942d23f8d3e0bee7b5ed3d4a9c3eeb50a2f07", + "sha256:fc431493df245f3c627c0c05c2bd134535e7929dbe2e602b80e42bf52ff760bc", + "sha256:fe8b9683eb26d2c4d5db32cd29b38fdcf8381324ab48313b5b69088e0e355379" + ], + "index": "pypi", + "version": "==1.23.0" }, "opencv-python-headless": { "hashes": [ @@ -2130,6 +2128,33 @@ "markers": "python_version >= '3.6'", "version": "==21.3" }, + "pandas": { + "hashes": [ + "sha256:07238a58d7cbc8a004855ade7b75bbd22c0db4b0ffccc721556bab8a095515f6", + "sha256:0daf876dba6c622154b2e6741f29e87161f844e64f84801554f879d27ba63c0d", + "sha256:16ad23db55efcc93fa878f7837267973b61ea85d244fc5ff0ccbcfa5638706c5", + "sha256:1d9382f72a4f0e93909feece6fef5500e838ce1c355a581b3d8f259839f2ea76", + "sha256:24ea75f47bbd5574675dae21d51779a4948715416413b30614c1e8b480909f81", + "sha256:2893e923472a5e090c2d5e8db83e8f907364ec048572084c7d10ef93546be6d1", + "sha256:2ff7788468e75917574f080cd4681b27e1a7bf36461fe968b49a87b5a54d007c", + "sha256:41fc406e374590a3d492325b889a2686b31e7a7780bec83db2512988550dadbf", + "sha256:48350592665ea3cbcd07efc8c12ff12d89be09cd47231c7925e3b8afada9d50d", + "sha256:605d572126eb4ab2eadf5c59d5d69f0608df2bf7bcad5c5880a47a20a0699e3e", + "sha256:6dfbf16b1ea4f4d0ee11084d9c026340514d1d30270eaa82a9f1297b6c8ecbf0", + "sha256:6f803320c9da732cc79210d7e8cc5c8019aad512589c910c66529eb1b1818230", + "sha256:721a3dd2f06ef942f83a819c0f3f6a648b2830b191a72bbe9451bcd49c3bd42e", + "sha256:755679c49460bd0d2f837ab99f0a26948e68fa0718b7e42afbabd074d945bf84", + "sha256:78b00429161ccb0da252229bcda8010b445c4bf924e721265bec5a6e96a92e92", + "sha256:958a0588149190c22cdebbc0797e01972950c927a11a900fe6c2296f207b1d6f", + "sha256:a3924692160e3d847e18702bb048dc38e0e13411d2b503fecb1adf0fcf950ba4", + "sha256:d51674ed8e2551ef7773820ef5dab9322be0828629f2cbf8d1fc31a0c4fed640", + "sha256:d5ebc990bd34f4ac3c73a2724c2dcc9ee7bf1ce6cf08e87bb25c6ad33507e318", + "sha256:d6c0106415ff1a10c326c49bc5dd9ea8b9897a6ca0c8688eb9c30ddec49535ef", + "sha256:e48fbb64165cda451c06a0f9e4c7a16b534fcabd32546d531b3c240ce2844112" + ], + "index": "pypi", + "version": "==1.4.3" + }, "parameterized": { "hashes": [ "sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c", @@ -2148,47 +2173,67 @@ }, "pillow": { "hashes": [ - "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f", - "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d", - "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b", - "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c", - "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9", - "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546", - "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578", - "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1", - "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe", - "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098", - "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2", - "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a", - "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45", - "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530", - "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108", - "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1", - "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd", - "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0", - "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6", - "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c", - "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf", - "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4", - "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d", - "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765", - "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602", - "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340", - "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c", - "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b", - "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84", - "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8", - "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92", - "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54", - "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601", - "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a", - "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf", - "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251", - "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a", - "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e" - ], - "index": "pypi", - "version": "==9.1.1" + "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927", + "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14", + "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc", + "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58", + "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60", + "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76", + "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c", + "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac", + "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490", + "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1", + "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f", + "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d", + "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f", + "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069", + "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402", + "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885", + "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e", + "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be", + "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8", + "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff", + "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da", + "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004", + "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f", + "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20", + "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d", + "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c", + "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544", + "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9", + "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3", + "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04", + "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c", + "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5", + "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4", + "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb", + "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4", + "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c", + "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467", + "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e", + "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421", + "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b", + "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8", + "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb", + "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3", + "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf", + "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1", + "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a", + "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28", + "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0", + "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1", + "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8", + "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd", + "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4", + "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8", + "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f", + "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013", + "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59", + "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc", + "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4" + ], + "index": "pypi", + "version": "==9.2.0" }, "platformdirs": { "hashes": [ @@ -2426,11 +2471,11 @@ }, "requests": { "hashes": [ - "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f", - "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b" + "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", + "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" ], "index": "pypi", - "version": "==2.28.0" + "version": "==2.28.1" }, "reverse-geocoder": { "hashes": [ @@ -2468,6 +2513,14 @@ "index": "pypi", "version": "==1.8.1" }, + "setuptools": { + "hashes": [ + "sha256:16923d366ced322712c71ccb97164d07472abeecd13f3a6c283f6d5d26722793", + "sha256:db3b8e2f922b2a910a29804776c643ea609badb6a32c4bcc226fd4fd902cce65" + ], + "markers": "python_version >= '3.7'", + "version": "==63.1.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -2492,11 +2545,11 @@ }, "sphinx": { "hashes": [ - "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6", - "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226" + "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0", + "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8" ], "index": "pypi", - "version": "==4.5.0" + "version": "==5.0.2" }, "sphinx-rtd-theme": { "hashes": [ @@ -2570,6 +2623,15 @@ "index": "pypi", "version": "==3.5.4" }, + "tabulate": { + "hashes": [ + "sha256:0ba055423dbaa164b9e456abe7920c5e8ed33fcc16f6d1b2f2d152c8e1e8b4fc", + "sha256:436f1c768b424654fce8597290d2764def1eea6a77cfa5c33be00b1bc0f4f63d", + "sha256:6c57f3f3dd7ac2782770155f3adb2db0b1a269637e42f27599925e64b114f519" + ], + "index": "pypi", + "version": "==0.8.10" + }, "tenacity": { "hashes": [ "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f", @@ -2596,27 +2658,27 @@ }, "typing-extensions": { "hashes": [ - "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", - "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" + "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02", + "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6" ], - "markers": "python_version < '3.10'", - "version": "==4.2.0" + "markers": "python_version >= '3.7'", + "version": "==4.3.0" }, "urllib3": { "hashes": [ - "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", - "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" + "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec", + "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6" ], "index": "pypi", - "version": "==1.26.9" + "version": "==1.26.10" }, "virtualenv": { "hashes": [ - "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a", - "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5" + "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4", + "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.14.1" + "version": "==20.15.1" }, "zipp": { "hashes": [ diff --git a/selfdrive/debug/can_table.py b/selfdrive/debug/can_table.py index e8cd084a322574..11d070e7089d51 100755 --- a/selfdrive/debug/can_table.py +++ b/selfdrive/debug/can_table.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import argparse -import pandas as pd # pylint: disable=import-error +import pandas as pd import cereal.messaging as messaging From d8089fb94e9ad1ab54bc3baf1acbb430b305f612 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 7 Jul 2022 23:07:21 -0700 Subject: [PATCH 273/436] Add video for 2020 Lexus ES Hybrid --- selfdrive/car/toyota/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 9324e6baf5066b..8149bfd063fe9e 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -152,7 +152,7 @@ class ToyotaCarInfo(CarInfo): CAR.LEXUS_CTH: ToyotaCarInfo("Lexus CT Hybrid 2017-18", "LSS", footnotes=[Footnote.DSU]), CAR.LEXUS_ESH: ToyotaCarInfo("Lexus ES Hybrid 2017-18", "LSS", footnotes=[Footnote.DSU]), CAR.LEXUS_ES_TSS2: ToyotaCarInfo("Lexus ES 2019-21"), - CAR.LEXUS_ESH_TSS2: ToyotaCarInfo("Lexus ES Hybrid 2019-22"), + CAR.LEXUS_ESH_TSS2: ToyotaCarInfo("Lexus ES Hybrid 2019-22", video_link="https://youtu.be/BZ29osRVJeg?t=12"), CAR.LEXUS_IS: ToyotaCarInfo("Lexus IS 2017-19"), CAR.LEXUS_NX: ToyotaCarInfo("Lexus NX 2018-19", footnotes=[Footnote.DSU]), CAR.LEXUS_NXH: ToyotaCarInfo("Lexus NX Hybrid 2018-19", footnotes=[Footnote.DSU]), From ed47032a6d51d1aeaf4cd553ae9b350e04672558 Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Fri, 8 Jul 2022 16:03:57 +0900 Subject: [PATCH 274/436] Add Korean translations (#25073) * Add Korean translations Signed-off-by: crwusiz * line error fix Signed-off-by: crwusiz * space error retry Signed-off-by: crwusiz * " fix Signed-off-by: crwusiz * translation --release * main_ko.qm remove * main_ko.qm remake * Update ko and fix zh * fix Linguist warnings * commit noun Co-authored-by: Shane Smiskol --- selfdrive/ui/translations/languages.json | 3 +- selfdrive/ui/translations/main_ko.qm | Bin 0 -> 19159 bytes selfdrive/ui/translations/main_ko.ts | 1212 ++++++++++++++++++++++ 3 files changed, 1214 insertions(+), 1 deletion(-) create mode 100644 selfdrive/ui/translations/main_ko.qm create mode 100644 selfdrive/ui/translations/main_ko.ts diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index e62de24a1e83a5..b8c3a50fd70f69 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -1,4 +1,5 @@ { "English": "", - "中文(简体)": "main_zh" + "中文(简体)": "main_zh", + "한국어": "main_ko" } diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm new file mode 100644 index 0000000000000000000000000000000000000000..62ddb3be2b89c6c73e82458ae86c02825c925de5 GIT binary patch literal 19159 zcmcJ0eSB2ang2;Llb6Zlg;yyJcnKh|LP!LZB338K0D+LiWCF3qT_%~EWXxn{oVf#J zOZR7g>QAlc;ucrBOI_E(s=pmrNP;97Xp&|!i3=FOQ2P;fS(aMt+ScysuG?ze?{m(b zJ9l0Z+dtNPDio(}@%N%1VagRzVOz)6gm!;CGo zF=l<7u@Wm|o1bFrc6_g_!}I%$-Cx33W&xgUjLrNA&nNI6^COlF_v|MHB{`! ze7CS~ytaq2(lzX%!7^9b={4rz8USh*Dsu`R2 z5&P+P0>E3IGW%=G8Cz7HGUug5j4hd!vgSGLPsy2-`$v{Bwm2o_uO{$*-HDX*HUse1 zq`aH@1I7w=qf)bXXY7vuo4UcIfUXx(8}qT=lJ?Z^zmo;{ z*HTaJ1zmUiXX=@X=NQW`OuckJ@XoWOULC_e7rmW&?d4g(w=Zqphep7EmR9)8y;x6s zTHt=*Tl>AVzt07|%U7gzy-|bl`_oQ!0lxUxX~RqY3^Ml1w6~}K7xwMBv`+(v!PVOh z)(?&|HlxC@y~M&S<(5{J!rcW6m>=fPeMIvfRa>|4)q#JFp*j?K8HT&OlD=#)lVy-jbWfhe@7H zd3a(SnO0+G%WlA*Gk$Y*F690D#%KN#beQfD&-vBjdFwOcS^Avu;GaVdvQHWh{l6!$ z-d`I3A$$$|`3vLF=iqbdVdIY$TwpBkJI42}f{wyp8~^pOG3@&f#MAOy<8M9(T`Mco zQy17EPj95>{;G(vTaTx&|6l{wV@%)Jb(*pDrRfi*fbWamOaH;AzXKolruP?p5A?p8 z{+sFv$X7>3;WF@P>9&l+b05Qg-=Fah?tjJn4`=+U_7(8$v5ZgN{R;Tx%lNOKgD+`U zGJbpa&#>PMGs~6@W7{$_H>ZK_{0B1SU;GyIel_#iz+W-;)mJnB=I?=TMo#95pDx7r zhqE$r{si*#t*nIyGjUEH$g=!bKkgy$@t{|<7X zA*bZ+6NWsKc2iUA;AB}GMC3MXW&zgrOB`O?9GbBCHs^V0-YHO zb$i3g#a<@$=P@Z9JpJe;qc(I}t=#1CD3aankm|h-yD}M?=}|P{JwsQSRT6x?5=h(=Cpjc6YPvsFIak9$!0N^Hmsn z4)r!_qYS0?=JqMUFZGUHh=N#=uy3328N zmEE?!!8io@Dgv9Vw8_3NLlNOl_=(=d8Vy-iw<7x_@MtRP=MIcADMG@T@_yF;)aA9< zX+5R_fypiJl9UKNlGpC{gUODmkegTqwoMxSY;dnemZ_pfvN#+*+3%lfXns6R8iMH> zPo$dPgNC``eceA{(h=8SNw^#fGfXH}=A5cape1y*9E_d2p9#twY*N1m&B&1dg znVbwbxd>;M%grvQTduLU%3Rd*gWl21FU$i+Mhlq47PAs|o)aw*_;y8VjX<&qYc z5P=CuYP$Y==vw$bocTFDSB9=IDfF!)EHuuf?w^DkCX~>gzOz8?VO}OPH~w`p7rrUP zUOEL6JD4;y2dsW-=tS3O*9}&r5mEMlCm?kM{LmTA&9YaKAl;Hp_I3EV62amfPGXZ^ za=Il57vxv*NCCerdAzdQ>vVaPqNF5m7E_1!g&UZ3wD@Ro5GIG#&yMlu9YTTO%_{ZH z>b^Zp3bMZ7_@Usnq{J?lK&?-5$UB|QvgGtjUXK$;$^o_w9g`N{!AV}ciaxK7P(`3M zcDL-}yD@jvFfvYT8GWMn>_o%RmEg<7lB1Ph+^Vr;p~WXlonVVUpnlk8cY`q=Ot%Z` z_q4R!t1dYf`D~$$nKTJvX;Q>KuC|k7U3C6HU+|2%H#4jZHt+)zB%M3S9Jfy~hhJ8t zX1mMTB+gFq>2AB&OH*}SrM(C%Jy&Q~x^4_z>Hjz>w^mIt;Z7$hCEekl_ket`U`0?s2!myi}AY9hy7RGinI$W72u$l zssHm0go(ypwoUAa)l*>e(z8oXj_Gw@*T03zLl`mPi~4rcH@2G0a#%Um zL;MUI_`5aqPQVd`{N36JTrW62xad6hN3_#f1Wbv33FaOf7|Ks@*@E!c9B~f^-$xm<3RGG4HsdT zQ)z<>vl`9;u?fD6_HKp#> zBKVAM6sZsDzG!lbPY@B1P}8)}xC+w2sB%Rc`Cnlrn??F}gUuwk+| zBLUKUu5oa$5NalAGvrpukWae;xwOOJB%Q-d2WxB^E22MbWKM+$8(@`+inx?zsc&Q- z-(%CzoS|!D)4rIin{Gy}+$=kHk|pyrJuEjX{&jrLxS{6m-QxrO7bcuBgQr93dt7|5 zLv|_lNyc6>hxY_ahOWho&F2{jnzw~^JL#+@R>mrs72$KOwn}4JrPWePY{}ee_eOnr z^JPOg$fQ%RkEZCd7cDHpe*s+g?_YJNk6A?V!+Zft(tcP3>3jfHh z7Oo;|hT`hLpB*a9%hh}blgWuW;_40wrxd$k8hd)^O3$x*AHrJOn1Vkq^RspE*;aB4 zZg`9`J}H2WVk`nyr?$eK8(~Och1@HeI#i)^ z9ond)!!DV;K4*uFV5Q05>~q4g@jz?UIa}SN%G{n^>!dbC@%q=TTnX*v2G$}zuE_38 zWNH4`1!kh?qG!(7KxlKgQX_F@6++y=}a&u8--|1`> zYbuo$?uEx<%@-7rsK5@5=(sZ5?SYo(ggK((mV_`v@Y>NVjhUH|v=}ka6qg&D9m)BS zNK)1(F8)C#i?T@iY|o@9DCWL6(7RTcPHq0&?LLGnQ=%|WG#o8PY6dH2h1x(PrD6(` zQEkGpt$=2)V74f9WyMyJsmrR`*}C27hD8;o2Y?pVpaG_BwKNdYI4~3Zs~RW+t1i$o z*40?8JjjXz&SdR4t1DTXhSkvCv9c`*+H}Bqb#Q4N-sE6XsIsqLLzgaDDy_A`V@JH{ z^Vl7ma3ItL#lmI?*gBd$J03VQl6#G!uCFP#*tnIBhfdweOxS|x-+E{z7rra36RVKf zJ!}bbDogOj&)-pOO7BQ-`4Fo*p?_BJh_wa3uLE2OTZ56i+1-HpdK`9|XFZ^uUxKd* zRFJMC4M{lHuwos~KTr>bC=Q)A_^hf^*JBrng8v=b3MsuxjxR#a{Wu?mbj-$;z9SP2 zly%bF-}_lDlis$8{73N7fiX4QECPT2H|ZlAF@?<_bryQ$?17V?scNl3SgXsMV83jq z6yjz7%-+TzJ6cRD>pP2t;V*`+j4XovnT&FJPg14hctkc+6qCgNTkmm6N~c%8XGzHt z$***}@TtS@YjwI;kUzanDp}**efN^DtKgEf-sOygYw{>a;;xg{6m!Up9v+WRVw@z8 zo04!)bZs8Dyndz2`3Imbb2*#ar9!*IAyJN81O$cLfcl&5Zm#z4uZ011AYvC^x%d4C zPE+dfw1Wrj6c@x;R$3S*o=m0$yDZC?MdVGLaP*w*GKi7vUax+}^(#Ftu}IPz<{G<` z@(nc}Z@|m7OkpHmd)v_mAr20Y&I&>dE*%K<^bHE}jBFdbjYbO^qTvuHNq7y&_EOFx z`9x+`a=bifz^P>6!y3wIB~qtsr~D`O)#F0~jz;Iyko#eAYV| zI(JCtV37bL@%TjCdRCUoARJs%y&mE8bjTe|u!ZoQ<9A>Z(#*kv{(|s6oRxy{tJ=!) zkb#k1h$bOzvag-n?fC`*cSc5*S$Fl7sQ<@-G;64R0ys#_j4|GC!>epjliFRd%9 zttb^PLsA0rc~L?N@4*vAiP32IY+hUXfG+PZ*eofv8Ug6nT=N1}a==%rRJZJNX%op=^qbU<_O?bHGXPBW!8LGa9&O zIPCs5M9fK+oRG@VNXgQYF$u%wNA%XY%%jDZZ3uvMg%@_OhT_X=7hHV_ZN^IoMg*g( z%9*s*r-z9t?`4#UA|O$#1Db+&I;iE5l?>guv=qlct;1g)7CA@CKXeaALPTwSg`8fg z#g6dTQ6#CgdYA*krMIx3uR3E@8?`AE`9a79f5=n$Co2sA>>b)9}i z?f?a95T_flGhwqtnjMQI{`Eh8v7da9E<>L}1)5W@^}uU7)-&=tnIZ}uDS0Q@)3Zzz z8}S{LSn@_9qbR}6BuZOHyef@0S*6WKw1G3u)>hd^>_T$i&ogjTx+PO1X z04FQL2=mDgg+s~REo3hfmQ+?RdJjMY&qhM2D0W0&NJWWgJz;5L@j7{rC)f}p%WA1Q zx7OL*W|McvB&TRQHRh(n(4ihmGM`%~@|AvFTxN=f&)N`EG(4VcSmyCX7kfv}jz@Fg z8EQ;B*$C6%-U$Q7+zj<-6J^$UT9jSLoK7~#aN*^nOasrbd0J6%;PAC;edp%N4C1eU3PCZFJ3uUPX*Pecq9P^0| zo_FZE)YJd5C^5%W-K$DvMAAs)$P%0;%>_kBxg1$Q<@%%F^CL;-cKX{;cITY|B0oU_ z-t3`@p`V-~2htTzmtP1+lwy&E-vMegE)%_j()iW5T*BP4HV=A@qzLQjn5Y>2Q^cNeS~NK z^`mA-M)Aa#C!i#qX+E5h01tvGD3y>q@PJ+gm?;pJH5&5D(Cy&q(AJ!=(DLL)u{>%N zb(H6J-5BZVZR~+MBU=!ir5x1*7dBMdz}0=+EcuaP2}(UTGBBFd)lFAR389_>EUzA} zXeF^KEM0z0h#?(yDw>3MUno9)m)Ix{aezrZb3#4%<*BayB&Iymsp@r&3C zBoHyCB}0uN6Q@FwF6z@5t+ln)ygU>`ae7$kwjIAqqdCoL+mwijj)(?g*jQc7Q_Hc4 z*hYlR(NH;UV-1gxV}K0D|3X(5Fl?&Wm^fKFQsg#f)uv0U6e-_WLI9lI zM;1*OPVUilDrQt~*iczfWlaDzj6S2IDz%!|Ok<`?A6XiMD!sZYDJBzGU;%06x=tix zjisvGk}zpHB};X+*YrA6@5&O{VHjE3J*=(NV6$$mNx)?w&8)#>Y_o2vKrt`@%*3|C zFl24U;U~ggjLp z1zNh4jH$X#+J;Iz+>A*^Jr`Dln+{tO*A6RmbA&AJ5|;!&i8P^bpQurEP=ud|M6MBN zBrOcRjYmE``5LvNkyu9kD{4}Ne>EB|KT%1IxZ&r!f0CflTWxThT(oqwp6R-0QMl!K zzm|ZDdP-}N;<S6ZlY-kUegp28-Us)Sqs@GbSCmCUg*{6)DH>n`PR@#Ih8(llHd}vN2#?~gC zEixq(TC~VYbDPS7$q1lwjy5*UQqEPmsm|X<;e*ekHUO(Ky$LQzQ=~;1E{VRGbFQec za=#6SWK*Dt8lHV_^bpeA@hBk|CzfdwXH-^O%GJ}H06AI^^lFf3>f2hqjcc=b!2FT@ zXO9R`q-07k+W&&CyGH`lmq=3NkPr+dBZ(@fbqpn;@k82lnRRuWq%CqMkC@{pET$4U z<;BBF-{WA%5}w?os-jRHoL%H9Cv4JU$`KQ5q$P_{8FF}Jzk7*-CP^oVL@*#2n2g9f zf8ROL56+UzyfAj=HoG6~4pKW-IOV)4W-|I;%nd8O8Nt1jjHEo4ew`HJBaelEKxDUq zpNt@9KYobT-_3YQTxD;_KDd?wi6eU_8p*>x{b=aJXx1yd4^8{PqF*OLh0432C1Pic zfq{=L3jY+V^^I!vyO@&6*dRLDp^K)*Lx*Cbp?kRZA$Xh+s!5ya2DT%3fd{`1%7y|s z7zV{Jk<5s?1f4?4wRrJDw3*DPw|N$5)o3|8InARA#shM4GJz2 z8x=_wM3B_ZB6oOQo=%yXTrgXeb(^(TvisW!l9Pav9)_c~6^E58zId^w4o!Xzh`riL zFsWC0usD*-ET{;POyYTc1Kp|6V2zem6z@2gjc!F~_c{!8uR{aNQ15ai0W=RC8h7WNJfNRWFl$bW%Z1ocOa9Y>JWX;Z zZ?as0Eyi~F>@6+g;?7Rw75HTmdXG{Y%v?v34r{0l(VM9@U;ELc%lFr;CjFw0*X$r0 zeD>0TF63ZGz2WC6txV=vM%u64v&ReMW% z;U5e>+da&rcN#HOv=eVJrlBq^$?v789|wb)Pk8c9Mh*xiQ#6@OwuG}w2PM+H<5bk8 zn4N2k=n_c1z?Q9QD4g*hKrl`yfe_ z60yull%_a*k!x|903=6rjHg&g&r+v|%1y-6tI&~Tr{76QBnbE`qFs^-4Y-4e24fGZ-!k-96D}=MssrU4?%82;cD13*&sAFF zS^xyN1%r6X)=v3aq{tv2xYh<|5ZA=CjHaFU|4DxI?b%&W35s(kZf~M(nfrKj4k)|A z@vK6L(Ay?0T~o}3Y}q7Y#m{pXCOy&wLwK#Z5BEgcdTwB?-rNul1qv$ zqIv|`VmC#3hW1F1uBt8dQcd)lNvJY-tuHusfYep6U@&q;BzjICRM*EVFzDezA&K&t z$?l5K(cqUwoE=E!;WDBQQg<7~?rO!g5T&hSl5MzcWYMUBtsGfI#ldbyfl(~u;ttfU z@M?5S(yI`A$4W0R$|i1x`OQx+6!*Unhie+)+A5=n)+~v#T5_IW5A}3UyZF%=GrSyE z@6EBNAT&2|Z^I$PMLk0}dBRALP@!etOZy>iZzfX{Lsy0lU1+48;OD9#>c!C6rHdNT z-_*-l9Rq`OO2fRoaF_Qacz}i26OGVsw6VVt?-V)~iG3e0~mB5rT z@_NJ@(}eLN@r1Tg(^!&UR6}{p<*=iJ7MtDd3*ed=uPnk&^P|BJZM!UcBl3c~u5fax zzE~tKCfXG$L#iq8NcTT=r;y`B4m(#L0ZssXEcNP15u#wb8;w-0pbM{6H%e0M?HC{< zZHMv-S1m-g8tU<@h|mv_kS|drHmT~dAfoWdQc|~2*N4RTNGUdP>xjubq*cqe8tXP} zC;cY#VkW8qUMk26s-Ycqmeos^RrqgcURLkwY3!cloKRn5oa2I%Qi->8_pG%xt3-XtcL86WuTnUG` zzqz`1@nzf46>&v1;(Ji(TinnbcM<$(0XnkiaO@5a_kWcuqsMTi68COtFGNxvTGGl& z3F=I_*eTWYEYKF?jV^{e!(b-A!>)?K61r&Ouv4OduD6hgL1KiBKxUZMOO8h?;v#~; ztxw2PQN(XpCxlJK>6a|l(gl;Thv#7;Av<|R{92CI7og?G)e>O{agr#Zgy5E;nG06? z%#Oa@q`kG2ka{5y5%DJxn@i0Tkz(4o3mRfSMtloW@PBfwlQF4%1$Iy8BLY2 z@?e|VF%mN(z9gl)P#6QJOH7hHXr$x`9-zB~-^t>oy2%E{)J${Kfg4fRu~6nDp(=7H54059TtdgE6#eBH9*~zM;Byqqumf2b>?RKR??5{Ok=L uAC>Hd@ThGLq9tCBZcY>lrNiO1#^2qUQ-?-WekYpj4l1a0HNqdag#Q85A*7H1 literal 0 HcmV?d00001 diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts new file mode 100644 index 00000000000000..fd643224cc1c27 --- /dev/null +++ b/selfdrive/ui/translations/main_ko.ts @@ -0,0 +1,1212 @@ + + + + + AbstractAlert + + + Close + 닫기 + + + + Snooze Update + 업데이트 일시중지 + + + + Reboot and Update + 업데이트 및 재부팅 + + + + AdvancedNetworking + + + Back + 뒤로 + + + + Enable Tethering + 테더링 사용 + + + + Tethering Password + 테더링 비밀번호 + + + + + EDIT + 편집 + + + + Enter new tethering password + 새 테더링 비밀번호 입력 + + + + IP Address + IP 주소 + + + + Enable Roaming + 로밍 사용 + + + + APN Setting + APN 설정 + + + + Enter APN + APN 입력 + + + + leave blank for automatic configuration + 자동 구성을 위해 비워둠 + + + + ConfirmationDialog + + + + Ok + 확인 + + + + Cancel + 취소 + + + + DeclinePage + + + You must accept the Terms and Conditions in order to use openpilot. + 당신은 반드시 약관에 동의해야만 openpilot을 사용할 수 있습니다. + + + + Back + 뒤로 + + + + Decline, uninstall %1 + 거절,삭제 %1 + + + + DevicePanel + + + Dongle ID + Dongle ID + + + + N/A + N/A + + + + Serial + Serial + + + + Driver Camera + 운전자 카메라 + + + + PREVIEW + 미리보기 + + + + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) + 운전자 카메라를 미리 보면서 최적의 운전자 모니터링 경험을 위해 기기 장착 위치를 최적화할수 있습니다. (차량은 반드시 닫아야 합니다) + + + + Reset Calibration + 캘리브레이션 재설정 + + + + RESET + 재설정 + + + + Are you sure you want to reset calibration? + 캘리브레이션을 재설정하시겠습니까? + + + + Review Training Guide + 트레이닝 가이드 다시보기 + + + + REVIEW + 다시보기 + + + + Review the rules, features, and limitations of openpilot + openpilot의 규칙, 기능, 제한 다시보기 + + + + Are you sure you want to review the training guide? + 트레이닝 가이드를 다시보시겠습니까? + + + + Regulatory + 규제 + + + + VIEW + 보기 + + + Change Language + 언어변경 + + + CHANGE + 변경 + + + Select a language + 언어선택 + + + + Reboot + 재부팅 + + + + Power Off + 전원 종료 + + + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot은 장치를 왼쪽 또는 오른쪽 4° 이내, 위쪽 5° 또는 아래쪽 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. + + + + Your device is pointed %1° %2 and %3° %4. + 사용자의 기기가 %1° %2 및 %3° %4를 가리키고 있습니다. + + + + down + 아래 + + + + up + + + + + left + 왼쪽 + + + + right + 오른쪽 + + + + Are you sure you want to reboot? + 재부팅 하시겠습니까? + + + + Disengage to Reboot + 재부팅 하려면 해제하세요 + + + + Are you sure you want to power off? + 전원을 종료하시겠습니까? + + + + Disengage to Power Off + 전원을 종료하려면 해제하세요 + + + + DriveStats + + + Drives + 주행수 + + + + Hours + 시간 + + + + ALL TIME + 전체 시간 + + + + PAST WEEK + 지난주 + + + + KM + Km + + + + Miles + Miles + + + + DriverViewScene + + + camera starting + 카메라 시작중 + + + + InputDialog + + + Cancel + 취소 + + + + Need at least + 최소 필요 + + + + characters! + 문자! + + + + Installer + + + Installing... + 설치중... + + + + Receiving objects: + 수신중: + + + + Resolving deltas: + 델타병합: + + + + Updating files: + 파일갱신: + + + + MapPanel + + + Current Destination + 현재 목적지 + + + + CLEAR + CLEAR + + + + Recent Destinations + 최근 목적지 + + + + Try the Navigation Beta + 네비게이션(베타)을 사용해보세요 + + + + Get turn-by-turn directions displayed and more with a comma +prime subscription. Sign up now: https://connect.comma.ai + 자세한 경로안내를 확인하시려면 comma prime을 구독하세요. +즉시등록:https://connect.comma.ai + + + + No home +location set + 집 +설정되지않음 + + + + No work +location set + 회사 +설정되지않음 + + + + no recent destinations + 최근 경로 없음 + + + + MultiOptionDialog + + Select + 선택 + + + Cancel + 취소 + + + + Networking + + + Advanced + 고급 + + + + Enter password + 비밀번호를 입력하세요 + + + + + for " + 하기위한 " + + + + Wrong password + 비밀번호가 틀렸습니다 + + + + NvgWindow + + + km/h + km/h + + + + mph + mph + + + + + MAX + MAX + + + + + SPEED + SPEED + + + + + LIMIT + LIMIT + + + + OffroadHome + + + UPDATE + 업데이트 + + + + ALERTS + 알림 + + + + ALERT + 알림 + + + + PairingPopup + + + Pair your device to your comma account + 장치를 콤마 계정과 페어링합니다 + + + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> + <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> + <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> + </ol> + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>https://connect.comma.ai에 접속하세요</li> + <li style='margin-bottom: 50px;'>"새 장치 추가"를 클릭하고 오른쪽 QR 코드를 검색합니다.</li> + <li style='margin-bottom: 50px;'>connect.comma.ai을 앱처럼 사용하려면 홈 화면에 바로가기를 만드십시오.</li> + </ol> + + + + + PrimeAdWidget + + + Upgrade Now + 지금 업그레이드 + + + + Become a comma prime member at connect.comma.ai + connect.comma.ai에서 comma prime에 가입합니다 + + + + PRIME FEATURES: + PRIME 기능: + + + + Remote access + 원격 접속 + + + + 1 year of storage + 1년간 저장 + + + + Developer perks + 개발자 혜택 + + + + PrimeUserWidget + + + ✓ SUBSCRIBED + ✓ 구독함 + + + + comma prime + comma prime + + + + CONNECT.COMMA.AI + CONNECT.COMMA.AI + + + + COMMA POINTS + COMMA POINTS + + + + QObject + + + Reboot + 재부팅 + + + + Exit + 종료 + + + + dashcam + dashcam + + + + openpilot + openpilot + + + + %1 minute%2 ago + %1 분%2 전 + + + + %1 hour%2 ago + %1 시간%2 전 + + + + %1 day%2 ago + %1 일%2 전 + + + + Reset + + + Reset failed. Reboot to try again. + 초기화 실패. 재부팅후 다시 시도하세요. + + + + Are you sure you want to reset your device? + 장치를 초기화 하시겠습니까? + + + + Resetting device... + 장치 초기화중... + + + + System Reset + 장치 초기화 + + + + System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + 장치를 초기화 합니다. 확인버튼을 누르면 모든 내용과 설정이 초기화됩니다. 취소를 누르면 다시 부팅합니다. + + + + Cancel + 취소 + + + + Reboot + 재부팅 + + + + Confirm + 확인 + + + + Unable to mount data partition. Press confirm to reset your device. + 데이터 파티션을 마운트할 수 없습니다. 확인 버튼을 눌러 장치를 리셋합니다. + + + + RichTextDialog + + + Ok + 확인 + + + + SettingsWindow + + + × + × + + + + Device + 장치 + + + + + Network + 네트워크 + + + + Toggles + 토글 + + + + Software + 소프트웨어 + + + + Navigation + 네비게이션 + + + + Setup + + + WARNING: Low Voltage + 경고: 전압이 낮습니다 + + + + Power your device in a car with a harness or proceed at your own risk. + 하네스 보드에 차량의 전원을 연결하세요. + + + + Power off + 전원 종료 + + + + + + Continue + 계속 + + + + Getting Started + 시작 + + + + Before we get on the road, let’s finish installation and cover some details. + 출발하기 전에 설치를 완료하고 몇 가지 세부 사항을 살펴보겠습니다. + + + + Connect to Wi-Fi + wifi 연결 + + + + + Back + 뒤로 + + + + Continue without Wi-Fi + wifi 없이 계속 + + + + Waiting for internet + 네트워크 접속을 기다립니다 + + + + Choose Software to Install + 설치할 소프트웨어를 선택하세요 + + + + Dashcam + Dashcam + + + + Custom Software + Custom Software + + + + Enter URL + URL 입력 + + + + for Custom Software + for Custom Software + + + + Downloading... + 다운로드중... + + + + Download Failed + 다운로드 실패 + + + + Ensure the entered URL is valid, and the device’s internet connection is good. + 입력된 URL이 유효하고 장치의 인터넷 연결이 잘 되어 있는지 확인합니다. + + + + Reboot device + 재부팅 + + + + Start over + 다시 시작 + + + + SetupWidget + + + Finish Setup + 설치완료 + + + + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. + 장치를 (connect.comma.ai)에서 페어링하고 comma prime 오퍼를 청구합니다. + + + + Pair device + 페어링 + + + + Sidebar + + + + CONNECT + 연결 + + + + OFFLINE + 오프라인 + + + + + ONLINE + 온라인 + + + + ERROR + 오류 + + + + + + TEMP + 온도 + + + + HIGH + 높음 + + + + GOOD + 경고 + + + + OK + 좋음 + + + + VEHICLE + 차량 + + + + NO + NO + + + + PANDA + PANDA + + + + GPS + GPS + + + + SEARCH + 검색중 + + + + -- + -- + + + + Wi-Fi + Wi-Fi + + + + ETH + 이더넷 + + + + 2G + 2G + + + + 3G + 3G + + + + LTE + LTE + + + + 5G + 5G + + + + SoftwarePanel + + + Git Branch + Git 브렌치 + + + + Git Commit + Git 커밋 + + + + OS Version + OS 버전 + + + + Version + 버전 + + + + Last Update Check + 최신 업데이트 검사 + + + + The last time openpilot successfully checked for an update. The updater only runs while the car is off. + 이전에 openpilot에서 업데이트를 성공적으로 확인한 시간입니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. + + + + Check for Update + 업데이트 확인 + + + + CHECKING + 검사중 + + + + Uninstall + 삭제 + + + + UNINSTALL + 삭제 + + + + Are you sure you want to uninstall? + 삭제하시겠습니까? + + + + failed to fetch update + 업데이트를 가져올수없습니다 + + + + + CHECK + 확인 + + + + SshControl + + + SSH Keys + SSH 키 + + + + Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. + 경고:이렇게 하면 GitHub 설정의 모든 공용 키에 대한 SSH 액세스 권한이 부여됩니다. 자신의 사용자 이름이 아닌 GitHub 사용자 이름을 입력하지 마십시오. comma 직원은 GitHub 사용자 이름을 추가하도록 요청하지 않습니다. + + + + + ADD + 추가 + + + + Enter your GitHub username + GitHub 사용자 ID + + + + LOADING + 로딩 + + + + REMOVE + 제거 + + + + Username '%1' has no keys on GitHub + 사용자 이름 '%1' GitHub에 키가 없습니다 + + + + Request timed out + 요청 시간 초과 + + + + Username '%1' doesn't exist on GitHub + 사용자 이름 '%1' GitHub에 없습니다 + + + + SshToggle + + + Enable SSH + SSH 사용 + + + + TermsPage + + + Terms & Conditions + 약관 + + + + Decline + 거절 + + + + Scroll to accept + 스크롤 허용 + + + + Agree + 동의 + + + + TogglesPanel + + + Enable openpilot + openpilot 사용 + + + + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 어댑티브 크루즈 컨트롤 및 차선 유지 운전자 보조를 위해 openpilot 시스템을 사용하십시오. 이 기능을 사용하려면 항상 주의를 기울여야 합니다. 이 설정을 변경하면 차량 전원이 꺼질 때 적용됩니다. + + + + Enable Lane Departure Warnings + 차선 이탈 경고 사용 + + + + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). + 차량이 50km/h(31mph) 이상의 속도로 주행하는 동안 방향 지시등이 활성화되지 않은 상태에서 감지된 차선 위를 주행할 경우 차선이탈 경고를 사용합니다. + + + + Enable Right-Hand Drive + 우측핸들 사용 + + + + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. + openpilot이 좌측 교통 규칙을 준수하고 우측 운전석에서 운전자 모니터링을 수행합니다. + + + + Use Metric System + 미터법 사용 + + + + Display speed in km/h instead of mph. + mph가 아닌 km/h로 속도 표시. + + + + Record and Upload Driver Camera + 운전자 카메라 기록 및 업로드 + + + + Upload data from the driver facing camera and help improve the driver monitoring algorithm. + 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. + + + + Disengage On Accelerator Pedal + 가속페달 조작시 해제 + + + + When enabled, pressing the accelerator pedal will disengage openpilot. + 활성화된 경우 가속 페달을 누르면 openpilot이 해제됩니다. + + + + Show ETA in 24h format + 24시간 형식으로 ETA 표시 + + + + Use 24h format instead of am/pm + 오전/오후 대신 24시간 형식 사용 + + + + openpilot Longitudinal Control + openpilot Longitudinal Control + + + + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + openpilot은 차량'의 레이더를 무력화시키고 가속페달과 브레이크의 제어를 인계받을 것이다. 경고: AEB를 비활성화합니다! + + + + Updater + + + Update Required + 업데이트 필요 + + + + An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. + OS 업데이트가 필요합니다. 장치를 wifi에 연결하여 가장 빠른 업데이트 경험을 제공합니다. 다운로드 크기는 약 1GB입니다. + + + + Connect to Wi-Fi + wifi 연결 + + + + Install + 설치 + + + + Back + 뒤로 + + + + Loading... + 로딩중... + + + + Reboot + 재부팅 + + + + Update failed + 업데이트 실패 + + + + WifiUI + + + + Scanning for networks... + 네트워크 검색 중... + + + + CONNECTING... + 연결중... + + + + FORGET + 저장안함 + + + + Forget Wi-Fi Network " + wifi 네트워크 저장안함" + + + From c9fa5ef11a6d47a886b30f65b2064b2d6ae05807 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 00:23:46 -0700 Subject: [PATCH 275/436] AGNOS 5.2 (#25011) * AGNOS 5.2 * casync manifest --- launch_env.sh | 2 +- system/hardware/tici/agnos.json | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 769613bc79b105..ac84d6dcbde6f4 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="5.1" + export AGNOS_VERSION="5.2" fi if [ -z "$PASSIVE" ]; then diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 004a0f9dfbb730..853d3ab434a07b 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,9 +1,9 @@ [ { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-bb71f49294150c4233b89e2a10768a5a3de003203ecd02e3f845821b35cd409f.img.xz", - "hash": "bb71f49294150c4233b89e2a10768a5a3de003203ecd02e3f845821b35cd409f", - "hash_raw": "bb71f49294150c4233b89e2a10768a5a3de003203ecd02e3f845821b35cd409f", + "url": "https://commadist.azureedge.net/agnosupdate/boot-243ddbb9e2256aa7af7fed0daf8cff4017a3c838c759373a634b8539f271bfb8.img.xz", + "hash": "243ddbb9e2256aa7af7fed0daf8cff4017a3c838c759373a634b8539f271bfb8", + "hash_raw": "243ddbb9e2256aa7af7fed0daf8cff4017a3c838c759373a634b8539f271bfb8", "size": 14780416, "sparse": false, "full_check": true, @@ -41,12 +41,14 @@ }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-11fdbc9e8a9cd27f98346d7e1039bc5b3032d0e892ff95fa1258673ff1809bca.img.xz", - "hash": "45b4719a9e580617cf840036b24fb0dcd32491edd9654d8d74c28d91ff362d36", - "hash_raw": "11fdbc9e8a9cd27f98346d7e1039bc5b3032d0e892ff95fa1258673ff1809bca", + "url": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13.img.xz", + "hash": "44da205d17b44b2be7c94854a6bb3efb2928ec9a9889fe62af8b322d2295b74f", + "hash_raw": "59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13", "size": 10737418240, "sparse": true, "full_check": false, - "has_ab": true + "has_ab": true, + "casync_caibx": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13.caibx", + "casync_store": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13" } ] From 5b4e39990ac4d098f874c8f1e378e2ab855c0036 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 8 Jul 2022 00:41:17 -0700 Subject: [PATCH 276/436] Add Japanese translations (#25078) * Add Japanese translations * test japanese * update tr file * test for unfinished translation tags * add compiled QM add compiled QM * mark as finished * remove from tests, needs some design decisions Co-authored-by: PONPC --- selfdrive/ui/tests/test_translations.py | 10 + selfdrive/ui/translations/main_ja.qm | Bin 0 -> 19159 bytes selfdrive/ui/translations/main_ja.ts | 1207 +++++++++++++++++++++++ 3 files changed, 1217 insertions(+) create mode 100644 selfdrive/ui/translations/main_ja.qm create mode 100644 selfdrive/ui/translations/main_ja.ts diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 2dedf3d78561f2..3230a995437f1b 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -61,6 +61,16 @@ def test_translations_updated(self): self.assertEqual(cur_translations, new_translations, f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py --release to update the translation files") + def test_unfinished_translations(self): + for name, file in self.translation_files.items(): + with self.subTest(name=name, file=file): + if not len(file): + raise self.skipTest(f"{name} translation has no defined file") + + cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file, "ts") + self.assertTrue(b"" not in cur_translations, + f"{file} ({name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist") + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/ui/translations/main_ja.qm b/selfdrive/ui/translations/main_ja.qm new file mode 100644 index 0000000000000000000000000000000000000000..552545cf879ed5c9a4009e63d334dee10c479e33 GIT binary patch literal 19159 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 HImG`LJd!}I literal 0 HcmV?d00001 diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts new file mode 100644 index 00000000000000..fbb654bc4cbb09 --- /dev/null +++ b/selfdrive/ui/translations/main_ja.ts @@ -0,0 +1,1207 @@ + + + + + AbstractAlert + + + Close + 閉じる + + + + Snooze Update + 更新停止 + + + + Reboot and Update + 再起動してアップデート + + + + AdvancedNetworking + + + Back + もどる + + + + Enable Tethering + テザリングを有効化 + + + + Tethering Password + テザリングパスワード + + + + + EDIT + 編集 + + + + Enter new tethering password + 新しいテザリングパスワードを入力 + + + + IP Address + IPアドレス + + + + Enable Roaming + ローミングを有効化 + + + + APN Setting + APN 設定 + + + + Enter APN + APN 入力 + + + + leave blank for automatic configuration + 空欄で自動設定 + + + + ConfirmationDialog + + + + Ok + OK + + + + Cancel + キャンセル + + + + DeclinePage + + + You must accept the Terms and Conditions in order to use openpilot. + openpilotを利用するためには、利用規約に同意する必要があります。 + + + + Back + 戻る + + + + Decline, uninstall %1 + 拒否してアンインストール %1 + + + + DevicePanel + + + Dongle ID + ドングル ID + + + + N/A + N/A + + + + Serial + シリアル + + + + Driver Camera + ドライバーカメラ + + + + PREVIEW + プレビュー + + + + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) + ドライバーカメラのプレビューにより、デバイスの取り付け位置を最適化し、最高のドライバーモニタリング体験を提供します。(車両の電源を切る必要があります) + + + + Reset Calibration + キャリブレーションリセット + + + + RESET + リセット + + + + Are you sure you want to reset calibration? + 本当にキャリブレーションをリセットしますか? + + + + Review Training Guide + トレーニングガイドを見る + + + + REVIEW + レビュー + + + + Review the rules, features, and limitations of openpilot + openpilot 規約 機能 制約を見る + + + + Are you sure you want to review the training guide? + 本当にトレーニングガイドを見ますか? + + + + Regulatory + レギュレーション + + + + VIEW + ビュー + + + Change Language + 言語を変更 + + + CHANGE + 変更 + + + Select a language + 言語を選択する + + + + Reboot + 再起動 + + + + Power Off + 電源を切る + + + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot は、左右に4°、上に5°、下に8°の範囲に設置する必要があります。openpilot は継続的に校正されているので、手動でリセットする必要はほとんどありません。 + + + + Your device is pointed %1° %2 and %3° %4. + デバイスは %1° %2 と %3° %4を示しています。 + + + + down + + + + + up + + + + + left + + + + + right + + + + + Are you sure you want to reboot? + 本当に再起動しますか? + + + + Disengage to Reboot + 再起動するために離脱します + + + + Are you sure you want to power off? + 本当に電源を切っても良いですか? + + + + Disengage to Power Off + 電源を切るために離脱します + + + + DriveStats + + + Drives + ドライブ + + + + Hours + 時間 + + + + ALL TIME + 累計 + + + + PAST WEEK + 先週 + + + + KM + km + + + + Miles + マイル + + + + DriverViewScene + + + camera starting + カメラ起動 + + + + InputDialog + + + Cancel + キャンセル + + + + Need at least + 最低限必要なもの + + + + characters! + 記号! + + + + Installer + + + Installing... + インストール... + + + + Receiving objects: + オブジェクトを受信中: + + + + Resolving deltas: + リゾルブ解決中: + + + + Updating files: + ファイルを更新中: + + + + MapPanel + + + Current Destination + 現在の目的地 + + + + CLEAR + クリア + + + + Recent Destinations + 最近の目的地 + + + + Try the Navigation Beta + ベータ版ナビゲーション + + + + Get turn-by-turn directions displayed and more with a comma +prime subscription. Sign up now: https://connect.comma.ai + より詳細な案内や表示に関する情報を得ることができます。 詳しくはこちら:https://connect.comma.ai + + + + No home +location set + 自宅の登録なし +位置を設定 + + + + No work +location set + 職場の登録なし +位置を設定 + + + + no recent destinations + 最寄りの目的地がありません + + + + MultiOptionDialog + + Select + 選択 + + + + Networking + + + Advanced + 詳細 + + + + Enter password + パスワードを入力 + + + + + for " + のため " + + + + Wrong password + パスワードが間違っています + + + + NvgWindow + + + km/h + km/時 + + + + mph + マイル/時 + + + + + MAX + 最大 + + + + + SPEED + 速度 + + + + + LIMIT + 制限 + + + + OffroadHome + + + UPDATE + 更新 + + + + ALERTS + 警告 + + + + ALERT + 警告 + + + + PairingPopup + + + Pair your device to your comma account + デバイスとアカウントを連携する + + + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> + <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> + <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> + </ol> + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>モバイルでアクセス https://connect.comma.ai</li> + <li style='margin-bottom: 50px;'>“新しいデバイスを追加”をクリックし,QRコードを読み込みます</li> + <li style='margin-bottom: 50px;'>connect.comma.aiをホーム画面にブックマークして、アプリのように使うことができます</li> + </ol> + + + + + PrimeAdWidget + + + Upgrade Now + いますぐアップグレード + + + + Become a comma prime member at connect.comma.ai + connect.comma.ai のプライムメンバーになる + + + + PRIME FEATURES: + 主な機能: + + + + Remote access + リモートアクセス + + + + 1 year of storage + 1年の保存期間 + + + + Developer perks + 開発者特典 + + + + PrimeUserWidget + + + ✓ SUBSCRIBED + ✓ 購読 + + + + comma prime + コンマプライム + + + + CONNECT.COMMA.AI + CONNECT.COMMA.AI + + + + COMMA POINTS + コンマポイント + + + + QObject + + + Reboot + 再起動 + + + + Exit + 退出 + + + + dashcam + ダッシュカム + + + + openpilot + オープンパイロット + + + + %1 minute%2 ago + %1 分%2 前 + + + + %1 hour%2 ago + %1 時間%2 前 + + + + %1 day%2 ago + %1 日%2 前 + + + + Reset + + + Reset failed. Reboot to try again. + 初期化に失敗しました。再起動後に再試行してください。 + + + + Are you sure you want to reset your device? + 本当に初期化しますか? + + + + Resetting device... + デバイスが初期化されます... + + + + System Reset + システムを初期化 + + + + System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + システムを初期化させます。 すべてのコンテンツと設定が削除されます。 キャンセルを押すと再起動します。 + + + + Cancel + キャンセル + + + + Reboot + 再起動 + + + + Confirm + 確認 + + + + Unable to mount data partition. Press confirm to reset your device. + dataパーティションをマウントできません。 確認を押すとデバイスが初期化されます。 + + + + RichTextDialog + + + Ok + OK + + + + SettingsWindow + + + × + × + + + + Device + デバイス + + + + + Network + ネットワーク + + + + Toggles + 切り替え + + + + Software + ソフトウェア + + + + Navigation + ナビゲーション + + + + Setup + + + WARNING: Low Voltage + 警告:低電圧 + + + + Power your device in a car with a harness or proceed at your own risk. + 自己責任でハーネスから電源を供給してください。 + + + + Power off + 電源を切る + + + + + + Continue + 続ける + + + + Getting Started + はじめに + + + + Before we get on the road, let’s finish installation and cover some details. + その前に、インストールを完了し、いくつかの詳細を説明します。 + + + + Connect to Wi-Fi + Wi-Fiに接続 + + + + + Back + 戻る + + + + Continue without Wi-Fi + Wi-Fi に未接続で続行 + + + + Waiting for internet + インターネット接続を待機中 + + + + Choose Software to Install + インストールするソフトウェアを選びます + + + + Dashcam + ダッシュカム + + + + Custom Software + カスタムソフトウェア + + + + Enter URL + URLを入力 + + + + for Custom Software + カスタムソフトウェア + + + + Downloading... + ダウンロード中... + + + + Download Failed + ダウンロード失敗 + + + + Ensure the entered URL is valid, and the device’s internet connection is good. + 入力されたURLが有効であること、デバイスがインターネットに接続されていることを確認してください。 + + + + Reboot device + デバイスを再起動 + + + + Start over + 再スタート + + + + SetupWidget + + + Finish Setup + セットアップ完了 + + + + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. + デバイスを comma connect (connect.comma.ai)でペアリングし comma prime 特典を申請してください。 + + + + Pair device + デバイスをペアリング + + + + Sidebar + + + + CONNECT + 接続 + + + + OFFLINE + オフライン + + + + + ONLINE + オンライン + + + + ERROR + エラー + + + + + + TEMP + 温度 + + + + HIGH + 高温 + + + + GOOD + 最適 + + + + OK + OK + + + + VEHICLE + 車両 + + + + NO + NO + + + + PANDA + パンダ + + + + GPS + GPS + + + + SEARCH + 検索 + + + + -- + -- + + + + Wi-Fi + Wi-Fi + + + + ETH + ETH + + + + 2G + 2G + + + + 3G + 3G + + + + LTE + LTE + + + + 5G + 5G + + + + SoftwarePanel + + + Git Branch + Git ブランチ + + + + Git Commit + Git コミット + + + + OS Version + OS バージョン + + + + Version + バージョン + + + + Last Update Check + 最終更新確認 + + + + The last time openpilot successfully checked for an update. The updater only runs while the car is off. + openpilotが最後にアップデートの確認に成功してからの時間です。アップデート処理は、車の電源が切れているときのみ実行されます。 + + + + Check for Update + 更新確認 + + + + CHECKING + 確認中 + + + + Uninstall + アンインストール + + + + UNINSTALL + アンインストール + + + + Are you sure you want to uninstall? + 本当にアンインストールしますか? + + + + failed to fetch update + 更新の取得に失敗しました + + + + + CHECK + 確認 + + + + SshControl + + + SSH Keys + SSH 鍵 + + + + Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. + 警告: これは、GitHub の設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外のGitHubのユーザー名を入力しないでください。コンマのスタッフがGitHubのユーザー名を追加するようお願いすることはありません。 + + + + + ADD + 追加 + + + + Enter your GitHub username + GitHubのユーザー名を入力してください + + + + LOADING + ローディング + + + + REMOVE + 削除 + + + + Username '%1' has no keys on GitHub + ユーザー名“%1”は GitHub に鍵がありません + + + + Request timed out + リクエストタイムアウト + + + + Username '%1' doesn't exist on GitHub + ユーザー名 '%1' は GitHub に存在しません + + + + SshToggle + + + Enable SSH + SSH を有効化 + + + + TermsPage + + + Terms & Conditions + 利用規約 + + + + Decline + 拒否 + + + + Scroll to accept + スクロールして同意 + + + + Agree + 同意 + + + + TogglesPanel + + + Enable openpilot + openpilot を有効化 + + + + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + アダプティブクルーズコントロールとレーンキーピングドライバーアシスト(openpilotシステム)。この機能を使用するには、常に注意が必要です。この設定を変更すると、車の電源が切れたときに有効になります。 + + + + Enable Lane Departure Warnings + 車線逸脱警報機能を有効化 + + + + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). + 時速31マイル(50km)を超えるスピードで走行中、方向指示器を作動させずに検出された車線ライン上に車両が触れた場合、車線に戻るアラートを受信します。 + + + + Enable Right-Hand Drive + 右ハンドルを有効化 + + + + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. + openpilotが左側通行規則を遵守し、右側の運転席でドライバーの監視を行うことを可能にします。 + + + + Use Metric System + メートル法を有効化 + + + + Display speed in km/h instead of mph. + 速度はmphではなくkm/hで表示されます。 + + + + Record and Upload Driver Camera + ドライバーカメラの録画とアップロード + + + + Upload data from the driver facing camera and help improve the driver monitoring algorithm. + ドライバーカメラからのデータをアップロードし、ドライバー監視のアルゴリズム向上に役立てます。 + + + + Disengage On Accelerator Pedal + アクセルペダルで解除する + + + + When enabled, pressing the accelerator pedal will disengage openpilot. + 有効な場合 openpilot はアクセルペダルを踏むと解除されます。 + + + + Show ETA in 24h format + 24時間表示 + + + + Use 24h format instead of am/pm + AM/PMの代わりに24時間形式を使用します + + + + openpilot Longitudinal Control + openpilot による垂直方向の制御 + + + + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + openpilotは、車のレーダーを無効化し、アクセルとブレーキの制御を引き継ぎます。注意:AEBを無効にします! + + + + Updater + + + Update Required + 更新が必要です + + + + An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. + OSのアップデートが必要です。Wi-Fiに接続することで、最速のアップデートを体験できます。ダウンロードサイズは約1GBです。 + + + + Connect to Wi-Fi + Wi-Fiに接続 + + + + Install + インストール + + + + Back + 戻る + + + + Loading... + 読み込み中... + + + + Reboot + 再起動 + + + + Update failed + 更新失敗 + + + + WifiUI + + + + Scanning for networks... + ネットワークをスキャン中... + + + + CONNECTING... + 接続中... + + + + FORGET + 削除 + + + + Forget Wi-Fi Network " + Wi-Fiを削除する” + + + From dccb184fbd4a2b39008471f94056f61bf7a7381d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 13:39:23 +0200 Subject: [PATCH 277/436] some more release notes --- RELEASES.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index fedff5dbff7b99..d7f4a37476a3af 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,17 +5,23 @@ Version 0.8.15 (2022-07-XX) * Effective feedforward that uses road roll * Simplified tuning, all car-specific parameters can be derived from data * Significantly improved control on TSS-P Prius +* New driving model + * Path planning uses end-to-end output instead of lane lines * New driver monitoring model - * takes a larger input frame - * outputs a driver state for both driver and passenger - * automatically determines which side the driver is on (soon) -* Reduced power usage: device runs cooler and fan spins less -* Minor UI updates + * Takes a larger input frame + * Outputs a driver state for both driver and passenger + * Automatically determines which side the driver is on (soon) +* Navigation improvements + * Speed limits shown while navigating + * Faster position fix by using raw GPS measurements +* UI updates * New font * Refreshed max speed design - * Speed limits shown while navigating * More consistent camera view perspective across cars +* Reduced power usage: device runs cooler and fan spins less * AGNOS 5 + * Support for delta updates to reduce data usage on future OS updates + * Support VSCode remote SSH target * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! From b5399fbd3ce3a03bb900f1e8597f0afbd19fa314 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 13:41:42 +0200 Subject: [PATCH 278/436] add multilang to release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index d7f4a37476a3af..80709e1482737c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Version 0.8.15 (2022-07-XX) * Speed limits shown while navigating * Faster position fix by using raw GPS measurements * UI updates + * Multilanguage support for settings and home screen * New font * Refreshed max speed design * More consistent camera view perspective across cars From c5e96201f30c7168e531ba5da98775fcd2d668c5 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 13:56:49 +0200 Subject: [PATCH 279/436] RELEASES.md: new driving model goes first --- RELEASES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 80709e1482737c..e0e75f3213ecb9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,12 +1,12 @@ Version 0.8.15 (2022-07-XX) ======================== +* New driving model + * Path planning uses end-to-end output instead of lane lines * New lateral controller based on physical wheel torque model * Much smoother control, consistent across the speed range * Effective feedforward that uses road roll * Simplified tuning, all car-specific parameters can be derived from data * Significantly improved control on TSS-P Prius -* New driving model - * Path planning uses end-to-end output instead of lane lines * New driver monitoring model * Takes a larger input frame * Outputs a driver state for both driver and passenger From b6df0cd2422d63eb58cc6abaa9968954feff2ccb Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 15:44:16 +0200 Subject: [PATCH 280/436] casync: handle hash failure (#25081) * casync: handle hashing failure due to IO errors * fix comment * all exceptions * fix typo * Update system/hardware/tici/agnos.py --- system/hardware/tici/agnos.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index bd8ce2bf025c45..750aa630aea249 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -186,14 +186,18 @@ def extract_casync_image(target_slot_number: int, partition: dict, cloudlog): sources: List[Tuple[str, casync.ChunkReader, casync.ChunkDict]] = [] - # First source is the current partition. Index file for current version is provided in the manifest - raw_hash = get_raw_hash(seed_path, partition['size']) - caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" + # First source is the current partition. try: - cloudlog.info(f"casync fetching {caibx_url}") - sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))] - except requests.RequestException: - cloudlog.error(f"casync failed to load {caibx_url}") + raw_hash = get_raw_hash(seed_path, partition['size']) + caibx_url = f"{CAIBX_URL}{partition['name']}-{raw_hash}.caibx" + + try: + cloudlog.info(f"casync fetching {caibx_url}") + sources += [('seed', casync.FileChunkReader(seed_path), casync.build_chunk_dict(casync.parse_caibx(caibx_url)))] + except requests.RequestException: + cloudlog.error(f"casync failed to load {caibx_url}") + except Exception: + cloudlog.exception("casync failed to hash seed partition") # Second source is the target partition, this allows for resuming sources += [('target', casync.FileChunkReader(path), casync.build_chunk_dict(target))] From 7a4c33795a12eefff4c3e5c311f40eae2cf3506b Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 18:20:16 +0200 Subject: [PATCH 281/436] laikad: add residual threshold for pos_fix (#25082) * laikad: add residual threshold for pos_fix * update ref * update test --- selfdrive/locationd/laikad.py | 5 +++-- selfdrive/locationd/test/test_laikad.py | 8 ++++---- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index e262407e024ad8..67867e82b3580d 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -28,6 +28,7 @@ MAX_TIME_GAP = 10 EPHEMERIS_CACHE = 'LaikadEphemeris' CACHE_VERSION = 0.1 +POS_FIX_RESIDUAL_THRESHOLD = 100.0 class Laikad: @@ -89,9 +90,9 @@ def cache_ephemeris(self, t: GPSTime): def get_est_pos(self, t, processed_measurements): if self.last_pos_fix_t is None or abs(self.last_pos_fix_t - t) >= 2: - min_measurements = 5 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 4 + min_measurements = 6 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 5 pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) - if len(pos_fix) > 0: + if len(pos_fix) > 0 and np.median(np.abs(pos_fix_residual)) < POS_FIX_RESIDUAL_THRESHOLD: self.last_pos_fix = pos_fix[:3] self.last_pos_residual = pos_fix_residual self.last_pos_fix_t = t diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index 3a7c073b55c493..c10a470d1a5b61 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -138,7 +138,7 @@ def test_laika_online(self): laikad = Laikad(auto_update=True, valid_ephem_types=EphemerisType.ULTRA_RAPID_ORBIT) correct_msgs = verify_messages(self.logs, laikad) - correct_msgs_expected = 560 + correct_msgs_expected = 555 self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @@ -159,7 +159,7 @@ def test_laika_online_nav_only(self): # Disable fetch_orbits to test NAV only laikad.fetch_orbits = Mock() correct_msgs = verify_messages(self.logs, laikad) - correct_msgs_expected = 560 + correct_msgs_expected = 559 self.assertEqual(correct_msgs_expected, len(correct_msgs)) self.assertEqual(correct_msgs_expected, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) @@ -168,8 +168,8 @@ def test_laika_offline(self, downloader_mock): downloader_mock.side_effect = IOError laikad = Laikad(auto_update=False) correct_msgs = verify_messages(self.logs, laikad) - self.assertEqual(256, len(correct_msgs)) - self.assertEqual(256, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) + self.assertEqual(16, len(correct_msgs)) + self.assertEqual(16, len([m for m in correct_msgs if m.gnssMeasurements.positionECEF.valid])) def test_laika_get_orbits(self): laikad = Laikad(auto_update=False) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 4908b8618296b9..7fced5ad62cad8 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -bd2ea158977f5c26658bed8ac683b72c2c592d06 \ No newline at end of file +0da0928230d11dd4c76293b9e77b027eb4a1e291 \ No newline at end of file From a9401319dfa89fea2d2699aaddc758cbc47b6396 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 18:20:36 +0200 Subject: [PATCH 282/436] nav: use laikad position if locationd is not yet available (#25033) * ui: use laikad position when locationd is not ready * cleanup * same threshold as locationd * use first bearing directly * use in navd too --- selfdrive/navd/navd.py | 19 ++++++-- selfdrive/ui/qt/maps/map.cc | 71 ++++++++++++++++++++++------- selfdrive/ui/qt/maps/map.h | 3 +- selfdrive/ui/qt/maps/map_helpers.cc | 5 ++ selfdrive/ui/qt/maps/map_helpers.h | 1 + selfdrive/ui/ui.cc | 2 +- 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/selfdrive/navd/navd.py b/selfdrive/navd/navd.py index f02de43c7bf9fb..89a1c9bdfb458d 100755 --- a/selfdrive/navd/navd.py +++ b/selfdrive/navd/navd.py @@ -4,12 +4,14 @@ import threading import requests +import numpy as np import cereal.messaging as messaging from cereal import log from common.api import Api from common.params import Params from common.realtime import Ratekeeper +from common.transformations.coordinates import ecef2geodetic from selfdrive.navd.helpers import (Coordinate, coordinate_from_param, distance_along_geometry, maxspeed_to_ms, minimum_distance, @@ -18,6 +20,7 @@ REROUTE_DISTANCE = 25 MANEUVER_TRANSITION_THRESHOLD = 10 +VALID_POS_STD = 50.0 class RouteEngine: @@ -72,13 +75,21 @@ def update(self): def update_location(self): location = self.sm['liveLocationKalman'] - self.gps_ok = location.gpsOK + laikad = self.sm['gnssMeasurements'] - self.localizer_valid = (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid + locationd_valid = (location.status == log.LiveLocationKalman.Status.valid) and location.positionGeodetic.valid + laikad_valid = laikad.positionECEF.valid and np.linalg.norm(laikad.positionECEF.std) < VALID_POS_STD - if self.localizer_valid: + self.localizer_valid = locationd_valid or laikad_valid + self.gps_ok = location.gpsOK or laikad_valid + + if locationd_valid: self.last_bearing = math.degrees(location.calibratedOrientationNED.value[2]) self.last_position = Coordinate(location.positionGeodetic.value[0], location.positionGeodetic.value[1]) + elif laikad_valid: + geodetic = ecef2geodetic(laikad.positionECEF.value) + self.last_position = Coordinate(geodetic[0], geodetic[1]) + self.last_bearing = None def recompute_route(self): if self.last_position is None: @@ -276,7 +287,7 @@ def should_recompute(self): def main(sm=None, pm=None): if sm is None: - sm = messaging.SubMaster(['liveLocationKalman', 'managerState']) + sm = messaging.SubMaster(['liveLocationKalman', 'gnssMeasurements', 'managerState']) if pm is None: pm = messaging.PubMaster(['navInstruction', 'navRoute']) diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc index 4c6a0a4e65e10e..fd47f4188f2402 100644 --- a/selfdrive/ui/qt/maps/map.cc +++ b/selfdrive/ui/qt/maps/map.cc @@ -1,5 +1,6 @@ #include "selfdrive/ui/qt/maps/map.h" +#include #include #include @@ -7,6 +8,7 @@ #include #include "common/swaglog.h" +#include "common/transformations/coordinates.hpp" #include "selfdrive/ui/qt/maps/map_helpers.h" #include "selfdrive/ui/qt/request_repeater.h" #include "selfdrive/ui/qt/util.h" @@ -22,6 +24,8 @@ const float MAX_PITCH = 50; const float MIN_PITCH = 0; const float MAP_SCALE = 2; +const float VALID_POS_STD = 50.0; // m + const QString ICON_SUFFIX = ".png"; MapWindow::MapWindow(const QMapboxGLSettings &settings) : m_settings(settings), velocity_filter(0, 10, 0.05) { @@ -105,18 +109,53 @@ void MapWindow::updateState(const UIState &s) { update(); if (sm.updated("liveLocationKalman")) { - auto location = sm["liveLocationKalman"].getLiveLocationKalman(); - auto pos = location.getPositionGeodetic(); - auto orientation = location.getCalibratedOrientationNED(); - auto velocity = location.getVelocityCalibrated(); - - localizer_valid = (location.getStatus() == cereal::LiveLocationKalman::Status::VALID) && - pos.getValid() && orientation.getValid() && velocity.getValid(); - - if (localizer_valid) { - last_position = QMapbox::Coordinate(pos.getValue()[0], pos.getValue()[1]); - last_bearing = RAD2DEG(orientation.getValue()[2]); - velocity_filter.update(velocity.getValue()[0]); + auto locationd_location = sm["liveLocationKalman"].getLiveLocationKalman(); + auto locationd_pos = locationd_location.getPositionGeodetic(); + auto locationd_orientation = locationd_location.getCalibratedOrientationNED(); + auto locationd_velocity = locationd_location.getVelocityCalibrated(); + + locationd_valid = (locationd_location.getStatus() == cereal::LiveLocationKalman::Status::VALID) && + locationd_pos.getValid() && locationd_orientation.getValid() && locationd_velocity.getValid(); + + if (locationd_valid) { + last_position = QMapbox::Coordinate(locationd_pos.getValue()[0], locationd_pos.getValue()[1]); + last_bearing = RAD2DEG(locationd_orientation.getValue()[2]); + velocity_filter.update(locationd_velocity.getValue()[0]); + } + } + + if (sm.updated("gnssMeasurements")) { + auto laikad_location = sm["gnssMeasurements"].getGnssMeasurements(); + auto laikad_pos = laikad_location.getPositionECEF(); + auto laikad_pos_ecef = laikad_pos.getValue(); + auto laikad_pos_std = laikad_pos.getStd(); + auto laikad_velocity_ecef = laikad_location.getVelocityECEF().getValue(); + + laikad_valid = laikad_pos.getValid() && Eigen::Vector3d(laikad_pos_std[0], laikad_pos_std[1], laikad_pos_std[2]).norm() < VALID_POS_STD; + + if (laikad_valid && !locationd_valid) { + ECEF ecef = {.x = laikad_pos_ecef[0], .y = laikad_pos_ecef[1], .z = laikad_pos_ecef[2]}; + Geodetic laikad_pos_geodetic = ecef2geodetic(ecef); + last_position = QMapbox::Coordinate(laikad_pos_geodetic.lat, laikad_pos_geodetic.lon); + + // Compute NED velocity + LocalCoord converter(ecef); + ECEF next_ecef = {.x = ecef.x + laikad_velocity_ecef[0], .y = ecef.y + laikad_velocity_ecef[1], .z = ecef.z + laikad_velocity_ecef[2]}; + Eigen::VectorXd ned_vel = converter.ecef2ned(next_ecef).to_vector() - converter.ecef2ned(ecef).to_vector(); + + float velocity = ned_vel.norm(); + velocity_filter.update(velocity); + + // Convert NED velocity to angle + if (velocity > 1.0) { + float new_bearing = fmod(RAD2DEG(atan2(ned_vel[1], ned_vel[0])) + 360.0, 360.0); + if (last_bearing) { + float delta = 0.1 * angle_difference(*last_bearing, new_bearing); // Smooth heading + last_bearing = fmod(*last_bearing + delta + 360.0, 360.0); + } else { + last_bearing = new_bearing; + } + } } } @@ -142,9 +181,7 @@ void MapWindow::updateState(const UIState &s) { initLayers(); - if (!localizer_valid) { - map_instructions->showError("Waiting for GPS"); - } else { + if (locationd_valid || laikad_valid) { map_instructions->noError(); // Update current location marker @@ -154,6 +191,8 @@ void MapWindow::updateState(const UIState &s) { carPosSource["type"] = "geojson"; carPosSource["data"] = QVariant::fromValue(feature1); m_map->updateSource("carPosSource", carPosSource); + } else { + map_instructions->showError("Waiting for GPS"); } if (pan_counter == 0) { @@ -174,7 +213,7 @@ void MapWindow::updateState(const UIState &s) { auto i = sm["navInstruction"].getNavInstruction(); emit ETAChanged(i.getTimeRemaining(), i.getTimeRemainingTypical(), i.getDistanceRemaining()); - if (localizer_valid) { + if (locationd_valid || laikad_valid) { m_map->setPitch(MAX_PITCH); // TODO: smooth pitching based on maneuver distance emit distanceChanged(i.getManeuverDistance()); // TODO: combine with instructionsChanged emit instructionsChanged(i); diff --git a/selfdrive/ui/qt/maps/map.h b/selfdrive/ui/qt/maps/map.h index 7c39b24c3cc33a..ecba867edb250c 100644 --- a/selfdrive/ui/qt/maps/map.h +++ b/selfdrive/ui/qt/maps/map.h @@ -104,7 +104,8 @@ class MapWindow : public QOpenGLWidget { std::optional last_position; std::optional last_bearing; FirstOrderFilter velocity_filter; - bool localizer_valid = false; + bool laikad_valid = false; + bool locationd_valid = false; MapInstructions* map_instructions; MapETA* map_eta; diff --git a/selfdrive/ui/qt/maps/map_helpers.cc b/selfdrive/ui/qt/maps/map_helpers.cc index 2b2c27418eae36..66acb7a25d9a39 100644 --- a/selfdrive/ui/qt/maps/map_helpers.cc +++ b/selfdrive/ui/qt/maps/map_helpers.cc @@ -116,3 +116,8 @@ std::optional coordinate_from_param(std::string param) { return {}; } } + +double angle_difference(double angle1, double angle2) { + double diff = fmod(angle2 - angle1 + 180.0, 360.0) - 180.0; + return diff < -180.0 ? diff + 360.0 : diff; +} diff --git a/selfdrive/ui/qt/maps/map_helpers.h b/selfdrive/ui/qt/maps/map_helpers.h index 344246bb05579c..1c08c541c3d77a 100644 --- a/selfdrive/ui/qt/maps/map_helpers.h +++ b/selfdrive/ui/qt/maps/map_helpers.h @@ -26,3 +26,4 @@ QMapbox::CoordinatesCollections capnp_coordinate_list_to_collection(const capnp: QMapbox::CoordinatesCollections coordinate_list_to_collection(QList coordinate_list); std::optional coordinate_from_param(std::string param); +double angle_difference(double angle1, double angle2); diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index f6193f97a613de..6fe1d838ed1f08 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -227,7 +227,7 @@ UIState::UIState(QObject *parent) : QObject(parent) { sm = std::make_unique>({ "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "roadCameraState", "pandaStates", "carParams", "driverMonitoringState", "sensorEvents", "carState", "liveLocationKalman", - "wideRoadCameraState", "managerState", "navInstruction", "navRoute", + "wideRoadCameraState", "managerState", "navInstruction", "navRoute", "gnssMeasurements", }); Params params; From cf862b6576a8b3f66e26d38080a1c8eec00f0793 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 8 Jul 2022 19:03:42 +0200 Subject: [PATCH 283/436] Laikad: Fix getting covariances for pos and velocity (#25084) * Fix getting covariances for pos and velocity * ref commit --- selfdrive/locationd/laikad.py | 13 +++++++------ selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 67867e82b3580d..0954cb4c9f03cb 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -120,11 +120,12 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): self.update_localizer(est_pos, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) - ecef_pos = self.gnss_kf.x[GStates.ECEF_POS].tolist() - ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY].tolist() + ecef_pos = self.gnss_kf.x[GStates.ECEF_POS] + ecef_vel = self.gnss_kf.x[GStates.ECEF_VELOCITY] - pos_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_POS].diagonal())).tolist() - vel_std = np.sqrt(abs(self.gnss_kf.P[GStates.ECEF_VELOCITY].diagonal())).tolist() + p = self.gnss_kf.P.diagonal() + pos_std = np.sqrt(p[GStates.ECEF_POS]) + vel_std = np.sqrt(p[GStates.ECEF_VELOCITY]) meas_msgs = [create_measurement_msg(m) for m in corrected_measurements] dat = messaging.new_message("gnssMeasurements") @@ -132,8 +133,8 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): dat.gnssMeasurements = { "gpsWeek": report.gpsWeek, "gpsTimeOfWeek": report.rcvTow, - "positionECEF": measurement_msg(value=ecef_pos, std=pos_std, valid=kf_valid), - "velocityECEF": measurement_msg(value=ecef_vel, std=vel_std, valid=kf_valid), + "positionECEF": measurement_msg(value=ecef_pos.tolist(), std=pos_std.tolist(), valid=kf_valid), + "velocityECEF": measurement_msg(value=ecef_vel.tolist(), std=vel_std.tolist(), valid=kf_valid), "positionFixECEF": measurement_msg(value=self.last_pos_fix, std=self.last_pos_residual, valid=self.last_pos_fix_t == t), "ubloxMonoTime": ublox_mono_time, "correctedMeasurements": meas_msgs diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7fced5ad62cad8..0d61dbd735b048 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -0da0928230d11dd4c76293b9e77b027eb4a1e291 \ No newline at end of file +dab90772097a0dd4706677ba4fe5e84b10232099 \ No newline at end of file From c04942795bbb0a2a31f4c05322e6897e279f4ba9 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Fri, 8 Jul 2022 10:16:34 -0700 Subject: [PATCH 284/436] Update RELEASES.md --- RELEASES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index e0e75f3213ecb9..392e9dc486a17d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,7 +1,9 @@ Version 0.8.15 (2022-07-XX) ======================== * New driving model - * Path planning uses end-to-end output instead of lane lines + * Path planning uses end-to-end output instead of lane lines at all times + * Reduced ping pong + * Improved lane centering * New lateral controller based on physical wheel torque model * Much smoother control, consistent across the speed range * Effective feedforward that uses road roll From 35c8c0e600746e092bb236dabd05491d8be1078a Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 8 Jul 2022 19:19:57 +0200 Subject: [PATCH 285/436] casync: increase chunk download timeout --- system/hardware/tici/casync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hardware/tici/casync.py b/system/hardware/tici/casync.py index d0d0da3c6a17a3..9dff64239e66cf 100755 --- a/system/hardware/tici/casync.py +++ b/system/hardware/tici/casync.py @@ -22,7 +22,7 @@ CA_TABLE_ENTRY_LEN = 40 CA_TABLE_MIN_LEN = CA_TABLE_HEADER_LEN + CA_TABLE_ENTRY_LEN -CHUNK_DOWNLOAD_TIMEOUT = 10 +CHUNK_DOWNLOAD_TIMEOUT = 60 CHUNK_DOWNLOAD_RETRIES = 3 CAIBX_DOWNLOAD_TIMEOUT = 120 From 0b6cf0481c07323ee427655a5849c90d9c623e64 Mon Sep 17 00:00:00 2001 From: eFini Date: Sat, 9 Jul 2022 03:12:14 +0800 Subject: [PATCH 286/436] Add Traditional Chinese translations (#25077) * Create main_zh-CHT * Update Co-authored-by: Shane Smiskol --- selfdrive/ui/translations/languages.json | 3 +- .../{main_zh.qm => main_zh-CHS.qm} | Bin .../{main_zh.ts => main_zh-CHS.ts} | 0 selfdrive/ui/translations/main_zh-CHT.qm | Bin 0 -> 17741 bytes selfdrive/ui/translations/main_zh-CHT.ts | 1220 +++++++++++++++++ 5 files changed, 1222 insertions(+), 1 deletion(-) rename selfdrive/ui/translations/{main_zh.qm => main_zh-CHS.qm} (100%) rename selfdrive/ui/translations/{main_zh.ts => main_zh-CHS.ts} (100%) create mode 100644 selfdrive/ui/translations/main_zh-CHT.qm create mode 100644 selfdrive/ui/translations/main_zh-CHT.ts diff --git a/selfdrive/ui/translations/languages.json b/selfdrive/ui/translations/languages.json index b8c3a50fd70f69..48f09486742e89 100644 --- a/selfdrive/ui/translations/languages.json +++ b/selfdrive/ui/translations/languages.json @@ -1,5 +1,6 @@ { "English": "", - "中文(简体)": "main_zh", + "中文(繁體)": "main_zh-CHT", + "中文(简体)": "main_zh-CHS", "한국어": "main_ko" } diff --git a/selfdrive/ui/translations/main_zh.qm b/selfdrive/ui/translations/main_zh-CHS.qm similarity index 100% rename from selfdrive/ui/translations/main_zh.qm rename to selfdrive/ui/translations/main_zh-CHS.qm diff --git a/selfdrive/ui/translations/main_zh.ts b/selfdrive/ui/translations/main_zh-CHS.ts similarity index 100% rename from selfdrive/ui/translations/main_zh.ts rename to selfdrive/ui/translations/main_zh-CHS.ts diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm new file mode 100644 index 0000000000000000000000000000000000000000..8b055d665e4582f8d1461595d0d88a7eeacfbb49 GIT binary patch literal 17741 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 + + + + AbstractAlert + + + Close + 關閉 + + + + Snooze Update + 暫停更新 + + + + Reboot and Update + 重啟並更新 + + + + AdvancedNetworking + + + Back + 回上頁 + + + + Enable Tethering + 啟用網路分享 + + + + Tethering Password + 網路分享密碼 + + + + + EDIT + 編輯 + + + + Enter new tethering password + 輸入新的網路分享密碼 + + + + IP Address + IP 地址 + + + + Enable Roaming + 啟用漫遊 + + + + APN Setting + APN 設置 + + + + Enter APN + 輸入 APN + + + + leave blank for automatic configuration + 留空白將自動配置 + + + + ConfirmationDialog + + + + Ok + 確定 + + + + Cancel + 取消 + + + + DeclinePage + + + You must accept the Terms and Conditions in order to use openpilot. + 您必須先接受條款和條件才能使用 openpilot。 + + + + Back + 回上頁 + + + + Decline, uninstall %1 + 拒絕並卸載 %1 + + + + DevicePanel + + + Dongle ID + Dongle ID + + + + N/A + 無法使用 + + + + Serial + 序號 + + + + Driver Camera + 駕駛監控 + + + + PREVIEW + 預覽 + + + + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) + 預覽駕駛監控鏡頭畫面,方便調整設備安裝的位置,以提供更準確的駕駛監控。(車子必須保持在熄火的狀態) + + + + Reset Calibration + 重置校準 + + + + RESET + 重置 + + + + Are you sure you want to reset calibration? + 您確定要重置校準嗎? + + + + Review Training Guide + 觀看使用教學 + + + + REVIEW + 觀看 + + + + Review the rules, features, and limitations of openpilot + 觀看 openpilot 的使用規則、功能和限制 + + + + Are you sure you want to review the training guide? + 您確定要觀看使用教學嗎? + + + + Regulatory + 法規/監管 + + + + VIEW + 觀看 + + + Change Language + 更改語言 + + + CHANGE + 更改 + + + Select a language + 選擇語言 + + + + Reboot + 重新啟動 + + + + Power Off + 關機 + + + + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. + openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以内或朝下偏差 8° 以内。鏡頭在後台會持續自動校準,很少有需要重置的情况。 + + + + Your device is pointed %1° %2 and %3° %4. + 你的設備目前朝%2 %1° 以及朝%4 %3° 。 + + + + down + + + + + up + + + + + left + + + + + right + + + + + Are you sure you want to reboot? + 您確定要重新啟動嗎? + + + + Disengage to Reboot + 請先取消控車才能重新啟動 + + + + Are you sure you want to power off? + 您確定您要關機嗎? + + + + Disengage to Power Off + 請先取消控車才能關機 + + + + DriveStats + + + Drives + 旅程 + + + + Hours + 小時 + + + + ALL TIME + 總共 + + + + PAST WEEK + 上周 + + + + KM + 公里 + + + + Miles + 英里 + + + + DriverViewScene + + + camera starting + 開啟相機中 + + + + InputDialog + + + Cancel + 取消 + + + + Need at least + 需要至少 + + + + characters! + 個字元! + + + + Installer + + + Installing... + 安裝中… + + + + Receiving objects: + 接收對象: + + + + Resolving deltas: + 分析差異: + + + + Updating files: + 更新檔案: + + + + MapPanel + + + Current Destination + 當前目的地 + + + + CLEAR + 清除 + + + + Recent Destinations + 最近目的地 + + + + Try the Navigation Beta + 試用導航功能 + + + + Get turn-by-turn directions displayed and more with a comma +prime subscription. Sign up now: https://connect.comma.ai + 成為 comma 高級會員來使用導航功能 +立即註冊:https://connect.comma.ai + + + Get turn-by-turn directions displayed and more with a comma +prime subscription. Sign up now: https://connect.comma.ai + 成為 comma 高級會員來使用導航功能,立即註冊:https://connect.comma.ai + + + + No home +location set + 未設定 +住家位置 + + + + No work +location set + 未設定 +工作位置 + + + + no recent destinations + 沒有最近的導航記錄 + + + + MultiOptionDialog + + Select + 選擇 + + + Cancel + 取消 + + + + Networking + + + Advanced + 進階 + + + + Enter password + 輸入密碼 + + + + + for " + 給 " + + + + Wrong password + 密碼錯誤 + + + + NvgWindow + + + km/h + km/h + + + + mph + mph + + + + + MAX + 最高 + + + + + SPEED + 速度 + + + + + LIMIT + 速限 + + + + OffroadHome + + + UPDATE + 更新 + + + + ALERTS + 提醒 + + + + ALERT + 提醒 + + + + PairingPopup + + + Pair your device to your comma account + 將設備與您的 comma 帳號配對 + + + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> + <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> + <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> + </ol> + + + <ol type='1' style='margin-left: 15px;'> + <li style='margin-bottom: 50px;'>用手機連至 https://connect.comma.ai</li> + <li style='margin-bottom: 50px;'>點選 "add new device" 後掃描右邊的二維碼</li> + <li style='margin-bottom: 50px;'>將 connect.comma.ai 加入您的主屏幕,以便像手機 App 一樣使用它</li> + </ol> + + + + + PrimeAdWidget + + + Upgrade Now + 馬上升級 + + + + Become a comma prime member at connect.comma.ai + 成為 connect.comma.ai 的高級會員 + + + + PRIME FEATURES: + 高級會員特點: + + + + Remote access + 遠程訪問 + + + + 1 year of storage + 一年的雲端行車記錄 + + + + Developer perks + 開發者福利 + + + + PrimeUserWidget + + + ✓ SUBSCRIBED + ✓ 已訂閱 + + + + comma prime + comma 高級會員 + + + + CONNECT.COMMA.AI + CONNECT.COMMA.AI + + + + COMMA POINTS + COMMA 積分 + + + + QObject + + + Reboot + 重新啟動 + + + + Exit + 離開 + + + + dashcam + 行車記錄器 + + + + openpilot + openpilot + + + + %1 minute%2 ago + we don't need %2 + %1 分鐘前 + + + + %1 hour%2 ago + we don't need %2 + %1 小時前 + + + + %1 day%2 ago + we don't need %2 + %1 天前 + + + + Reset + + + Reset failed. Reboot to try again. + 重置失敗。請重新啟動後再試。 + + + + Are you sure you want to reset your device? + 您確定要重置你的設備嗎? + + + + Resetting device... + 重置設備中… + + + + System Reset + 系統重置 + + + + System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + 系統重置已觸發。請按確認刪除所有內容和設置。按取消恢復啟動。 + + + + Cancel + 取消 + + + + Reboot + 重新啟動 + + + + Confirm + 確認 + + + + Unable to mount data partition. Press confirm to reset your device. + 無法掛載數據分區。請按確認重置您的設備。 + + + + RichTextDialog + + + Ok + 確定 + + + + SettingsWindow + + + × + × + + + + Device + 設備 + + + + + Network + 網路 + + + + Toggles + 設定 + + + + Software + 軟體 + + + + Navigation + 導航 + + + + Setup + + + WARNING: Low Voltage + 警告:電壓過低 + + + + Power your device in a car with a harness or proceed at your own risk. + 請使用車上 harness 提供的電源,若繼續的話您需要自擔風險。 + + + + Power off + 關機 + + + + + + Continue + 繼續 + + + + Getting Started + 入門 + + + + Before we get on the road, let’s finish installation and cover some details. + 在我們上路之前,讓我們完成安裝並介紹一些細節。 + + + + Connect to Wi-Fi + 連接到無線網絡 + + + + + Back + 回上頁 + + + + Continue without Wi-Fi + 在沒有 Wi-Fi 的情況下繼續 + + + + Waiting for internet + 連接至網路中 + + + + Choose Software to Install + 選擇要安裝的軟體 + + + + Dashcam + 行車記錄器 + + + + Custom Software + 定制的軟體 + + + + Enter URL + 輸入網址 + + + + for Custom Software + 定制的軟體 + + + + Downloading... + 下載中… + + + + Download Failed + 下載失敗 + + + + Ensure the entered URL is valid, and the device’s internet connection is good. + 請確定您輸入的是有效的安裝網址,並且確定設備的網路連線狀態良好。 + + + + Reboot device + 重新啟動 + + + + Start over + 重新開始 + + + + SetupWidget + + + Finish Setup + 完成設置 + + + + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. + 將您的設備與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。 + + + + Pair device + 配對設備 + + + + Sidebar + + + + CONNECT + 雲端服務 + + + + OFFLINE + 已離線 + + + + + ONLINE + 已連線 + + + + ERROR + 錯誤 + + + + + + TEMP + 溫度 + + + + HIGH + 偏高 + + + + GOOD + 正常 + + + + OK + 一般 + + + + VEHICLE + 車輛通訊 + + + + NO + 未連線 + + + + PANDA + 車輛通訊 + + + + GPS + GPS + + + + SEARCH + 車輛通訊 + + + + -- + -- + + + + Wi-Fi + + + + + ETH + + + + + 2G + + + + + 3G + + + + + LTE + + + + + 5G + + + + + SoftwarePanel + + + Git Branch + Git 分支 + + + + Git Commit + Git 提交 + + + + OS Version + 系統版本 + + + + Version + 版本 + + + + Last Update Check + 上次檢查時間 + + + + The last time openpilot successfully checked for an update. The updater only runs while the car is off. + 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 + + + + Check for Update + 檢查更新 + + + + CHECKING + 檢查中 + + + + Uninstall + 卸載 + + + + UNINSTALL + 卸載 + + + + Are you sure you want to uninstall? + 您確定您要卸載嗎? + + + + failed to fetch update + 下載更新失敗 + + + + + CHECK + 檢查 + + + + SshControl + + + SSH Keys + SSH 密鑰 + + + + Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. + 警告:這將授權給 GitHub 帳號中所有公鑰 SSH 訪問權限。切勿輸入非您自己的 GitHub 用戶名。comma 員工「永遠不會」要求您添加他們的 GitHub 用戶名。 + + + + + ADD + 新增 + + + + Enter your GitHub username + 請輸入您 GitHub 的用戶名 + + + + LOADING + 載入中 + + + + REMOVE + 移除 + + + + Username '%1' has no keys on GitHub + GitHub 用戶 '%1' 沒有設定任何密鑰 + + + + Request timed out + 請求超時 + + + + Username '%1' doesn't exist on GitHub + GitHub 用戶 '%1' 不存在 + + + + SshToggle + + + Enable SSH + 啟用 SSH 服務 + + + + TermsPage + + + Terms & Conditions + 條款和條件 + + + + Decline + 拒絕 + + + + Scroll to accept + 滑動至頁尾接受條款 + + + + Agree + 接受 + + + + TogglesPanel + + + Enable openpilot + 啟用 openpilot + + + + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. + 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。 + + + + Enable Lane Departure Warnings + 啟用車道偏離警告 + + + + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). + 車速在時速 50 公里 (31 英里) 以上且未打方向燈的情況下,如果偵測到車輛駛出目前車道線時,發出車道偏離警告。 + + + + Enable Right-Hand Drive + 啟用右駕模式 + + + + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. + openpilot 將對右側駕駛進行監控 (但仍遵守靠左駕的交通慣例)。 + + + + Use Metric System + 使用公制單位 + + + + Display speed in km/h instead of mph. + 啟用後,速度單位顯示將從 mp/h 改為 km/h。 + + + + Record and Upload Driver Camera + 記錄並上傳駕駛監控影像 + + + + Upload data from the driver facing camera and help improve the driver monitoring algorithm. + 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 + + + + Disengage On Accelerator Pedal + 油門取消控車 + + + + When enabled, pressing the accelerator pedal will disengage openpilot. + 啟用後,踩踏油門將會取消 openpilot 控制。 + + + + Show ETA in 24h format + 預計到達時間單位改用 24 小時制 + + + + Use 24h format instead of am/pm + 使用 24 小時制。(預設值為 12 小時制) + + + + openpilot Longitudinal Control + openpilot 縱向控制 + + + + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! + openpilot 將會關閉雷達訊號並接管油門和剎車的控制。注意:這也會關閉自動緊急煞車 (AEB) 系統! + + + + Updater + + + Update Required + 系統更新 + + + + An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. + 設備的操作系統需要更新。請將您的設備連接到 Wi-Fi 以獲得最快的更新體驗。下載大小約為 1GB。 + + + + Connect to Wi-Fi + 連接到無線網絡 + + + + Install + 安裝 + + + + Back + 回上頁 + + + + Loading... + 載入中… + + + + Reboot + 重新啟動 + + + + Update failed + 更新失敗 + + + + WifiUI + + + + Scanning for networks... + 掃描無線網路中... + + + + CONNECTING... + 連線中... + + + + FORGET + 清除 + + + + Forget Wi-Fi Network " + 清除 Wi-Fi 網路 " + + + From e0f8f50baa209f0f5fc713100bcec16411cd5925 Mon Sep 17 00:00:00 2001 From: "Mr.one" <84395321+hellokitty-666@users.noreply.github.com> Date: Sat, 9 Jul 2022 03:49:04 +0800 Subject: [PATCH 287/436] Improve Chinese (Simplified) translations (#25075) * Optimize Chinese local translation * update QM Co-authored-by: Shane Smiskol --- selfdrive/ui/translations/main_zh-CHS.qm | Bin 17617 -> 17629 bytes selfdrive/ui/translations/main_zh-CHS.ts | 38 +++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 627f6afc204dba0b2c2a87f13cad266e9edcbd4f..be6c51030617f25c54c086e107274a1b0434ea5c 100644 GIT binary patch delta 1770 zcmX9;ZB$g%8h*~1ea_4|b7oG+SD^;X0P$^JzYxI!S13>@2IbOBQ@CSkUNenbmUI0U zF~yFQijpO0S*D9g%a6*G^pb*F+VU$#eaRTuj;(S2d0bE6ZG6+ayOc_P&AWq?)r-K+h8L%G(jNSZB z0x@%d$Cd+W%vrFSc`gHwKgn}RK=EMiokipq>LIQG{D~~I;9j5(VpT1GwjkP;2QkJ9 z;;8>YZ0Z6e9inL{6DklbpA(B9&Iq7?JCrpsKvD&iJ-Yz?Yp4e<5qn{enZ*3_VNW>^ zI88ik67^7wZC(UFl5;V(XFo7_B*tko0COn{hS!h?8>q--2Zu3!jzuLBFyTiM=yw|v z-}n#rSE58d&iKnHnX!b0Q}NWUdcg4$CgpEr!9q;_;B_FY6jQ%$=Ksg=Vi~*ccLsmI z!}G=xy!UOZAIQ!`h0^_Xae()fG;AF$O6!t} zPf*U->(Z7Bj1T)=YAq2=yi&R-?*O9yBJ~bg4&0w2>%MP*_}Aq4?hYV*r<|uRAhBlo znfusq+MDvW^YI!F%#uG_OJ;-I@|XGR0QVO8`(h@J`b}CMn8$~P z+dguWUgBX};{i^KY1j@mhBKYlS|~^R-xJe!1z6;!cs`#Jn&ZT)9ixGPd&Da78!%+4 z*pz>S^OF!ncSaBg^oXtXeL(WR#QR-ktQjIcK2rlkFB6~6uBS-z#6g2<8@I)&D{YMb zC{F$KGxL8UIv)8HQ2$W$;S@1B^P1wSdWotuC}Z65lwBy3rnjw220zoXQzzZF{aYFteGCI4`Nn-<<=dtNI9y?u_YXeA5`}*>A>WHrD|4h z9{E?OV;jE!Mod?yNK`ZSq`IT;F7RNKdL(@_VDD9LjlV!MJMEq%>JqQo8={u6jhF3v z=XR37^CnyKwWyQG!g0F=qRv}ru&Se{Q7OqYOUkVgsn~1?S{T*^+p!f^g|CTBsN_?=Hod<)|>PP`5GM5 znPVM0BjT!BH>3c!l9(68xC5#7eI!|1q zA#u^pe?HN~%adS)9cAIJa+u0%d}!Sz`Mqr;0_O`e_f z^oXk|m+0+teVav3weQ^FA96I~_PV21v6=g>x;=ek*z{NKjLUooQfu6WYbi!Zg1fSo zH!ps#d)w7V>Fx~o?t11O{JT}EM(4Gd5m)Z=Hos<$>*DV8QRZAH0ZcP%=m6`yA__1Ae{?jW#mXseO=O=gEc*soo|=zSTj;m QmkxU&cA6Qe&dkyO1fHhgfdBvi delta 1804 zcmYLJYg80v7=C7UW_D+Gb|w@gNd*>AxvEKs3YJ(XXdsAXp;5}ZQD)jfsZ95jlA=j? zMhy|j62dI=#2S@ZSptQmcF_*Sp$x%G*HdPiiKnNtKl(A}+3(%&y*F-7`>S|nRt>|$UAojFuVY;9|rVS_l=K z`>RkO9-;kF6fAt2iAUhBrUtHa1^lU$TavPhwMyYxiw6kvFhv|hRhj9)2LXC31F zj0~WW9Kd0{QfMK_WS$x63{ z`liXNk`0dSetFY&rd87eI7|rO@c00xHORHkQ%ud08=mFZ2D|0`pPU8~TI7!hNR?PF zABp^e)Isj_5FV({NB+-3MEpE>MqPx^egi_`@ z^<;MrXCq(j33`+-!#-_lJYNl;qHD#UAEW$=R&ilCFl-btodF)wHk~thF4wkRr+n^h zTJ@pbEa-c!wrehzWwTa)jB?t3*Y<7LO1=iPgJWebUB33QmwvLSwQ3{y8oaBu4oTx~ z)M)1yl`_v+r_i#7_m?@N4!5BY=7uwd`^q#n{icz6nh!z3dSm8uBqPXeRP5yD#WWgq z=We6A_ZfQ|7jh>XpN^L$qq#1#914|VgtYYEn#QvQr^`$XRKZB$6R_5JX4Cy z>hCtpDwyYwiTcK&I#y*BmFH-_Jijv`+h!AUT>klEjyR+Z%c@{SrH&N7^=GF}6 REVIEW - 审查 + 重新查看 @@ -180,15 +180,15 @@ VIEW - 看法 + 查看 Change Language - 改变语言 + 切换语言 CHANGE - 改变 + 切换 Select a language @@ -227,7 +227,7 @@ left - 剩下 + 向左 @@ -270,7 +270,7 @@ ALL TIME - 整天 + 全部 @@ -285,7 +285,7 @@ Miles - 迈尔斯 + 英里 @@ -293,7 +293,7 @@ camera starting - 相机启动 + 相机启动中 @@ -324,7 +324,7 @@ Receiving objects: - 接收物体: + 正在接收: @@ -462,12 +462,12 @@ location set ALERTS - 个警报 + 警报 ALERT - 个警报 + 警报 @@ -561,7 +561,7 @@ location set Exit - 出口 + 退出 @@ -571,7 +571,7 @@ location set openpilot - 开放式飞行员 + openpilot @@ -732,7 +732,7 @@ location set Waiting for internet - 等待上网 + 等待网络连接 @@ -857,7 +857,7 @@ location set NO - + 未连接 @@ -1046,12 +1046,12 @@ location set Decline - 衰退 + 拒绝 Scroll to accept - 滚动接受 + 滑动接受 @@ -1084,7 +1084,7 @@ location set Enable Right-Hand Drive - 启用右手驱动 + 启用右舵模式 @@ -1114,7 +1114,7 @@ location set Disengage On Accelerator Pedal - 松开加速踏板 + 踩油门解除 From df251ef50ebfe5c997e14a03fca4ec932bc4c5cf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 13:00:43 -0700 Subject: [PATCH 288/436] Fix master-ci dirty working tree + CI test (#25087) * check * test for dirty working tree * swap order * fix diff --- .github/workflows/selfdrive_tests.yaml | 7 +++++-- .gitignore | 2 -- Jenkinsfile | 1 + rednose_repo | 2 +- release/check-dirty.sh | 11 +++++++++++ release/files_common | 3 +++ selfdrive/loggerd/.gitignore | 1 + 7 files changed, 22 insertions(+), 5 deletions(-) create mode 100755 release/check-dirty.sh diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 35a08d4fe9a44d..99a21b58f37a86 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -59,11 +59,14 @@ jobs: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh cp Dockerfile.openpilot_base $STRIPPED_DIR - name: Build Docker image - run: eval "$BUILD" + run: | + eval "$BUILD" + rm $STRIPPED_DIR/Dockerfile.openpilot_base - name: Build openpilot and run checks run: | cd $STRIPPED_DIR ${{ env.RUN }} "CI=1 python selfdrive/manager/build.py && \ + release/check-dirty.sh && \ python -m unittest discover selfdrive/car" build_all: @@ -89,7 +92,7 @@ jobs: - name: Build Docker image run: eval "$BUILD" - name: Build openpilot with all flags - run: ${{ env.RUN }} "scons -j$(nproc) --extras --test" + run: ${{ env.RUN }} "scons -j$(nproc) --extras --test && release/check-dirty.sh" - name: Cleanup scons cache run: | ${{ env.RUN }} "scons -j$(nproc) --extras --test && \ diff --git a/.gitignore b/.gitignore index 0092c4dc946bab..e1ff5d500826d8 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,6 @@ system/proclogd/proclogd selfdrive/ui/_ui selfdrive/test/longitudinal_maneuvers/out selfdrive/visiond/visiond -selfdrive/loggerd/loggerd -selfdrive/loggerd/bootlog selfdrive/sensord/_gpsd selfdrive/sensord/_sensord system/camerad/camerad diff --git a/Jenkinsfile b/Jenkinsfile index 0fa623fbcd2a24..4e13717851a755 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -115,6 +115,7 @@ pipeline { phone_steps("tici", [ ["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"], ["build openpilot", "cd selfdrive/manager && ./build.py"], + ["check dirty", "release/check-dirty.sh"], ["test manager", "python selfdrive/manager/test/test_manager.py"], ["onroad tests", "cd selfdrive/test/ && ./test_onroad.py"], ["test car interfaces", "cd selfdrive/car/tests/ && ./test_car_interfaces.py"], diff --git a/rednose_repo b/rednose_repo index 225dbacbaac312..3b6bd703b7a766 160000 --- a/rednose_repo +++ b/rednose_repo @@ -1 +1 @@ -Subproject commit 225dbacbaac312f85eaaee0b97a3acc31f9c6b47 +Subproject commit 3b6bd703b7a7667e4f82d0b81ef9a454819b94bd diff --git a/release/check-dirty.sh b/release/check-dirty.sh new file mode 100755 index 00000000000000..9c6389f3801ac6 --- /dev/null +++ b/release/check-dirty.sh @@ -0,0 +1,11 @@ +#!/usr/bin/bash +set -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd $DIR + +if [ ! -z "$(git status --porcelain)" ]; then + echo "Dirty working tree after build:" + git status --porcelain + exit 1 +fi diff --git a/release/files_common b/release/files_common index 260e37e29ac019..fb911705610df3 100644 --- a/release/files_common +++ b/release/files_common @@ -231,6 +231,7 @@ selfdrive/locationd/models/gnss_helpers.py selfdrive/locationd/calibrationd.py +system/logcatd/.gitignore system/logcatd/SConscript system/logcatd/logcatd_systemd.cc @@ -239,6 +240,7 @@ system/proclogd/main.cc system/proclogd/proclog.cc system/proclogd/proclog.h +selfdrive/loggerd/.gitignore selfdrive/loggerd/SConscript selfdrive/loggerd/encoder/encoder.cc selfdrive/loggerd/encoder/encoder.h @@ -414,6 +416,7 @@ scripts/stop_updater.sh pyextra/.gitignore pyextra/acados_template/** +rednose/.gitignore rednose/** laika/** diff --git a/selfdrive/loggerd/.gitignore b/selfdrive/loggerd/.gitignore index 6437be5e38443d..53dc24e6f27894 100644 --- a/selfdrive/loggerd/.gitignore +++ b/selfdrive/loggerd/.gitignore @@ -1,3 +1,4 @@ loggerd encoderd +bootlog tests/test_logger From 8ea982264ec4ba7aa47a3228236f943e76a911c5 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 13:05:25 -0700 Subject: [PATCH 289/436] remove casync from agnos manifest for now --- system/hardware/tici/agnos.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 853d3ab434a07b..7ccea95ee78380 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -47,8 +47,6 @@ "size": 10737418240, "sparse": true, "full_check": false, - "has_ab": true, - "casync_caibx": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13.caibx", - "casync_store": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13" + "has_ab": true } ] From 1d6623c6092e312d03ce4d434077780e2287a010 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 13:18:12 -0700 Subject: [PATCH 290/436] update release notes --- RELEASES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 392e9dc486a17d..7e8f80500eb6d1 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,18 +1,18 @@ -Version 0.8.15 (2022-07-XX) +Version 0.8.15 (2022-07-20) ======================== * New driving model * Path planning uses end-to-end output instead of lane lines at all times * Reduced ping pong * Improved lane centering * New lateral controller based on physical wheel torque model - * Much smoother control, consistent across the speed range + * Much smoother control that's consistent across the speed range * Effective feedforward that uses road roll * Simplified tuning, all car-specific parameters can be derived from data + * Used on select Toyota and Hyundai models at first * Significantly improved control on TSS-P Prius * New driver monitoring model * Takes a larger input frame * Outputs a driver state for both driver and passenger - * Automatically determines which side the driver is on (soon) * Navigation improvements * Speed limits shown while navigating * Faster position fix by using raw GPS measurements @@ -23,8 +23,8 @@ Version 0.8.15 (2022-07-XX) * More consistent camera view perspective across cars * Reduced power usage: device runs cooler and fan spins less * AGNOS 5 - * Support for delta updates to reduce data usage on future OS updates * Support VSCode remote SSH target + * Support for delta updates to reduce data usage on future OS updates * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! From 5f77451aec3345c80f8cf2e5cd15c0ce911d5612 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 8 Jul 2022 13:46:09 -0700 Subject: [PATCH 291/436] FW fingerprinting updates (#25088) * Print brand along with ecu * fix json decoding * fw_versions updates * add timeout handling back * keep logging the same --- selfdrive/car/car_helpers.py | 6 +++--- selfdrive/car/fw_versions.py | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 7f83732153acc1..690072cc4d9ec6 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -79,7 +79,7 @@ def _get_interface_names() -> Dict[str, List[str]]: def fingerprint(logcan, sendcan): fixed_fingerprint = os.environ.get('FINGERPRINT', "") skip_fw_query = os.environ.get('SKIP_FW_QUERY', False) - ecu_responses = set() + ecu_rx_addrs = set() if not fixed_fingerprint and not skip_fw_query: # Vin query only reliably works thorugh OBDII @@ -98,7 +98,7 @@ def fingerprint(logcan, sendcan): else: cloudlog.warning("Getting VIN & FW versions") _, vin = get_vin(logcan, sendcan, bus) - ecu_responses = get_present_ecus(logcan, sendcan) + ecu_rx_addrs = get_present_ecus(logcan, sendcan) car_fw = get_fw_versions(logcan, sendcan) exact_fw_match, fw_candidates = match_fw_to_car(car_fw) @@ -166,7 +166,7 @@ def fingerprint(logcan, sendcan): source = car.CarParams.FingerprintSource.fixed cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, - fw_count=len(car_fw), ecu_responses=ecu_responses, error=True) + fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), error=True) return car_fingerprint, finger, vin, car_fw, source, exact_match diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 03dcece10c85c9..c51d120166f9be 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -290,24 +290,21 @@ def match_fw_to_car(fw_versions, allow_fuzzy=True): versions = get_interface_attr('FW_VERSIONS', ignore_none=True) # Try exact matching first - exact_matches = [True] + exact_matches = [(True, match_fw_to_car_exact)] if allow_fuzzy: - exact_matches.append(False) + exact_matches.append((False, match_fw_to_car_fuzzy)) - for exact_match in exact_matches: + for exact_match, match_func in exact_matches: # For each brand, attempt to fingerprint using FW returned from its queries + matches = set() for brand in versions.keys(): fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand) + matches |= match_func(fw_versions_dict) - if exact_match: - matches = match_fw_to_car_exact(fw_versions_dict) - else: - matches = match_fw_to_car_fuzzy(fw_versions_dict) + if len(matches): + return exact_match, matches - if len(matches) == 1: - return exact_match, matches - - return True, [] + return True, set() def get_present_ecus(logcan, sendcan): @@ -448,9 +445,10 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr print() print("Found FW versions") print("{") + padding = max([len(fw.brand) for fw in fw_vers]) for version in fw_vers: subaddr = None if version.subAddress == 0 else hex(version.subAddress) - print(f" (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") + print(f" Brand: {version.brand:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") print("}") print() From b5f0cb22a5aab4ea94cdc817bf0331919a265bfb Mon Sep 17 00:00:00 2001 From: realfast Date: Fri, 8 Jul 2022 16:36:02 -0500 Subject: [PATCH 292/436] Add Chrysler FPv2 requests and logging (#24460) * Chrylser FPv2 * Update fw_versions.py * formatting and remove default * fix rx offset * move to end * add fw versions * this won't be fingerprinted on as it returns from Mazda * only log FW versions * add type annotation * fix typing * Skip if FW versions are for read/request-only * Fix crash if no fw versions Fix crash if no fw versions Co-authored-by: Shane Smiskol Co-authored-by: Adeeb Shihadeh --- selfdrive/car/chrysler/values.py | 18 +++++++++++++++++- selfdrive/car/fw_versions.py | 21 ++++++++++++++++++++- selfdrive/car/tests/test_fw_fingerprint.py | 2 ++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 40210e68e66985..ada4f486fc06f2 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -1,6 +1,7 @@ +import capnp from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Tuple, Union from cereal import car from selfdrive.car import dbc_dict @@ -110,6 +111,21 @@ class ChryslerCarInfo(CarInfo): ], } +FW_VERSIONS: Dict[str, Dict[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]], List[str]]] = { + CAR.RAM_1500: { + (Ecu.combinationMeter, 0x742, None): [], + (Ecu.srs, 0x744, None): [], + (Ecu.esp, 0x747, None): [], + (Ecu.fwdCamera, 0x753, None): [], + (Ecu.fwdCamera, 0x764, None): [], + (Ecu.eps, 0x761, None): [], + (Ecu.fwdRadar, 0x757, None): [], + (Ecu.eps, 0x75A, None): [], + (Ecu.engine, 0x7e0, None): [], + (Ecu.transmission, 0x7e1, None): [], + (Ecu.gateway, 0x18DACBF1, None): [], + } +} DBC = { CAR.PACIFICA_2017_HYBRID: dbc_dict('chrysler_pacifica_2017_hybrid_generated', 'chrysler_pacifica_2017_hybrid_private_fusion'), diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index c51d120166f9be..04610b96d979df 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -92,6 +92,13 @@ def p16(val): SUBARU_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \ p16(uds.DATA_IDENTIFIER_TYPE.APPLICATION_DATA_IDENTIFICATION) +CHRYSLER_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ + p16(0xf132) +CHRYSLER_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \ + p16(0xf132) + +CHRYSLER_RX_OFFSET = -0x280 + @dataclass class Request: @@ -188,6 +195,18 @@ class Request: [TESTER_PRESENT_RESPONSE, UDS_VERSION_RESPONSE], bus=0, ), + # Chrysler / FCA / Stellantis + Request( + "chrysler", + [CHRYSLER_VERSION_REQUEST], + [CHRYSLER_VERSION_RESPONSE], + rx_offset=CHRYSLER_RX_OFFSET, + ), + Request( + "chrysler", + [CHRYSLER_VERSION_REQUEST], + [CHRYSLER_VERSION_RESPONSE], + ), ] @@ -445,7 +464,7 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr print() print("Found FW versions") print("{") - padding = max([len(fw.brand) for fw in fw_vers]) + padding = max([len(fw.brand) for fw in fw_vers] or [0]) for version in fw_vers: subaddr = None if version.subAddress == 0 else hex(version.subAddress) print(f" Brand: {version.brand:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") diff --git a/selfdrive/car/tests/test_fw_fingerprint.py b/selfdrive/car/tests/test_fw_fingerprint.py index 49fa66d36d476e..cda241c73f049a 100755 --- a/selfdrive/car/tests/test_fw_fingerprint.py +++ b/selfdrive/car/tests/test_fw_fingerprint.py @@ -27,6 +27,8 @@ def test_fw_fingerprint(self, brand, car_model, ecus): for _ in range(200): fw = [] for ecu, fw_versions in ecus.items(): + if not len(fw_versions): + raise unittest.SkipTest("Car model has no FW versions") ecu_name, addr, sub_addr = ecu fw.append({"ecu": ecu_name, "fwVersion": random.choice(fw_versions), 'brand': brand, "address": addr, "subAddress": 0 if sub_addr is None else sub_addr}) From c907a0c28aa0958fbc841019eb401a23432ef897 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 14:42:54 -0700 Subject: [PATCH 293/436] add chrysler fw query to release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 7e8f80500eb6d1..1e9a1b0423a14f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -25,6 +25,7 @@ Version 0.8.15 (2022-07-20) * AGNOS 5 * Support VSCode remote SSH target * Support for delta updates to reduce data usage on future OS updates +* Chrysler ECU firmware fingerprinting thanks to realfast! * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! * Lexus NX Hybrid 2020 support thanks to AlexandreSato! From 76dde007959b633f4140d04e79043c7faef567b9 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Fri, 8 Jul 2022 16:57:50 -0700 Subject: [PATCH 294/436] Update RELEASES.md --- RELEASES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 1e9a1b0423a14f..588b88827a983e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,8 +11,9 @@ Version 0.8.15 (2022-07-20) * Used on select Toyota and Hyundai models at first * Significantly improved control on TSS-P Prius * New driver monitoring model - * Takes a larger input frame - * Outputs a driver state for both driver and passenger + * Bigger model, covering full interior view from driver camera + * Works with a wider variety of mounting angles + * 3x more unique comma three training data than previous * Navigation improvements * Speed limits shown while navigating * Faster position fix by using raw GPS measurements From 4c493237d52525d2effb95b5cda96b36a684303c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 8 Jul 2022 19:36:09 -0700 Subject: [PATCH 295/436] Interleave VIN queries (#25090) Interleave the two requests --- selfdrive/car/vin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index 7413c3f2350564..fd1ca61e665b52 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -18,8 +18,8 @@ def get_vin(logcan, sendcan, bus, timeout=0.1, retry=5, debug=False): - for request, response in ((UDS_VIN_REQUEST, UDS_VIN_RESPONSE), (OBD_VIN_REQUEST, OBD_VIN_RESPONSE)): - for i in range(retry): + for i in range(retry): + for request, response in ((UDS_VIN_REQUEST, UDS_VIN_RESPONSE), (OBD_VIN_REQUEST, OBD_VIN_RESPONSE)): try: query = IsoTpParallelQuery(sendcan, logcan, bus, FUNCTIONAL_ADDRS, [request, ], [response, ], functional_addr=True, debug=debug) for addr, vin in query.get_data(timeout).items(): From 94c8717cac0cfad4603d57a8da108a124019dd73 Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:03:21 -0500 Subject: [PATCH 296/436] Add missing HIGHLANDERH_TSS2 ESP & engine f/w (#25066) * Add missing HIGHLANDERH_TSS2 ESP & engine f/w `@pkozlowski#5214` 2022 Highlander Hybrid (Poland) DongleID/route b2e9858e29db492b|2022-07-07--17-57-24 * Fix test_fw_query_on_routes with older routes Co-authored-by: Shane Smiskol --- selfdrive/car/toyota/values.py | 4 +++- selfdrive/debug/test_fw_query_on_routes.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 8149bfd063fe9e..2a03999342d03d 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -954,11 +954,13 @@ class ToyotaCarInfo(CarInfo): b'\x01F15264873500\x00\x00\x00\x00', b'\x01F152648C6300\x00\x00\x00\x00', b'\x01F152648J4000\x00\x00\x00\x00', + b'\x01F152648J6000\x00\x00\x00\x00', ], (Ecu.engine, 0x700, None): [ + b'\x01896630EE4000\x00\x00\x00\x00', + b'\x01896630EE6000\x00\x00\x00\x00', b'\x01896630E67000\x00\x00\x00\x00', b'\x01896630EA1000\x00\x00\x00\x00', - b'\x01896630EE4000\x00\x00\x00\x00', b'\x01896630EA1000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630E66000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', b'\x02896630EB3000\x00\x00\x00\x00897CF4801001\x00\x00\x00\x00', diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 8c8c631c38fd3c..9ce0ebb3f59ca4 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -17,6 +17,7 @@ VERSIONS = get_interface_attr('FW_VERSIONS', ignore_none=True) SUPPORTED_BRANDS = VERSIONS.keys() SUPPORTED_CARS = [brand for brand in SUPPORTED_BRANDS for brand in interface_names[brand]] +UNKNOWN_BRAND = "unknown" try: from xx.pipeline.c.CarState import migration @@ -126,10 +127,10 @@ print("New style (exact):", exact_matches) print("New style (fuzzy):", fuzzy_matches) - padding = max([len(fw.brand) for fw in car_fw]) + padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw]) for version in sorted(car_fw, key=lambda fw: fw.brand): subaddr = None if version.subAddress == 0 else hex(version.subAddress) - print(f" Brand: {version.brand:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") + print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") print("Mismatches") found = False From 949de4d2b6b293d9f77d83c58212f5dee176cbf1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 8 Jul 2022 20:25:54 -0700 Subject: [PATCH 297/436] UI: Internationalization support (#21212) * rough multiple language demo * more wrappings * stash * add some bad translations * updates * map from french to spanish still has same problem of needing to call setText on everything * add files * restart UI * use return code * relative path * more translations * don't loop restart * Toggle and prime translations * try on device * try QComboBox with readable style * stash * not yet scrollable * stash * dynamic translations (doesn't work for dynamic widget strings yet) * clean up multiple option selector * store languages in json * try transparent * Try transparent popup * see how this looks * tweaks * clean up * update names * Add Chinese (Simplified) translations * Do missing French translations * unit tests caught that :) * fix test * fix other test (on PC) * add entries to dialog to test * add cancel button, clean up a bit * just chinese * some clean up * use quotes * clean up * Just quit, set timeout to 0 * half a second * use exitcode * don't print if it's expected * this comment is outdated * update translations * Update translations * re-order input classes * Update line numbers * use enabled property for button style * Get rid of ListWidget * Update line numbers * Log failed to load language * Log failed to load language * Move to utils and fix english logging extra line * Update translations * spacing * looks a bit better * try this instead of exitcode fixes fix * only one function * comment * Update line numbers * fixup some japanese translations * clean up multi option dialog * Update line numbers --- common/params.cc | 1 + common/watchdog.cc | 5 +- common/watchdog.h | 4 +- selfdrive/ui/main.cc | 9 ++ selfdrive/ui/qt/offroad/settings.cc | 14 ++ selfdrive/ui/qt/util.cc | 16 +++ selfdrive/ui/qt/util.h | 1 + selfdrive/ui/qt/widgets/input.cc | 86 ++++++++++++ selfdrive/ui/qt/widgets/input.h | 9 ++ selfdrive/ui/tests/test_translations.cc | 1 + selfdrive/ui/translations/main_ja.ts | 6 +- selfdrive/ui/translations/main_ko.qm | Bin 19159 -> 19449 bytes selfdrive/ui/translations/main_ko.ts | 167 ++++++++++++----------- selfdrive/ui/translations/main_zh-CHS.qm | Bin 17629 -> 17919 bytes selfdrive/ui/translations/main_zh-CHS.ts | 167 ++++++++++++----------- selfdrive/ui/translations/main_zh-CHT.qm | Bin 17741 -> 18031 bytes selfdrive/ui/translations/main_zh-CHT.ts | 167 ++++++++++++----------- selfdrive/ui/ui.cc | 2 +- selfdrive/ui/update_translations.py | 2 +- 19 files changed, 404 insertions(+), 253 deletions(-) diff --git a/common/params.cc b/common/params.cc index f93c87cd98286a..c4f65a9e02512a 100644 --- a/common/params.cc +++ b/common/params.cc @@ -129,6 +129,7 @@ std::unordered_map keys = { {"IsUpdateAvailable", CLEAR_ON_MANAGER_START}, {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"LaikadEphemeris", PERSISTENT | DONT_LOG}, + {"LanguageSetting", PERSISTENT}, {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, {"LastGPSPosition", PERSISTENT}, {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, diff --git a/common/watchdog.cc b/common/watchdog.cc index 5a10207828d15b..920df4030a9303 100644 --- a/common/watchdog.cc +++ b/common/watchdog.cc @@ -1,12 +1,9 @@ #include "common/watchdog.h" -#include "common/timing.h" #include "common/util.h" const std::string watchdog_fn_prefix = "/dev/shm/wd_"; // + -bool watchdog_kick() { +bool watchdog_kick(uint64_t ts) { static std::string fn = watchdog_fn_prefix + std::to_string(getpid()); - - uint64_t ts = nanos_since_boot(); return util::write_file(fn.c_str(), &ts, sizeof(ts), O_WRONLY | O_CREAT) > 0; } diff --git a/common/watchdog.h b/common/watchdog.h index 7ed23aa0d9ca4e..12dd2ca0355f09 100644 --- a/common/watchdog.h +++ b/common/watchdog.h @@ -1,3 +1,5 @@ #pragma once -bool watchdog_kick(); +#include + +bool watchdog_kick(uint64_t ts); diff --git a/selfdrive/ui/main.cc b/selfdrive/ui/main.cc index 1eecd78b1951e1..ed54d5aa19206f 100644 --- a/selfdrive/ui/main.cc +++ b/selfdrive/ui/main.cc @@ -1,6 +1,7 @@ #include #include +#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/qt_window.h" @@ -13,7 +14,15 @@ int main(int argc, char *argv[]) { qInstallMessageHandler(swagLogMessageHandler); initApp(argc, argv); + QTranslator translator; + QString translation_file = QString::fromStdString(Params().get("LanguageSetting")); + if (!translator.load(translation_file, "translations") && translation_file.length()) { + qCritical() << "Failed to load translation file:" << translation_file; + } + QApplication a(argc, argv); + a.installTranslator(&translator); + MainWindow w; setMainWindow(&w); a.installEventFilter(&w); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index a03af23951b1f8..6bcdd55b0a08ad 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -13,6 +13,7 @@ #endif #include "common/params.h" +#include "common/watchdog.h" #include "common/util.h" #include "system/hardware/hw.h" #include "selfdrive/ui/qt/widgets/controls.h" @@ -133,6 +134,19 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { addItem(regulatoryBtn); } + auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); + connect(translateBtn, &ButtonControl::clicked, [=]() { + QMap langs = getSupportedLanguages(); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), this); + if (!selection.isEmpty()) { + // put language setting, exit Qt UI, and trigger fast restart + Params().put("LanguageSetting", langs[selection].toStdString()); + qApp->exit(18); + watchdog_kick(0); + } + }); + addItem(translateBtn); + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { btn->setEnabled(offroad); diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index cab7299cd68843..a7d5438ae471c1 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -1,6 +1,9 @@ #include "selfdrive/ui/qt/util.h" #include +#include +#include +#include #include #include #include @@ -36,6 +39,19 @@ std::optional getDongleId() { } } +QMap getSupportedLanguages() { + QFile f("translations/languages.json"); + f.open(QIODevice::ReadOnly | QIODevice::Text); + QString val = f.readAll(); + + QJsonObject obj = QJsonDocument::fromJson(val.toUtf8()).object(); + QMap map; + for (auto key : obj.keys()) { + map[key] = obj[key].toString(); + } + return map; +} + void configFont(QPainter &p, const QString &family, int size, const QString &style) { QFont f(family); f.setPixelSize(size); diff --git a/selfdrive/ui/qt/util.h b/selfdrive/ui/qt/util.h index 9491c6798e1819..f0e57526c89088 100644 --- a/selfdrive/ui/qt/util.h +++ b/selfdrive/ui/qt/util.h @@ -14,6 +14,7 @@ QString getBrand(); QString getBrandVersion(); QString getUserAgent(); std::optional getDongleId(); +QMap getSupportedLanguages(); void configFont(QPainter &p, const QString &family, int size, const QString &style); void clearLayout(QLayout* layout); void setQtSurfaceFormat(); diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 755ccfe8c5aa65..a130a8e9351f55 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -1,6 +1,7 @@ #include "selfdrive/ui/qt/widgets/input.h" #include +#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" @@ -257,3 +258,88 @@ bool RichTextDialog::alert(const QString &prompt_text, QWidget *parent) { auto d = RichTextDialog(prompt_text, tr("Ok"), parent); return d.exec(); } + +// MultiOptionDialog + +MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, QStringList l, QWidget *parent) : QDialogBase(parent) { + QFrame *container = new QFrame(this); + container->setStyleSheet(R"( + QFrame { background-color: #1B1B1B; } + #confirm_btn[enabled="false"] { background-color: #2B2B2B; } + #confirm_btn:enabled { background-color: #465BEA; } + #confirm_btn:enabled:pressed { background-color: #3049F4; } + )"); + + QVBoxLayout *main_layout = new QVBoxLayout(container); + main_layout->setContentsMargins(55, 50, 55, 50); + + QLabel *title = new QLabel(prompt_text, this); + title->setStyleSheet("font-size: 70px; font-weight: 500;"); + main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); + main_layout->addSpacing(25); + + QWidget *listWidget = new QWidget(this); + QVBoxLayout *listLayout = new QVBoxLayout(listWidget); + listLayout->setSpacing(20); + listWidget->setStyleSheet(R"( + QPushButton { + height: 135; + padding: 0px 50px; + text-align: left; + font-size: 55px; + font-weight: 300; + border-radius: 10px; + background-color: #4F4F4F; + } + QPushButton:checked { background-color: #465BEA; } + )"); + + QButtonGroup *group = new QButtonGroup(listWidget); + group->setExclusive(true); + + QPushButton *confirm_btn = new QPushButton(tr("Select")); + confirm_btn->setObjectName("confirm_btn"); + confirm_btn->setEnabled(false); + + for (QString &s : l) { + QPushButton *selectionLabel = new QPushButton(s); + selectionLabel->setCheckable(true); + QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) { + if (checked) selection = s; + confirm_btn->setEnabled(true); + }); + + group->addButton(selectionLabel); + listLayout->addWidget(selectionLabel); + } + + ScrollView *scroll_view = new ScrollView(listWidget, this); + scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + main_layout->addWidget(scroll_view); + main_layout->addStretch(1); + main_layout->addSpacing(35); + + // cancel + confirm buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *cancel_btn = new QPushButton(tr("Cancel")); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); + QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); + blayout->addWidget(cancel_btn); + blayout->addWidget(confirm_btn); + + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(50, 50, 50, 50); + outer_layout->addWidget(container); +} + +QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList l, QWidget *parent) { + MultiOptionDialog d = MultiOptionDialog(prompt_text, l, parent); + if (d.exec()) { + return d.selection; + } + return ""; +} diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index f81211d0eed73d..47d8b74efdcc27 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -68,3 +68,12 @@ class RichTextDialog : public QDialogBase { explicit RichTextDialog(const QString &prompt_text, const QString &btn_text, QWidget* parent); static bool alert(const QString &prompt_text, QWidget *parent); }; + +class MultiOptionDialog : public QDialogBase { + Q_OBJECT + +public: + explicit MultiOptionDialog(const QString &prompt_text, const QStringList l, QWidget *parent); + static QString getSelection(const QString &prompt_text, const QStringList l, QWidget *parent); + QString selection; +}; diff --git a/selfdrive/ui/tests/test_translations.cc b/selfdrive/ui/tests/test_translations.cc index fecb9da44a1527..ba0612b4c0fb56 100644 --- a/selfdrive/ui/tests/test_translations.cc +++ b/selfdrive/ui/tests/test_translations.cc @@ -41,6 +41,7 @@ void checkWidgetTrWrap(MainWindow &w) { // Tests all strings in the UI are wrapped with tr() TEST_CASE("UI: test all strings wrapped") { + Params().remove("LanguageSetting"); Params().remove("HardwareSerial"); Params().remove("DongleId"); qputenv("TICI", "1"); diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index fbb654bc4cbb09..5c0f54a3149b6b 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -528,12 +528,12 @@ location set ✓ SUBSCRIBED - ✓ 購読 + ✓ 購読しました comma prime - コンマプライム + comma prime @@ -543,7 +543,7 @@ location set COMMA POINTS - コンマポイント + COMMA POINTS diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm index 62ddb3be2b89c6c73e82458ae86c02825c925de5..f6e95b5038f984183d9d4273c581bb6ef6e4b91d 100644 GIT binary patch delta 1948 zcmZWqX;4#F7(ID;FE4p{c>!6pwrB`JMJwQ1tf2}B3RVzBbX-~&FhVJ63$?BpS6px# zT(F{b0d4En0k=}LF1X_pwkRM?;;ivrxA}5*Awdik?Yp+fD{IFmWaKHLx`goljp&pR{{ALpp^sa z8NiVU#4Z53_5l(W1Fv%Y=1?MYCU*ghvx$=cx8FnlSjYt)l*0-CNzgChA#xbRyo~^U z_Mm>h2OYT{jJ^u7umRB7AXawg{`VnXJWQ;Bcv%G6218nEBhkl@_Ut67`7j*(mbeSD zZ3NGG19GQoxS?J{xV0KE=c9ksOdzxZ{qG+Ddgft(iMRGHMnccEB*KeKi{=3#NXjap zBCliccO>9<1Vd*3#`qgZ5l(VG4=Lm4^57z*?konBSd55Y!vnoB@{?IWOc=%-ui`nc zUykTsk$E4C7%xA{4?E0*E!#NpYBqM$h--X1%%WI!FTF` zZnK0q#hpWDD}~Wsym8lbVNZo8Cfg6>A<=CDd*4 zW5T8SfJZDN}5m$@5FRulHhKc*%DW3^uRVZ1OO{J1aQj8YNi{wZn#@8&qkry)Vkfu-kknv}vncX!u!Fp-gIucPfd$4W12W<;I7_(R^>Qqb*`tFvBR?-`4 zvQ)fiA(gl1h)QhQ}F% zz!ZAybA$Fc6tL|wI3L7O!HI_crAL5Br(u+il?c9W*xvY@3NJELgl?b*O-94hr0evY zOSXqlvXH@YN$dHvz(9Fl_H7c#lphb-MhR!jPj0>hv{2X{l+Zpdz=C-80k_ z^^1!eZkvV;Y#`%uQ|f$5*zbaA)M!fH-eF2R&F%}an^vGDO;Tp6u1TcVZkcMmCj*86 zC8`tOCqurHcI6^(K2}-sAb`)nRAMC1ZjrL=o|WTolr_&;>UIZ|!iw#HZ!e|DeRCMi z>!WPG$l8m6%AUn*DB%O8JX|8Ny~+s}H%ggGrP79`y{c6DJ4vWcxj!L?5~i4S2j?(8 z)f{x}6bTJ7kE!SL;INseUK2@poq6sDzEsd+KK+O#>X2bRJBUj8-co&!@!YVMsynch z3+*1Mog=5yEa%j?7}mP)bTuJj4B-4)9T-goB4?tM_tBf z1M{hvPntG0I+iAGr7hXWw(rgGb${5|W2^lU2BX#Hx+UqGMd z*4q1<*$wlob#Z$+PRih)PRp`#gGu%iD~|0xLSxYq&7YV7QHlLy3#O=kcH^nTr}n?F zYK@sRF58uEdz0U}<6P;@VPz*RrK01X*yFYgC~K^h5yHwZEWof{~HSV10MhY delta 1717 zcmX9;eOQfW9KO#v?|VMp^MyW)9O+buR6=QmIw`^=sv&JgG?|PmOEWtxMoP2M@=<9j z+00xkADanSscg+R=3K;4h{F<^Ck%#Zc_!9;1?a7u-+VHbcWc2pd)qx&j5hTVj) zy#%Ru-*RHS_Xcoo6s5Dl0fqjl2?1|BbS4)O*(qGj(Cc>sf)WTSG~qTyAtm~&o$ zr%bl<2KV1REV~>lFz_^4y}ShIH&@m%Vja+TkX%YW3k+_Pd$u+M{$I%>r8E-Dlh5g~ zfE5p4EHA9n@WMX%(Ht`Ke<81m$_09u<=0~v*c2vjtYNQxuE?8;ofvS7!gOEc`8Nu4 z_Bd9wSg|680Y~pp>^G3G_b^4pk!ifYO>wM(=lz~+T`hODMhSDvE1CARuqcY6_P8o! zG>-)a^bs}){{n&C!uF`+bX2?@ja7DZwFm{Jub4ei*wd0k%ol#Xk_VU*ghPut3w|gbJl{m8`F$r=cTrDynRv$S8erTcHZ+om`H6TZvxW^Wv13@L z__~Y4e5WYoZn1RE5vAdg58(Q%GNdDt6^Y6T6(@iYi!wn*eLWhKC9k@vWRCK<&khnU zSH6zEK_e|unZ2mg;CNM8-%L8qKSp&h^$rQ7s2)w!OTFMhCkeZ#H?P~S@}XKtHZ?MPA86`<#VL$yGE}6k+yZS$DV&k+mDx!&~B-q zK8}v;BNf%M=YpGbVBEqXGv&=HnEvQ?>6K+39CE$h*L0HwMC*gPvN)4h^an=r zzuQ{<(R5<4PH!{sr5DolHVbi(#j3v;LNiFW3{F3maii1FcLOW#m1Qu$3I_su8%8(r zmGPTzn4Ck&dR;VR?cxx71sMwOhO(FKhM!6qIAF5j#0&P`<(}bk=vBbEpHcmo&z|bG z(J_qU8yIYKZtDg{q#7r-FphsuV`_3M74eBMZmD0-UN1N9SddC0`Nor ConfirmationDialog - - + + Ok 확인 - + Cancel 취소 @@ -108,149 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A N/A - + Serial Serial - + Driver Camera 운전자 카메라 - + PREVIEW 미리보기 - + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) 운전자 카메라를 미리 보면서 최적의 운전자 모니터링 경험을 위해 기기 장착 위치를 최적화할수 있습니다. (차량은 반드시 닫아야 합니다) - + Reset Calibration 캘리브레이션 재설정 - + RESET 재설정 - + Are you sure you want to reset calibration? 캘리브레이션을 재설정하시겠습니까? - + Review Training Guide 트레이닝 가이드 다시보기 - + REVIEW 다시보기 - + Review the rules, features, and limitations of openpilot openpilot의 규칙, 기능, 제한 다시보기 - + Are you sure you want to review the training guide? 트레이닝 가이드를 다시보시겠습니까? - + Regulatory 규제 - + VIEW 보기 + Change Language - 언어변경 + 언어변경 + CHANGE - 변경 + 변경 + Select a language - 언어선택 + 언어선택 - + Reboot 재부팅 - + Power Off 전원 종료 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot은 장치를 왼쪽 또는 오른쪽 4° 이내, 위쪽 5° 또는 아래쪽 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. - + Your device is pointed %1° %2 and %3° %4. 사용자의 기기가 %1° %2 및 %3° %4를 가리키고 있습니다. - + down 아래 - + up - + left 왼쪽 - + right 오른쪽 - + Are you sure you want to reboot? 재부팅 하시겠습니까? - + Disengage to Reboot 재부팅 하려면 해제하세요 - + Are you sure you want to power off? 전원을 종료하시겠습니까? - + Disengage to Power Off 전원을 종료하려면 해제하세요 @@ -299,17 +302,17 @@ InputDialog - + Cancel 취소 - + Need at least 최소 필요 - + characters! 문자! @@ -389,12 +392,14 @@ location set MultiOptionDialog + Select - 선택 + 선택 + Cancel - 취소 + 취소 @@ -564,27 +569,27 @@ location set 종료 - + dashcam dashcam - + openpilot openpilot - + %1 minute%2 ago %1 분%2 전 - + %1 hour%2 ago %1 시간%2 전 - + %1 day%2 ago %1 일%2 전 @@ -640,7 +645,7 @@ location set RichTextDialog - + Ok 확인 @@ -648,33 +653,33 @@ location set SettingsWindow - + × × - + Device 장치 - - + + Network 네트워크 - + Toggles 토글 - + Software 소프트웨어 - + Navigation 네비게이션 @@ -913,68 +918,68 @@ location set SoftwarePanel - + Git Branch Git 브렌치 - + Git Commit Git 커밋 - + OS Version OS 버전 - + Version 버전 - + Last Update Check 최신 업데이트 검사 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 이전에 openpilot에서 업데이트를 성공적으로 확인한 시간입니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. - + Check for Update 업데이트 확인 - + CHECKING 검사중 - + Uninstall 삭제 - + UNINSTALL 삭제 - + Are you sure you want to uninstall? 삭제하시겠습니까? - + failed to fetch update 업데이트를 가져올수없습니다 - - + + CHECK 확인 @@ -1062,82 +1067,82 @@ location set TogglesPanel - + Enable openpilot openpilot 사용 - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 어댑티브 크루즈 컨트롤 및 차선 유지 운전자 보조를 위해 openpilot 시스템을 사용하십시오. 이 기능을 사용하려면 항상 주의를 기울여야 합니다. 이 설정을 변경하면 차량 전원이 꺼질 때 적용됩니다. - + Enable Lane Departure Warnings 차선 이탈 경고 사용 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 차량이 50km/h(31mph) 이상의 속도로 주행하는 동안 방향 지시등이 활성화되지 않은 상태에서 감지된 차선 위를 주행할 경우 차선이탈 경고를 사용합니다. - + Enable Right-Hand Drive 우측핸들 사용 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. openpilot이 좌측 교통 규칙을 준수하고 우측 운전석에서 운전자 모니터링을 수행합니다. - + Use Metric System 미터법 사용 - + Display speed in km/h instead of mph. mph가 아닌 km/h로 속도 표시. - + Record and Upload Driver Camera 운전자 카메라 기록 및 업로드 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. - + Disengage On Accelerator Pedal 가속페달 조작시 해제 - + When enabled, pressing the accelerator pedal will disengage openpilot. 활성화된 경우 가속 페달을 누르면 openpilot이 해제됩니다. - + Show ETA in 24h format 24시간 형식으로 ETA 표시 - + Use 24h format instead of am/pm 오전/오후 대신 24시간 형식 사용 - + openpilot Longitudinal Control openpilot Longitudinal Control - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot은 차량'의 레이더를 무력화시키고 가속페달과 브레이크의 제어를 인계받을 것이다. 경고: AEB를 비활성화합니다! diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index be6c51030617f25c54c086e107274a1b0434ea5c..19eccddbc0e919fdadb32a5621f8f7474aded85a 100644 GIT binary patch delta 1961 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 ConfirmationDialog - - + + Ok 好的 - + Cancel 取消 @@ -108,149 +108,152 @@ DevicePanel - + Dongle ID 加密狗 ID - + N/A 不适用 - + Serial 串行 - + Driver Camera 司机摄像头 - + PREVIEW 预习 - + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) 预览面向驾驶员的摄像头,以帮助优化设备安装位置以获得最佳驾驶员监控体验。 (车辆必须关闭) - + Reset Calibration 重置校准 - + RESET 重置 - + Are you sure you want to reset calibration? 您确定要重置校准吗? - + Review Training Guide 查看培训指南 - + REVIEW 重新查看 - + Review the rules, features, and limitations of openpilot 查看 openpilot 的规则、功能和限制 - + Are you sure you want to review the training guide? 您确定要查看培训指南吗? - + Regulatory 监管 - + VIEW 查看 + Change Language - 切换语言 + 切换语言 + CHANGE - 切换 + 切换 + Select a language - 选择语言 + 选择语言 - + Reboot 重启 - + Power Off 关机 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 要求设备安装在左或右 4° 以内,上 5° 或下 8° 以内。 openpilot 会持续校准,很少需要重置。 - + Your device is pointed %1° %2 and %3° %4. 您的设备指向 %1° %2 和 %3° %4。 - + down - + up 向上 - + left 向左 - + right 向右 - + Are you sure you want to reboot? 您确定要重新启动吗? - + Disengage to Reboot 脱离以重新启动 - + Are you sure you want to power off? 您确定要关闭电源吗? - + Disengage to Power Off 脱离以关闭电源 @@ -299,17 +302,17 @@ InputDialog - + Cancel 取消 - + Need at least 需要至少 - + characters! 字符! @@ -389,12 +392,14 @@ location set MultiOptionDialog + Select - 选择 + 选择 + Cancel - 取消 + 取消 @@ -564,27 +569,27 @@ location set 退出 - + dashcam 行车记录器 - + openpilot openpilot - + %1 minute%2 ago %1 分钟%2 前 - + %1 hour%2 ago %1 小时%2 前 - + %1 day%2 ago %1 天%2 前 @@ -640,7 +645,7 @@ location set RichTextDialog - + Ok 好的 @@ -648,33 +653,33 @@ location set SettingsWindow - + × × - + Device 设备 - - + + Network 网络 - + Toggles 切换 - + Software 软件 - + Navigation 导航 @@ -913,68 +918,68 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 操作系统版本 - + Version 版本 - + Last Update Check 最后更新检查 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次 openpilot 成功检查更新的时间。 更新程序仅在汽车关闭时运行。 - + Check for Update 检查更新 - + CHECKING 正在检查 - + Uninstall 卸载 - + UNINSTALL 卸载 - + Are you sure you want to uninstall? 您确定要卸载吗? - + failed to fetch update 未能获取更新 - - + + CHECK 查看 @@ -1062,82 +1067,82 @@ location set TogglesPanel - + Enable openpilot 启用 openpilot - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 使用 openpilot 系统进行自适应巡航控制和车道保持驾驶员辅助。 任何时候都需要您注意使用此功能。 更改此设置在汽车断电时生效。 - + Enable Lane Departure Warnings 启用车道偏离警告 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 当您的车辆在以超过 31 英里/小时(50 公里/小时)的速度行驶时在检测到的车道线上漂移而没有激活转向信号时,接收提醒以返回车道。 - + Enable Right-Hand Drive 启用右舵模式 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. 允许 openpilot 遵守左侧交通惯例并在右侧驾驶座上执行驾驶员监控。 - + Use Metric System 使用公制 - + Display speed in km/h instead of mph. 以公里/小时而不是英里/小时显示速度。 - + Record and Upload Driver Camera 记录和上传司机摄像头 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 从面向驾驶员的摄像头上传数据,帮助改进驾驶员监控算法。 - + Disengage On Accelerator Pedal 踩油门解除 - + When enabled, pressing the accelerator pedal will disengage openpilot. 启用后,踩下油门踏板将解除 openpilot。 - + Show ETA in 24h format 以 24 小时格式显示 ETA - + Use 24h format instead of am/pm 使用 24 小时制代替上午/下午 - + openpilot Longitudinal Control openpilot 纵向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot 将禁用汽车的雷达并接管油门和刹车的控制。 警告:这会禁用 AEB! diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm index 8b055d665e4582f8d1461595d0d88a7eeacfbb49..e64aabecd68c5df555b350ade44bd61fb2ea5cd9 100644 GIT binary patch delta 1961 zcmZWpdr(w$6#jPaz5BR(_bzXhKzS^_fQ1qjAEzkBcRob&t6Io~;rMVfmh zn)P0e(}1=M=${1$cH&xMK5-B6Jz@hO@?0GVND;t*Ys3-2z)0dO+PvVxfKotz3@{e~ z$|)dlA24neVA~2zTtmNT;OVKvbyec6SN`P)Z#MK`F zXmO!X;=;g7E{u5r;^ymsMuq5%qW`lH8$TzOK|Ch{{{E1P0!XwG(%zkb>22t0ejt7U zeZWlKvj+Omi*Uen1(BAEfc!BM>&Y&3BNE#W0#Ch$@y5A;+Z;?BvWY~PNPZkI@I+G9 z23ACi@Auk} z8&~P~y*5F1#cNyY+4Hb1 z+SW2}2CNnQ@3K{X5Q2(FGou(GFNN_2=Lox15+0}*Dr=JI-!2@gq`mDq!P)P1>g~L6 z&f^cZZkf>G$lJlfhKl2E9RNIX#20r~0p60BIf0+utm4wEqk!JOi?7ZlL0y5kTKo-& z`a#@0;V{K#bD_n@g&q%G*gH%tt>_@p0pj-S>BKZ~&$&%Nz)7+Cd1tXIsb8!wZcok^D{zC?Y{0G6!peEkmJmCSILet-5g5=hqHo3fpOAL)N@>cxuH>i@XS3JEp( zMSB4LFGoHs#!@(QDoPXapje0Ye zh@Zn4Tx#GL#T#RPwbP;8IDOJ}wj$CvV9(=HcM_DV@$8(81@}*T#FtQ$%n>^ z&66p*L&g^OMO;Q-%CVtbR61Wd{d^-cPnHXB`Ewt2FiSL+oJFL8Kg*n3iI}(X859@Li zvL}LhZxjszdYC`UCk`z$JA*!^7GllLIATbgxnU%=AZMuFyLK=Wlj^&g8M_};gF51Y zL1s0&m5V9-j5?`=6?1>6=ERMoh;3@&2b}+Yp6b?{W2l8O>ZcWqKPXW>@{k&M0_xdW zW6o3Mvn_^SxH9!M7Oz;&byP2lPupWIplZwb>kJr?W68GPVDDB~);ATf=L;-bGO|f1 z%yM+`U%>DVOH2DUCh)7JHDMp$Q!^=;x2g{53U%J%{0AAsC@e~%`xiexc5>pl4Q`5e zkfHmeXj>8fJ+;IyNXxco1U$|Eyfk|T&3vlHniEBoRwrsuK5g`VIGkd&apxlb4m7nY1`Jb5Zu#%(SdUc8x#+Vt1f_FVwk$=y~ve`zr5V H(P4iBk~aPp delta 1718 zcmX9;X;4#F7(ID;?j9}Z(DM;-5D*+qOy`*?F7(?A7%BntbN(&? z{wILptAMb5K++oC8w`wopSThj-tG0} z<9ayySnJ?`IvgXamjb=EVZ_6eK;Jx!RHgw|Cz7H!kq8sWCCQ#$Fm}!cDlr4&ej@=7 zA52*Og6C_IB3$79Nu*3;cEKx~9A@X#_KrV5!qR`VSj=4ROd_dl?> z{e^>aRpaZ+*MQhVD9xD+^jHC>$5_DU30evlF|eo3YhWl4uu0c>R}c{7qwBYh7KPh% z<1YpSw*Ie=-woBZ)xJaCLux1A+cYD zj1Jk%IILROS*P$qsc^cO%z7sY=MvWe+FhY`JOleA3JultI%K!dxThlnI`zI!B<{<4 zThS0^6t7=2o&lqB^+z=l4z}n^PmQC3WAx>vJRde?!*ZdU!|k>>^=Nqs zi};=StpP*CEBEVJdcoqA$4?pmvDh@^1Yq!%)ac1{dQhrlDOv#ZYn9@)?&O&!rA?*q zk+USbdX6PATbdQLp8}1MX1y&PWeBmA0ZvLwK3dH4hoz-a8Xx#iD*B2Lp% zOI;Z2E$#T6j;l3N$?AM2JU}{rsgX{HK|0?~L4_jeLcmQ5oFLUVkcj^z>0Vwn9sb#c zF-6j=b`lG9mxX{7X84}0JqxE&`{bBM34ok0k0?D0^gl1B>nO0zP2Sh`2Iw|Tt_a@> z7%lRvvA0-B$p%|6mFhmnu;1H}$IOl!j?cSC0^kKdNbOAa98CoAx8T~24 z>p>4#39-ibU?;o(sWD04NGaWn*}uQ0#0|#J7HtE%Tr%!H%6M)&jVD_Ixh_@=7ES`6 zd?j#)kv);94Exj01QliCsAdwruB1BhDBa-EN_qyR4opz8F2<4BP-PuDu^4tMb@gLd zLX}E`+kB3c@6}-;95=GJnsuXwnWw62ANjJo7Z78aak*Obz{>S4b>ka)Y->@sRP1Bz zcT#uUnMkWz)m=4{nb2$X*qV)0FjsYy_mRl#hA(F(mX{eKUFNxC-V(B# zH@}+109EGFFW6K*r_I%Ss90Ep`D#5efYS%*0LeE?~xPKANp99*FSJ z1~hWagx=If6;m>|3~lXpc5#m`+Rg_<>E!|KNC^W+Bx`5?ruW{mc5UblR`)ck@j2(7 zVWQP@7~40}&+2vm4W~%Cb!0Q+gy&l4*;}}_6 ConfirmationDialog - - + + Ok 確定 - + Cancel 取消 @@ -108,149 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A 無法使用 - + Serial 序號 - + Driver Camera 駕駛監控 - + PREVIEW 預覽 - + Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) 預覽駕駛監控鏡頭畫面,方便調整設備安裝的位置,以提供更準確的駕駛監控。(車子必須保持在熄火的狀態) - + Reset Calibration 重置校準 - + RESET 重置 - + Are you sure you want to reset calibration? 您確定要重置校準嗎? - + Review Training Guide 觀看使用教學 - + REVIEW 觀看 - + Review the rules, features, and limitations of openpilot 觀看 openpilot 的使用規則、功能和限制 - + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? - + Regulatory 法規/監管 - + VIEW 觀看 + Change Language - 更改語言 + 更改語言 + CHANGE - 更改 + 更改 + Select a language - 選擇語言 + 選擇語言 - + Reboot 重新啟動 - + Power Off 關機 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以内或朝下偏差 8° 以内。鏡頭在後台會持續自動校準,很少有需要重置的情况。 - + Your device is pointed %1° %2 and %3° %4. 你的設備目前朝%2 %1° 以及朝%4 %3° 。 - + down - + up - + left - + right - + Are you sure you want to reboot? 您確定要重新啟動嗎? - + Disengage to Reboot 請先取消控車才能重新啟動 - + Are you sure you want to power off? 您確定您要關機嗎? - + Disengage to Power Off 請先取消控車才能關機 @@ -299,17 +302,17 @@ InputDialog - + Cancel 取消 - + Need at least 需要至少 - + characters! 個字元! @@ -394,12 +397,14 @@ location set MultiOptionDialog + Select - 選擇 + 選擇 + Cancel - 取消 + 取消 @@ -569,29 +574,29 @@ location set 離開 - + dashcam 行車記錄器 - + openpilot openpilot - + %1 minute%2 ago we don't need %2 %1 分鐘前 - + %1 hour%2 ago we don't need %2 %1 小時前 - + %1 day%2 ago we don't need %2 %1 天前 @@ -648,7 +653,7 @@ location set RichTextDialog - + Ok 確定 @@ -656,33 +661,33 @@ location set SettingsWindow - + × × - + Device 設備 - - + + Network 網路 - + Toggles 設定 - + Software 軟體 - + Navigation 導航 @@ -921,68 +926,68 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 系統版本 - + Version 版本 - + Last Update Check 上次檢查時間 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 - + Check for Update 檢查更新 - + CHECKING 檢查中 - + Uninstall 卸載 - + UNINSTALL 卸載 - + Are you sure you want to uninstall? 您確定您要卸載嗎? - + failed to fetch update 下載更新失敗 - - + + CHECK 檢查 @@ -1070,82 +1075,82 @@ location set TogglesPanel - + Enable openpilot 啟用 openpilot - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。 - + Enable Lane Departure Warnings 啟用車道偏離警告 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 車速在時速 50 公里 (31 英里) 以上且未打方向燈的情況下,如果偵測到車輛駛出目前車道線時,發出車道偏離警告。 - + Enable Right-Hand Drive 啟用右駕模式 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. openpilot 將對右側駕駛進行監控 (但仍遵守靠左駕的交通慣例)。 - + Use Metric System 使用公制單位 - + Display speed in km/h instead of mph. 啟用後,速度單位顯示將從 mp/h 改為 km/h。 - + Record and Upload Driver Camera 記錄並上傳駕駛監控影像 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 - + Disengage On Accelerator Pedal 油門取消控車 - + When enabled, pressing the accelerator pedal will disengage openpilot. 啟用後,踩踏油門將會取消 openpilot 控制。 - + Show ETA in 24h format 預計到達時間單位改用 24 小時制 - + Use 24h format instead of am/pm 使用 24 小時制。(預設值為 12 小時制) - + openpilot Longitudinal Control openpilot 縱向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot 將會關閉雷達訊號並接管油門和剎車的控制。注意:這也會關閉自動緊急煞車 (AEB) 系統! diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 6fe1d838ed1f08..7922714c17d3c6 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -246,7 +246,7 @@ void UIState::update() { updateStatus(); if (sm->frame % UI_FREQ == 0) { - watchdog_kick(); + watchdog_kick(nanos_since_boot()); } emit uiUpdate(*this); } diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index 263eb5e6703a4c..d872be0d86c522 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import argparse -import os import json +import os from common.basedir import BASEDIR From cbff8fcbd02b262860b3540a3dba7108237d2b46 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 8 Jul 2022 21:17:00 -0700 Subject: [PATCH 298/436] Nav: wrap strings (#25089) * Wrap nav strings and translate * Update QM * Update QM --- selfdrive/ui/qt/maps/map.cc | 22 ++++---- selfdrive/ui/translations/main_ko.qm | Bin 19449 -> 19997 bytes selfdrive/ui/translations/main_ko.ts | 64 +++++++++++++++++++++++ selfdrive/ui/translations/main_zh-CHS.qm | Bin 17919 -> 18457 bytes selfdrive/ui/translations/main_zh-CHS.ts | 64 +++++++++++++++++++++++ selfdrive/ui/translations/main_zh-CHT.qm | Bin 18031 -> 18569 bytes selfdrive/ui/translations/main_zh-CHT.ts | 64 +++++++++++++++++++++++ 7 files changed, 203 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc index fd47f4188f2402..a486110a736d11 100644 --- a/selfdrive/ui/qt/maps/map.cc +++ b/selfdrive/ui/qt/maps/map.cc @@ -175,7 +175,7 @@ void MapWindow::updateState(const UIState &s) { loaded_once = loaded_once || m_map->isFullyLoaded(); if (!loaded_once) { - map_instructions->showError("Map Loading"); + map_instructions->showError(tr("Map Loading")); return; } @@ -192,7 +192,7 @@ void MapWindow::updateState(const UIState &s) { carPosSource["data"] = QVariant::fromValue(feature1); m_map->updateSource("carPosSource", carPosSource); } else { - map_instructions->showError("Waiting for GPS"); + map_instructions->showError(tr("Waiting for GPS")); } if (pan_counter == 0) { @@ -418,10 +418,10 @@ void MapInstructions::updateDistance(float d) { if (uiState()->scene.is_metric) { if (d > 500) { distance_str.setNum(d / 1000, 'f', 1); - distance_str += " km"; + distance_str += tr(" km"); } else { distance_str.setNum(50 * int(d / 50)); - distance_str += " m"; + distance_str += tr(" m"); } } else { float miles = d * METER_TO_MILE; @@ -429,10 +429,10 @@ void MapInstructions::updateDistance(float d) { if (feet > 500) { distance_str.setNum(miles, 'f', 1); - distance_str += " mi"; + distance_str += tr(" mi"); } else { distance_str.setNum(50 * int(feet / 50)); - distance_str += " ft"; + distance_str += tr(" ft"); } } @@ -615,7 +615,7 @@ void MapETA::updateETA(float s, float s_typical, float d) { auto eta_time = QDateTime::currentDateTime().addSecs(s).time(); if (params.getBool("NavSettingTime24h")) { eta->setText(eta_time.toString("HH:mm")); - eta_unit->setText("eta"); + eta_unit->setText(tr("eta")); } else { auto t = eta_time.toString("h:mm a").split(' '); eta->setText(t[0]); @@ -625,11 +625,11 @@ void MapETA::updateETA(float s, float s_typical, float d) { // Remaining time if (s < 3600) { time->setText(QString::number(int(s / 60))); - time_unit->setText("min"); + time_unit->setText(tr("min")); } else { int hours = int(s) / 3600; time->setText(QString::number(hours) + ":" + QString::number(int((s - hours * 3600) / 60)).rightJustified(2, '0')); - time_unit->setText("hr"); + time_unit->setText(tr("hr")); } QString color; @@ -649,10 +649,10 @@ void MapETA::updateETA(float s, float s_typical, float d) { float num = 0; if (uiState()->scene.is_metric) { num = d / 1000.0; - distance_unit->setText("km"); + distance_unit->setText(tr("km")); } else { num = d * METER_TO_MILE; - distance_unit->setText("mi"); + distance_unit->setText(tr("mi")); } distance_str.setNum(num, 'f', num < 100 ? 1 : 0); diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm index f6e95b5038f984183d9d4273c581bb6ef6e4b91d..60966cdde587ebf4a43deb8ad255f374d32ca7c8 100644 GIT binary patch delta 2328 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 WBvzGk6d diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 91685383f77d9b..ad35a37fa12283 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -340,6 +340,57 @@ 파일갱신: + + MapETA + + + eta + 에타 + + + + min + + + + + hr + 시간 + + + + km + km + + + + mi + mi + + + + MapInstructions + + + km + km + + + + m + m + + + + mi + mi + + + + ft + ft + + MapPanel @@ -389,6 +440,19 @@ location set 최근 경로 없음 + + MapWindow + + + Map Loading + 지도 로딩 + + + + Waiting for GPS + GPS를 기다리는 중 + + MultiOptionDialog diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 19eccddbc0e919fdadb32a5621f8f7474aded85a..b96acb89d99ed8207dd34740c51714706e771040 100644 GIT binary patch delta 2290 zcmZuxeNh6su&m3bEY$c`E+nrLX6ZtQX>7bVeC*}7}GBrp~nGj0r4ua6d0ip-zG)? zBJa&TfD{RYO(%{c)(~@OF3F5j-fJTPc63vGWL2Ubof%*Tm2n4K$ z^a@o99}Q{8Ho%kv-5x$}Dx-MWd?*6?FeYMZK$J?9T}WzOO8b5!_3Q>l-@qfr*-Xsk zMoLT-nJ}?ZN^-1&8&;H_R0ii)}URK&cHEewC`I;JgijP zxF-!z-_?HHNWaLh*DMzT+&*i>S=*ZU{+~r>GSyYP#3fzvfXyi`n@v8tQgNmDPlkyX z*Cp>~uMYE|<*WyNU-ID4da=6x7BI3(+EQWB@#NL-;f8mu*p;x0y*X2oW2pG}?UK22J_XE`CRswryIIPf#fpqMD;3BG*(ztG zxxrg0)G2B1KuZ~2#GCZLE-lGl%)HIg(r623WWH4SClWDk_F%vP4-Wm#g9-Db>acn@ zg=>_mUuR30MoINAms5$aq@AB$pg=E3hx;hFP%a$_Jj0f%kvcj_$bM3~Sk}r)r+P5{ zOR2Yy1S6K|guqO;&RaUmwFoNct&6)dg$lOok{S;H5BckIG!#C#Ot~RPS_-&G98dpnx$?5g^CWp~KT93rkk{?s zLPF;A%h(HCN0I54)K{sT&tS{^#L4u#WvO_F(>}b=vhnf+miD&gy?VYA zz1wo&NA`mMOO{g;J_j848C1g$T$K8^RNsl5?=cms-#7ih?_<V^{SsdcbiJ(tDQYHlzg6gVakWVxYJHH&eu&{I-8eObR5afWP`Ca z_|wjDA@-ziBMe z_#XPUJM@3kl3_z|Zz@`wCzoGZH}&~72y+aMsI2~s+Sixg&CEoywy7n#dCua30rE!v wcQ}jlN=on0R+c6|T~0PhNW=8IPO2l<>7s)zzhr?eDQ)`yJ2utb|5W&Y0j3r(R{#J2 delta 1749 zcmX9;XHZmE7(M&;y)AFwzC{INVUeXNg4jS&!2%H}Dq_$OODsW&L`|5%HflDCh>A5r z1jLRKTbu#GGN`DCqGE|Uk`Nq%1$8u7Vsua-hhhJmz3<-pedjyh`EK@RO-s7Q9&|Fk z)76ID%u1W{x#@e()Pw-KD}c}h^w0s?1Y!^3B4Q+Q8*wfma^L(Eka`2&TZlu5e-S71 z%$IHG8Vl%4dG2#SxeVB510z=felvi`HH`Bo;5GUSUUUay`tooE@ms*rnV8D&OqATZ z&f4N~#^crH;BS0fVEc0iW7KoK6c{&{8O%ZUu4{4n%&@&0r(R{!(8M@*J z#1!aV;}~r&^gegFK8QXRqMVFT6-xo1AdGri1oZ2VaN|tC!5NVQGST{rToeL3dK4jg z?pD@QigACFK!Q2e-tEt4Rm)vS%+vAl7q+V7H~gN({Yfm9dH||>9o@W z@VKN2*vN+XJa`1E7%;%1J*<+kcagTBcpL*9(4H>fc|YscO2Nh6euy|d zzmRzw#5oa^)lw%esT&Hoxp^Ph zu{+}4NAaX(5|7->1YDPk$G^*?kcr|cg$*%hir1c0^Lw>;?fF0KphLJ=H|#i#HdT@b zQ0XC0By)Nah1(^Cs-EQCEX7S{C;T0x1oQ5e827fJ^T#%H@oB^0LsG6!9y{3nm6W@QRxtII@>Zt;PR`P?%e6qS zv(kkoDlMc+7j3s`rJYiB4GGzd(!-Prc6evU1Rh;TA@bo=1s}Gpx@smg%!@z zADjP>1V-x{#_y$soAmFhI&%nC>fb-7LfRwxk3;H#F7FMY-enxRjfO}qiCg9vX1)AK z!U2X23v#%hYS?v{@!D$)MUUONjx_4c)9B^yMt6Iz0qEOe9PuWB7iJhIL_MOE&c=x; zlyGR3aq<*O?*7RbUmgN@Rv95fPx2b)$bbmYBvrt%kTk!NSql`&MRgRA0L%Dg_=cEwh}4fk@TSMU;=<+>6U%3cp{ zQ6dAU0s|YBs1Pa;n4wI}WI*d!W!~6ULSHG{8u_I9Bq#-2IBd?-l#1O{tk)~$dNt8^ zrBYo^V&ZDEZodZ;jxqPP-Y1bk=0QzsIjk4WNBz0q&3@B-ViErg3^$j#9iSJi=CTl? z#~bt2AbLUWuR0yvPv!^JE@`Z|-3rz1LnxIwr3TjeGT|mQYCRQeSE?q3jO6(b>e}s` za<7Bxj{4zj=_mD29^?0WtDgCf9_Ui4-WYz1CJ(b1Uh!Rw*8gmA9KpHnf7#OUNi*M> zWJ~xX2J9VRnV;~O$_%t@u1aOkXIrvo&8I?*mUBORqC$C=nx{KC$A>JnVZU*0o&yZC Y77o_CIusUk$`iK6%bg4V=sMi@e-;zY9{>OV diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 1bcb30142c6b79..c3e1954f1e9479 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -340,6 +340,57 @@ 更新文件: + + MapETA + + + eta + 埃塔 + + + + min + 分钟 + + + + hr + 小时 + + + + km + km + + + + mi + mi + + + + MapInstructions + + + km + km + + + + m + m + + + + mi + mi + + + + ft + ft + + MapPanel @@ -389,6 +440,19 @@ location set 没有最近的目的地 + + MapWindow + + + Map Loading + 地图加载 + + + + Waiting for GPS + 等待 GPS + + MultiOptionDialog diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm index e64aabecd68c5df555b350ade44bd61fb2ea5cd9..208f29c0ec9dd45424301c70cdaf64c2e9a901bc 100644 GIT binary patch delta 2307 zcmZuxdsLKl8h+-x%*^-A?8i3by};+e-a zz{z`GC&XgL)*peic@KbgxE}lwOWg;-JKY!^2x-@KKxc*2Jdpv1LTWuu%!G770{lOR zyoD-_ErWdc0ASh%ef?G9`xGyOi4tK5V>(G95#VH+;%CT(S}Bd+RF&Wz^yx4PYzcpjSQYZl-PhiueOy^p)4;tfhUnyndpUZ{!2H-p%_>#y1Wzo>!Uiy{=$>&r}Q+ZV3Mv zCD>JAd<}au7>?o zUQt~YSDseR|s_FlZF9)5KPE1jU8h>%GvM{UZbqdZ7$R{gnV8r5dsC=z-{?Ox(! zqOVQ5`WN|{51N8cp5~17m=<($D+I@yR(vW^$t=^xHQx05(RAi3wrFIS=`T-HDW4%` z?~}|s?oTdrU@b2~|85>PbsdN0j(J8Ldp*_1oH%6xd*)%D6-xykZ!pg-XTX4G&H1zY z2<4i0-c4tKjpo{Id~E)!%q>+^Ec~|luWdy8Zgbli5|e&!(bt49VVou0?=p!@wM6z7 z^Uhs4Mp0`?u zZD!?O7HiO*I4X3(I;Df_DEu|+tWqlHHQTx(b~=aIZY|!!*FNf!b??n-oQY}Hx2u_F zl45QA4`*R`llAXv=XN@-VIs=ZMpgD58s8F2t zyDLTP{Zj4qYzK*r)jnGO6A*PmYwxb)dp@dl%y^gI0eKvpq`FT15)W7Usf;wv7bk;1 zG26M=(D(o8#mlFs#pp!-m*#6ZvHcdp(v`n3z%t}NG+37ZOM`smJv7MA8(`pa=SbD> z7}9Sc=I1#Em^`r1!-gC;^>tm-!&$Vs%hN&Sj(8Vh9T z0sL(b`2UJ&pG34AP>N`C*Yf|yb0al5Aa*L^?qw`2bY71Am0 delta 1749 zcmX9;c~p&Q9DeS1zwa*J{qA?8g{Y?6f^=OWghDln%&6=$vd6X0bPkhk=1zxfS;k;& zAqrz`N60cNOQKAegOQVHEXg(vGe{<9o=*3l=f2 zY!mus0{XLnc?Fp_k{s%J{!V63xLEP9k1#k-|rt^CdFuQS_ zyY{7z(|QZy(yc(mScsb*0U8xzQ7Bi9fLL~tm<92g2zYx#TH#ANWsnZ;1x#z9%ezNB z1-}R+y?G9PsGmBt3dBv$eld~XgeQ8uJM3ZH6E{+!}&uRj~+HB;I~H8a(e&} z=&tFynuc`r(j*oK0sfsdTW;}t_gKxxJa?@3W1Lw zaNk`aMxIS#cZ4Jtc5J^OY`baX`UAp=l_VDOsZcP0rd#F;HxgObD_W>5iJ-H#%|ca{ zI|~+QyjL(F%?E7cVYp1*kI*B%MlhmJVO40y@-7 z=NqWBkRe^L{>~^>NfnhOWKEPFE-In(H<~aaU8-*&!48T}unuLXy>)7>oeFN#g+Gm< zg4Md%+M!uy+a*>xX*Pm*+J?9R<(K1)|0e~TH5)%yievNn_TyIU<{hx_!0 zXFVi=1byw`T`c@T|EAoN78U5C4M$@xF%D^Zp5LT}Y-@t<2o0OS@8VNa+jrAi~pis$O%gg3y9brkOi>-zJd=bB~7Qv_NY<7|MBHXAAR*RQ~CnWiImD!z@Iai=v2u zQRdQcWViy-4%77;Oc^0=w-s@0Li%0cGzB9)x z{U5Pl#}vyf$76bxX8EB!ot{szY@RfWglv|=um1yj)>$fRwzGrRma3RT9FLyLz^ppj XQ$NvV-R)TOQNeSCaP)2Kb+-QjngYwa diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 2aff3334d9ff99..f6e36081fac730 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -340,6 +340,57 @@ 更新檔案: + + MapETA + + + eta + 埃塔 + + + + min + 分鐘 + + + + hr + 小時 + + + + km + km + + + + mi + mi + + + + MapInstructions + + + km + km + + + + m + m + + + + mi + mi + + + + ft + ft + + MapPanel @@ -394,6 +445,19 @@ location set 沒有最近的導航記錄 + + MapWindow + + + Map Loading + 地圖加載 + + + + Waiting for GPS + 等待 GPS + + MultiOptionDialog From f261b8a8c29fb2c9c5fee3b382e12e7887d50269 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 22:09:37 -0700 Subject: [PATCH 299/436] EV6: supress LFA (#25094) * EV6: supress LFA * bump panda --- opendbc | 2 +- panda | 2 +- selfdrive/car/hyundai/carcontroller.py | 3 +++ selfdrive/car/hyundai/carstate.py | 5 ++++- selfdrive/car/hyundai/hda2can.py | 5 +++++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/opendbc b/opendbc index 1e9693ce0916b8..9fc90a9f5816ed 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 1e9693ce0916b896568dcd5558a670e67843c299 +Subproject commit 9fc90a9f5816ed82e0f25f2eaf7ad4af3d45733e diff --git a/panda b/panda index 53466f09344c8f..ca927fe9312651 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 53466f09344c8ff6cdce3b19df76b5bca79e1327 +Subproject commit ca927fe9312651a16f13aaddca8b46af5315ede6 diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 73635375ad5810..a878ad3274c694 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -75,6 +75,9 @@ def update(self, CC, CS): # steering control can_sends.append(hda2can.create_lkas(self.packer, CC.enabled, self.frame, CC.latActive, apply_steer)) + if self.frame % 5 == 0: + can_sends.append(hda2can.create_cam_0x2a4(self.packer, self.frame, CS.cam_0x2a4)) + # cruise cancel if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: if CC.cruiseControl.cancel: diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 6c82c3385686c4..a10cdadbca75d6 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -172,6 +172,7 @@ def update_hda2(self, cp, cp_cam): ret.cruiseState.speed = cp.vl["CRUISE_INFO"]["SET_SPEED"] * speed_factor self.buttons_counter = cp.vl["CRUISE_BUTTONS"]["_COUNTER"] + self.cam_0x2a4 = copy.copy(cp_cam.vl["CAM_0x2a4"]) return ret @@ -313,7 +314,9 @@ def get_can_parser(CP): @staticmethod def get_cam_can_parser(CP): if CP.carFingerprint in HDA2_CAR: - return None + signals = [(f"BYTE{i}", "CAM_0x2a4") for i in range(3, 24)] + checks = [("CAM_0x2a4", 20)] + return CANParser(DBC[CP.carFingerprint]["pt"], signals, checks, 6) signals = [ # signal_name, signal_address diff --git a/selfdrive/car/hyundai/hda2can.py b/selfdrive/car/hyundai/hda2can.py index e4c658c1a9f133..437f5cf5388334 100644 --- a/selfdrive/car/hyundai/hda2can.py +++ b/selfdrive/car/hyundai/hda2can.py @@ -12,6 +12,11 @@ def create_lkas(packer, enabled, frame, lat_active, apply_steer): } return packer.make_can_msg("LKAS", 4, values, frame % 255) +def create_cam_0x2a4(packer, frame, camera_values): + camera_values.update({ + "BYTE7": 0, + }) + return packer.make_can_msg("CAM_0x2a4", 4, camera_values, frame % 255) def create_buttons(packer, cnt, cancel, resume): values = { From 825acfae98543c915c18d3b19a9c5d2503e431a6 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 22:09:58 -0700 Subject: [PATCH 300/436] Improve EV6 tune (#25085) --- selfdrive/car/torque_data/override.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index be81af260641bd..476313df2b8461 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -20,7 +20,7 @@ FORD FOCUS 4TH GEN: [.nan, 1.5, .nan] COMMA BODY: [.nan, 1000, .nan] # Totally new cars -KIA EV6 2022: [3.0, 2.5, 0.0] +KIA EV6 2022: [3.5, 2.5, 0.0] RAM 1500 5TH GEN: [2.0, 2.0, 0.05] # Dashcam or fallback configured as ideal car From d08a23177495c778fe628dc97728f5bbfee8b0f1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 8 Jul 2022 22:46:20 -0700 Subject: [PATCH 301/436] Ship EV6 (#25095) * Ship EV6 * bump opendbc --- RELEASES.md | 1 + docs/CARS.md | 3 ++- opendbc | 2 +- selfdrive/car/hyundai/interface.py | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/process_replay/test_processes.py | 1 + 6 files changed, 7 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 588b88827a983e..b87bd2ee7d1d15 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -29,6 +29,7 @@ Version 0.8.15 (2022-07-20) * Chrysler ECU firmware fingerprinting thanks to realfast! * Honda Civic 2022 support * Hyundai Tucson 2021 support thanks to bluesforte! +* Kia EV6 2022 support * Lexus NX Hybrid 2020 support thanks to AlexandreSato! * Ram 1500 2019-21 support thanks to realfast! diff --git a/docs/CARS.md b/docs/CARS.md index a1e89efe121b95..eb570faa7ec4ce 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -71,7 +71,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 68 cars +# Silver - 69 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -95,6 +95,7 @@ How We Rate The Cars |Hyundai|Santa Fe Plug-in Hybrid 2022|All|||||| |Hyundai|Tucson Diesel 2019|SCC + LKAS|||||| |Kia|Ceed 2019|SCC + LKAS|||||| +|Kia|EV6 2022|All|||||| |Kia|Forte 2018|SCC + LKAS|||||| |Kia|Forte 2019-21|SCC + LKAS|||||| |Kia|K5 2021-22|SCC|||||| diff --git a/opendbc b/opendbc index 9fc90a9f5816ed..81148db67fd00d 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 9fc90a9f5816ed82e0f25f2eaf7ad4af3d45733e +Subproject commit 81148db67fd00d4e2a107b5b8269c532436edf2b diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 97119c77b770c1..069b0e74e54d7e 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -36,7 +36,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl # These cars have been put into dashcam only due to both a lack of users and test coverage. # These cars likely still work fine. Once a user confirms each car works and a test route is # added to selfdrive/car/tests/routes.py, we can remove it from this list. - ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} or candidate in HDA2_CAR + ret.dashcamOnly = candidate in {CAR.KIA_OPTIMA_H, CAR.ELANTRA_GT_I30} ret.steerActuatorDelay = 0.1 # Default delay ret.steerLimitTimer = 0.4 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 0d61dbd735b048..65ecbb4be37130 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -dab90772097a0dd4706677ba4fe5e84b10232099 \ No newline at end of file +825acfae98543c915c18d3b19a9c5d2503e431a6 \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 96d1014004deb6..652c49db3d0b4d 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -18,6 +18,7 @@ original_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA + ("HYUNDAI", "d824e27e8c60172c|2022-07-08--21-21-15--1"), # HYUNDAI.KIA_EV6 ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS (INDI) ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4 (LQR) ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2 From 89d1d9f6df6e1fbe65609fa8f93702479620a50e Mon Sep 17 00:00:00 2001 From: Greg Hogan Date: Sat, 9 Jul 2022 00:55:40 -0700 Subject: [PATCH 302/436] firmware fingerprinting: order brand requests (#23311) Co-authored-by: Shane Smiskol --- selfdrive/car/car_helpers.py | 4 +- selfdrive/car/fw_versions.py | 75 ++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 690072cc4d9ec6..b6bdece676a94f 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -8,7 +8,7 @@ from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from selfdrive.car.vin import get_vin, VIN_UNKNOWN -from selfdrive.car.fw_versions import get_fw_versions, match_fw_to_car, get_present_ecus +from selfdrive.car.fw_versions import get_fw_versions_ordered, match_fw_to_car, get_present_ecus from system.swaglog import cloudlog import cereal.messaging as messaging from selfdrive.car import gen_empty_fingerprint @@ -99,7 +99,7 @@ def fingerprint(logcan, sendcan): cloudlog.warning("Getting VIN & FW versions") _, vin = get_vin(logcan, sendcan, bus) ecu_rx_addrs = get_present_ecus(logcan, sendcan) - car_fw = get_fw_versions(logcan, sendcan) + car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs) exact_fw_match, fw_candidates = match_fw_to_car(car_fw) else: diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 04610b96d979df..c4b158aebb1461 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -225,6 +225,15 @@ def build_fw_dict(fw_versions, filter_brand=None): return fw_versions_dict +def get_brand_addrs(): + versions = get_interface_attr('FW_VERSIONS', ignore_none=True) + brand_addrs = defaultdict(set) + for brand, cars in versions.items(): + for fw in cars.values(): + brand_addrs[brand] |= {(addr, sub_addr) for _, addr, sub_addr in fw.keys()} + return brand_addrs + + def match_fw_to_car_fuzzy(fw_versions_dict, log=True, exclude=None): """Do a fuzzy FW match. This function will return a match, and the number of firmware version that were matched uniquely to that specific car. If multiple ECUs uniquely match to different cars @@ -236,7 +245,7 @@ def match_fw_to_car_fuzzy(fw_versions_dict, log=True, exclude=None): # time and only one is in our database. exclude_types = [Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps, Ecu.debug] - # Build lookup table from (addr, subaddr, fw) to list of candidate cars + # Build lookup table from (addr, sub_addr, fw) to list of candidate cars all_fw_versions = defaultdict(list) for candidate, fw_by_addr in FW_VERSIONS.items(): if candidate == exclude: @@ -361,24 +370,59 @@ def get_present_ecus(logcan, sendcan): return ecu_responses -def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progress=False): - ecu_types = {} +def get_brand_ecu_matches(ecu_rx_addrs): + """Returns dictionary of brands and matches with ECUs in their FW versions""" - # Extract ECU addresses to query from fingerprints - # ECUs using a subaddress need be queried one by one, the rest can be done in parallel - addrs = [] - parallel_addrs = [] + brand_addrs = get_brand_addrs() + brand_matches = {r.brand: set() for r in REQUESTS} + + brand_rx_offsets = set((r.brand, r.rx_offset) for r in REQUESTS) + for addr, sub_addr, _ in ecu_rx_addrs: + # Since we can't know what request an ecu responded to, add matches for all possible rx offsets + for brand, rx_offset in brand_rx_offsets: + a = (uds.get_rx_addr_for_tx_addr(addr, -rx_offset), sub_addr) + if a in brand_addrs[brand]: + brand_matches[brand].add(a) + + return brand_matches + + +def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=False, progress=False): + """Queries for FW versions ordering brands by likelihood, breaks when exact match is found""" + all_car_fw = [] + brand_matches = get_brand_ecu_matches(ecu_rx_addrs) + + for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True): + car_fw = get_fw_versions(logcan, sendcan, brand=brand, timeout=timeout, debug=debug, progress=progress) + all_car_fw.extend(car_fw) + matches = match_fw_to_car_exact(build_fw_dict(car_fw)) + if len(matches) == 1: + break + + return all_car_fw + + +def get_fw_versions(logcan, sendcan, brand=None, extra=None, timeout=0.1, debug=False, progress=False): versions = get_interface_attr('FW_VERSIONS', ignore_none=True) + if brand is not None: + versions = {brand: versions[brand]} + if extra is not None: versions.update(extra) + # Extract ECU addresses to query from fingerprints + # ECUs using a subaddress need be queried one by one, the rest can be done in parallel + addrs = [] + parallel_addrs = [] + ecu_types = {} + for brand, brand_versions in versions.items(): for c in brand_versions.values(): for ecu_type, addr, sub_addr in c.keys(): a = (brand, addr, sub_addr) if a not in ecu_types: - ecu_types[(addr, sub_addr)] = ecu_type + ecu_types[a] = ecu_type if sub_addr is None: if a not in parallel_addrs: @@ -390,17 +434,17 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr addrs.insert(0, parallel_addrs) fw_versions = {} - for i, addr in enumerate(tqdm(addrs, disable=not progress)): + requests = [r for r in REQUESTS if brand is None or r.brand == brand] + for addr in tqdm(addrs, disable=not progress): for addr_chunk in chunks(addr): - for r in REQUESTS: + for r in requests: try: addrs = [(a, s) for (b, a, s) in addr_chunk if b in (r.brand, 'any') and - (len(r.whitelist_ecus) == 0 or ecu_types[(a, s)] in r.whitelist_ecus)] + (len(r.whitelist_ecus) == 0 or ecu_types[(b, a, s)] in r.whitelist_ecus)] if addrs: query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) - t = 2 * timeout if i == 0 else timeout - fw_versions.update({(r.brand, addr): (version, r) for addr, version in query.get_data(t).items()}) + fw_versions.update({(r.brand, addr): (version, r) for addr, version in query.get_data(timeout).items()}) except Exception: cloudlog.warning(f"FW query exception: {traceback.format_exc()}") @@ -409,7 +453,7 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr for (brand, addr), (version, request) in fw_versions.items(): f = car.CarParams.CarFw.new_message() - f.ecu = ecu_types[addr] + f.ecu = ecu_types[(brand, addr[0], addr[1])] f.fwVersion = version f.address = addr[0] f.responseAddress = uds.get_rx_addr_for_tx_addr(addr[0], request.rx_offset) @@ -433,6 +477,7 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr parser = argparse.ArgumentParser(description='Get firmware version of ECUs') parser.add_argument('--scan', action='store_true') parser.add_argument('--debug', action='store_true') + parser.add_argument('--brand', help='Only query addresses/with requests for this brand') args = parser.parse_args() logcan = messaging.sub_sock('can') @@ -458,7 +503,7 @@ def get_fw_versions(logcan, sendcan, extra=None, timeout=0.1, debug=False, progr print() t = time.time() - fw_vers = get_fw_versions(logcan, sendcan, extra=extra, debug=args.debug, progress=True) + fw_vers = get_fw_versions(logcan, sendcan, brand=args.brand, extra=extra, debug=args.debug, progress=True) _, candidates = match_fw_to_car(fw_vers) print() From eb17291ca13035b40d653da107653d4420517aaa Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Sat, 9 Jul 2022 16:47:10 +0800 Subject: [PATCH 303/436] Display the current language in MultiOptionDialog (#25098) * check the selected language in lange select dialog * disable if user selects current option * update line numbers Co-authored-by: Shane Smiskol --- selfdrive/ui/qt/offroad/settings.cc | 3 +- selfdrive/ui/qt/widgets/input.cc | 15 +++-- selfdrive/ui/qt/widgets/input.h | 4 +- selfdrive/ui/translations/main_ko.ts | 70 ++++++++++++------------ selfdrive/ui/translations/main_zh-CHS.ts | 70 ++++++++++++------------ selfdrive/ui/translations/main_zh-CHT.ts | 70 ++++++++++++------------ 6 files changed, 119 insertions(+), 113 deletions(-) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 6bcdd55b0a08ad..d5b8d4bbd132c7 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -137,7 +137,8 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { auto translateBtn = new ButtonControl(tr("Change Language"), tr("CHANGE"), ""); connect(translateBtn, &ButtonControl::clicked, [=]() { QMap langs = getSupportedLanguages(); - QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), this); + QString currentLang = QString::fromStdString(Params().get("LanguageSetting")); + QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), langs.key(currentLang), this); if (!selection.isEmpty()) { // put language setting, exit Qt UI, and trigger fast restart Params().put("LanguageSetting", langs[selection].toStdString()); diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index a130a8e9351f55..b0facfce83d436 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -261,7 +261,7 @@ bool RichTextDialog::alert(const QString &prompt_text, QWidget *parent) { // MultiOptionDialog -MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, QStringList l, QWidget *parent) : QDialogBase(parent) { +MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) : QDialogBase(parent) { QFrame *container = new QFrame(this); container->setStyleSheet(R"( QFrame { background-color: #1B1B1B; } @@ -301,12 +301,17 @@ MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, QStringList l, confirm_btn->setObjectName("confirm_btn"); confirm_btn->setEnabled(false); - for (QString &s : l) { + for (const QString &s : l) { QPushButton *selectionLabel = new QPushButton(s); selectionLabel->setCheckable(true); + selectionLabel->setChecked(s == current); QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) { if (checked) selection = s; - confirm_btn->setEnabled(true); + if (selection != current) { + confirm_btn->setEnabled(true); + } else { + confirm_btn->setEnabled(false); + } }); group->addButton(selectionLabel); @@ -336,8 +341,8 @@ MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, QStringList l, outer_layout->addWidget(container); } -QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList l, QWidget *parent) { - MultiOptionDialog d = MultiOptionDialog(prompt_text, l, parent); +QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) { + MultiOptionDialog d = MultiOptionDialog(prompt_text, l, current, parent); if (d.exec()) { return d.selection; } diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index 47d8b74efdcc27..6c47a31d872b9a 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -73,7 +73,7 @@ class MultiOptionDialog : public QDialogBase { Q_OBJECT public: - explicit MultiOptionDialog(const QString &prompt_text, const QStringList l, QWidget *parent); - static QString getSelection(const QString &prompt_text, const QStringList l, QWidget *parent); + explicit MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); + static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); QString selection; }; diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index ad35a37fa12283..5a8f21d7a7ab2d 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -193,67 +193,67 @@ 변경 - + Select a language 언어선택 - + Reboot 재부팅 - + Power Off 전원 종료 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot은 장치를 왼쪽 또는 오른쪽 4° 이내, 위쪽 5° 또는 아래쪽 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. - + Your device is pointed %1° %2 and %3° %4. 사용자의 기기가 %1° %2 및 %3° %4를 가리키고 있습니다. - + down 아래 - + up - + left 왼쪽 - + right 오른쪽 - + Are you sure you want to reboot? 재부팅 하시겠습니까? - + Disengage to Reboot 재부팅 하려면 해제하세요 - + Are you sure you want to power off? 전원을 종료하시겠습니까? - + Disengage to Power Off 전원을 종료하려면 해제하세요 @@ -461,7 +461,7 @@ location set 선택 - + Cancel 취소 @@ -717,33 +717,33 @@ location set SettingsWindow - + × × - + Device 장치 - - + + Network 네트워크 - + Toggles 토글 - + Software 소프트웨어 - + Navigation 네비게이션 @@ -982,68 +982,68 @@ location set SoftwarePanel - + Git Branch Git 브렌치 - + Git Commit Git 커밋 - + OS Version OS 버전 - + Version 버전 - + Last Update Check 최신 업데이트 검사 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 이전에 openpilot에서 업데이트를 성공적으로 확인한 시간입니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. - + Check for Update 업데이트 확인 - + CHECKING 검사중 - + Uninstall 삭제 - + UNINSTALL 삭제 - + Are you sure you want to uninstall? 삭제하시겠습니까? - + failed to fetch update 업데이트를 가져올수없습니다 - - + + CHECK 확인 diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index c3e1954f1e9479..dbbde363949db7 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -193,67 +193,67 @@ 切换 - + Select a language 选择语言 - + Reboot 重启 - + Power Off 关机 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 要求设备安装在左或右 4° 以内,上 5° 或下 8° 以内。 openpilot 会持续校准,很少需要重置。 - + Your device is pointed %1° %2 and %3° %4. 您的设备指向 %1° %2 和 %3° %4。 - + down - + up 向上 - + left 向左 - + right 向右 - + Are you sure you want to reboot? 您确定要重新启动吗? - + Disengage to Reboot 脱离以重新启动 - + Are you sure you want to power off? 您确定要关闭电源吗? - + Disengage to Power Off 脱离以关闭电源 @@ -461,7 +461,7 @@ location set 选择 - + Cancel 取消 @@ -717,33 +717,33 @@ location set SettingsWindow - + × × - + Device 设备 - - + + Network 网络 - + Toggles 切换 - + Software 软件 - + Navigation 导航 @@ -982,68 +982,68 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 操作系统版本 - + Version 版本 - + Last Update Check 最后更新检查 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次 openpilot 成功检查更新的时间。 更新程序仅在汽车关闭时运行。 - + Check for Update 检查更新 - + CHECKING 正在检查 - + Uninstall 卸载 - + UNINSTALL 卸载 - + Are you sure you want to uninstall? 您确定要卸载吗? - + failed to fetch update 未能获取更新 - - + + CHECK 查看 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index f6e36081fac730..2920916ece1ccc 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -193,67 +193,67 @@ 更改 - + Select a language 選擇語言 - + Reboot 重新啟動 - + Power Off 關機 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以内或朝下偏差 8° 以内。鏡頭在後台會持續自動校準,很少有需要重置的情况。 - + Your device is pointed %1° %2 and %3° %4. 你的設備目前朝%2 %1° 以及朝%4 %3° 。 - + down - + up - + left - + right - + Are you sure you want to reboot? 您確定要重新啟動嗎? - + Disengage to Reboot 請先取消控車才能重新啟動 - + Are you sure you want to power off? 您確定您要關機嗎? - + Disengage to Power Off 請先取消控車才能關機 @@ -466,7 +466,7 @@ location set 選擇 - + Cancel 取消 @@ -725,33 +725,33 @@ location set SettingsWindow - + × × - + Device 設備 - - + + Network 網路 - + Toggles 設定 - + Software 軟體 - + Navigation 導航 @@ -990,68 +990,68 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 系統版本 - + Version 版本 - + Last Update Check 上次檢查時間 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 - + Check for Update 檢查更新 - + CHECKING 檢查中 - + Uninstall 卸載 - + UNINSTALL 卸載 - + Are you sure you want to uninstall? 您確定您要卸載嗎? - + failed to fetch update 下載更新失敗 - - + + CHECK 檢查 From be7f7041681ab088f03c075a8a836e538378a7a8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 11 Jul 2022 12:52:03 -0700 Subject: [PATCH 304/436] Fix new steer saturated warning with joystick mode (#25113) Fix steer sat warning with joystick mode --- selfdrive/controls/controlsd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 6f0c9c2ae6c3a7..117509f1e6bfae 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -602,14 +602,14 @@ def state_control(self, CS): lac_log.saturated = abs(actuators.steer) >= 0.9 # Send a "steering required alert" if saturation count has reached the limit - if lac_log.active and not CS.steeringPressed and self.CP.lateralTuning.which() == 'torque': + if lac_log.active and not CS.steeringPressed and self.CP.lateralTuning.which() == 'torque' and not self.joystick_mode: undershooting = abs(lac_log.desiredLateralAccel) / abs(1e-3 + lac_log.actualLateralAccel) > 1.2 turning = abs(lac_log.desiredLateralAccel) > 1.0 good_speed = CS.vEgo > 5 max_torque = abs(self.last_actuators.steer) > 0.99 if undershooting and turning and good_speed and max_torque: self.events.add(EventName.steerSaturated) - elif lac_log.active and lac_log.saturated and not CS.steeringPressed: + elif lac_log.active and not CS.steeringPressed and lac_log.saturated: dpath_points = lat_plan.dPathPoints if len(dpath_points): # Check if we deviated from the path From 5e896ce2f134b9f17d466bf1aec694142bcb5ed3 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Mon, 11 Jul 2022 13:22:13 -0700 Subject: [PATCH 305/436] Improve Simplified Chinese translations (#25091) * 1 * shane told me to do this * 1.5 * 2 * 3 * 4 * Update main_zh-CHS.ts * release * some minor improvements * build * remove state Co-authored-by: Shane Smiskol --- selfdrive/ui/translations/main_zh-CHS.qm | Bin 18457 -> 18507 bytes selfdrive/ui/translations/main_zh-CHS.ts | 272 +++++++++++------------ 2 files changed, 135 insertions(+), 137 deletions(-) diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index b96acb89d99ed8207dd34740c51714706e771040..59487d263a7a7524f15a239bb7ace144137f31b0 100644 GIT binary patch delta 4896 zcmZuz30PBC_CHD9%f5gtDzbzR+dJw5{3}wOY}-)N!h|+8V30j@CNpXqEqYi8%FhK0ZA!_nvdl_B+3OH?8J; zwVJcq+cFn``vf4|0KhY$G^3n{awEzOC=UQoxGz@&(9r-9w^5D;h>S;Bh&o264Cn&j z-ve-22O#i4`60?tC@-Kq3?S$PfKb%4xMt%AD!RT2Fs>CqeFR`q z8#+n^n5;)>0nop`}ii9AS%PePQawZP7DO|nwNp+ z%Ro~@u%bf9yLKJ3^RR=i={_3tf`R)5ZcG6~SqnN|3{!XQ01(ZCX%p9>!_n}k?!^FO zG*JB6adZ>_wG{zq{~^qOg7&g<*m~j=z}Q&W(@+8sRs}t78UWwRV1K<4_xEr-M@0ez zwR48O6@*pia1vHou-7qZ9PQCCfZzyD*EhH^Dv)!^fEDmq!nw%Z4&XC^b1SYDV8jTn z*o5s-^>BmjKcL-HZnn4q1KZCnV*9QBnfvB>5$dnwe$;`1#a3_+PFw|`Xyu;M;<>&f zxmS)OW}-gjUftq>=Plt;mZ+fs?gpOEFW57mo4f$)7|bk}*QmvVUn$_dE62zqWxPH6 z^6;RCyaRjCP~F1o32jE;UgVt~f_>*KNkMIqm*Ui#7_{#@svX<0-VzQuv037sLeYbV#KM`w(Aur zo2d6rcLD@_LH(_I2bQ{$+AqbH$*xde-MED4_NTt;`xo~7F7?BhzX1p=v^Wu4k$RVw zS!V+z_0pO0U`%K#J#9L+DzU^uo5TlkG(_~PL6|{I4gD$$HqjQ8n3+&TFQ}Z42D|7* z@p5$VZ`!&Z0}-8YO21U6RMtA>m~MJA0vO!y(wl9F6^W7F@!E2Lp+o2oj$Osd`O=?1 z3Bta1B4@ z7LF|cTmJYxp8_N=;TLkSCBZa*`@^RI{?quML~X*rm+&8HzCjQs2m->eVyZmBPOoK{ zV6WhVx*susse(JYZ8-m9t%84FL^2o>E_l#~CE^_rJRbcMV!=(A8PNKK)=<}T851%U%KH@K8|0nbk7OASmODLm$mtH!Gfl;eu{GCDQFh>^c^A8MUlx)q6K09L^qB&CD{s!_u7)So*Ph$}iljVQefsPW!$Up5zEO=j* zpBno$qCG_+yo(GfII3_@$MH^7C_HaGMM65Mm~b7>i|JIoIYlL|%5b`AUgd zKi}3w9*C1TQA90q=QDPVrBOu~iPo3%{!7Qi;Bh=yl zGoc25H86I^0m1Twq00ZQK}zK*e!lF1FcGo_gph@Hbznk!BZkDTiAc9xC6IPCcbN^- z$T#vhJ}?js=|rvw=C*%H5)^uEUYWiWO3WKu+rM<`Rb=IzE{GqHHmaCa9Xw-0VSC$= z_Om7LF!p61m2NPBW|&jYe5QPtM&H$;<}ycSx7Jr1`}KDkRcbEzQE{52#LG#KOSVX8 zKC@`6=GydGMBy4Hl0Gj%s%tzgbkty>Yf?hI$v#i=at+L_Y~kB`bi)Uvj9q_1Yu3HJ z4xebkcJ_-Jj&!lJ|7aoMbS4T*s}KJ1KV3_GT4H#DVbaI zG)u$V&*rO&Z$LckHgxEJV(E-Z~S8Lg=GvImV< zIj8w{<$dFB((HDb4W-CEloiX}HGJcdg%V>u`PMy`($=gwNg_NZ2!ys{`i~hPW{)tL zw7NWBWr)^wn;#-}{^Ai!i$TeRlkYu>ge|`nxo&{=7Lw^1?NnrwP=+W&NRwxkkhLtJ z8u;Y4XB_Rq{@Uw5bPb25>ga_F$*02#=?d$%^)#8S_H<~Ns^i;tM3O|Dub%o;x5d`P zj3lKaKBK9wi_N=8sCS4+$j)Tp(fJ%=@QxG-*{{yw);r`)??{Q%qHVrxkS(}7;6=Xn z4wi^vGU7EC{~5?5?_hq{#lo)Dek9mOiCcPPhjI9qMJD+Kh=foLwTR&=GSA0fz{l@; zsM@>TXDKzpw}?MDo{SvIu{pK)H)CZ8;v+3!l1K_MB@GTvSs9Q>XR){^scI$8cg;<0 z2U{A_A7JBnvtQJl1*S~0D)22IZ^#4<#+HjTrNmG)7$pM+(AV$BUd9D2<9Iqu4+TXD z#G!7WEWWq*UQiN;%k=Gy3|Yi+b#!A8_>P7h2*naiFDsB~AL*A`50cxVp{Z%b_u6mh z&n$l2{5`8X;5EO_x`VOnlp3hPN0vgtS1d5ef$$5$LAE3t zBQ&n=jLi03`Od*Lq-!cNp5M|}*u_jFX_1jwnk<+BbD;{0Jp*b7gl&^;mDb&*QD*5t_ho5;{mmG!~=I?WYk?Gl4U%O1aURzVZv&VmK$dkW}avPoo|K%z&k z7YW(e*qq6Uk(!vX)(0#dIDE~)R#c(SIY@X$j9_l_AyX-nJlKBC8;uDA(!I%1$s8HN zoekhw*bAaJ$Qu5L6XK`#;tmK|Ah-;O*5nrc2f6Sy{RQ?Ywy%5J)ig&TcYuJw#*f1< z10gYpL^gWuqt4;vKuj>#q9xa2{3tdXLdA}wY{%+vlib)W9@lu4Y{UhYHORGCADYTn zH6J2$oIhea4?)FX71K#XoKnEA@NNpgLCTBsr-hEb&WQ^|Kb5xESYM3F` zgJbIB%RSs-JYE_NSdk2@#|*4UIZnsb_=rF3WIh`P)FZc?q!`Az6_S z7oEt68L0LzbJ4v$xNB}T_IKV_b6Ix_B_%fINWQA3$yUiSJ{d^(l@{7&G5zG%#F0{# zRamuxn3cgK{FP!4FSe^}A)iG!0UDuPsNOP-WoNCtXRC9F#-GDR1mcHU&8GC`}i=jIGBS}8z62r7&drgwY>@%bnD+=zAlJ7WxuP-s<4{4`zok7L-ZUX2NK9Jjj8|Fx zmCJ3%APyfU4r;`AK?0};tp`NxX-n-@hih|mY8F^4U$-4YD}D1J#@!TNrnl7?TRCEPmsnzE#2V$Zm&ISlOdJsl@kI(_e^pAU zf{lD*t92`SILM-h3>ll~rv9(K^M!%)HDl~1A8*G{KeN^)I`Ts@L5i=3S~4Pij7-XE zXlrzd`YD=RQl1`Tsdw1C^k%YMYMb2Bx0+kBuK3#0a6<>1m*z@2$F^MeXEm3#sjgsO z5XS!L-K9r1E;9D(Mzi}hH)kkxZ>YIeJBtx>D}s1bzRLNgY4kHrP;;MC=-DZAjY<-h zaUI`=@=Q;PZ5h4{eaQICVmGh<(ct}=**tkqpK`MiIa5 zU3QZgves&*R`hC$*j})AKRa9)<%9q77fTB|^&mq#FZRm&r9V4z*!}EvhU`}mTBK-x zhe;-5#_e@Jcl*bM{E_W_Nk(?OlU&%_+(FmNvp;aIuwEQ1nd2wRhLm2k?jH0=wvA78 TUT|gA<)_(O-J$pM_^AH}{k-)m delta 4941 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 Close - + 关闭 @@ -16,7 +16,7 @@ Reboot and Update - 重启和更新 + 重启并更新 @@ -24,17 +24,17 @@ Back - 后退 + 返回 Enable Tethering - 启用网络共享 + 启用WiFi热点 Tethering Password - 网络共享密码 + WiFi热点密码 @@ -45,7 +45,7 @@ Enter new tethering password - 输入新的网络共享密码 + 输入新的WiFi热点密码 @@ -55,22 +55,22 @@ Enable Roaming - 启用漫游 + 启用数据漫游 APN Setting - APN 设置 + APN设置 Enter APN - 输入 APN + 输入APN leave blank for automatic configuration - 为自动配置留空 + 留空以自动配置 @@ -92,17 +92,17 @@ You must accept the Terms and Conditions in order to use openpilot. - 您必须接受条款和条件才能使用 openpilot。 + 您必须接受条款和条件以使用openpilot。 Back - 后退 + 返回 Decline, uninstall %1 - 拒绝,卸载 %1 + 拒绝并卸载%1 @@ -110,37 +110,37 @@ Dongle ID - 加密狗 ID + 设备ID(Dongle ID) N/A - 不适用 + N/A Serial - 串行 + 序列号 Driver Camera - 司机摄像头 + 驾驶员摄像头 PREVIEW - 预习 + 预览 Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - 预览面向驾驶员的摄像头,以帮助优化设备安装位置以获得最佳驾驶员监控体验。 (车辆必须关闭) + 打开并预览驾驶员摄像头,用于调整安装角度以获得最优驾驶员监控体验。仅熄火时可用。 Reset Calibration - 重置校准 + 重置设备校准 @@ -150,32 +150,32 @@ Are you sure you want to reset calibration? - 您确定要重置校准吗? + 您确定要重置设备校准吗? Review Training Guide - 查看培训指南 + 新手指南 REVIEW - 重新查看 + 查看 Review the rules, features, and limitations of openpilot - 查看 openpilot 的规则、功能和限制 + 查看openpilot的使用规则,以及其功能和限制。 Are you sure you want to review the training guide? - 您确定要查看培训指南吗? + 您确定要查看新手指南吗? Regulatory - 监管 + 监管信息 @@ -210,32 +210,32 @@ openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot 要求设备安装在左或右 4° 以内,上 5° 或下 8° 以内。 openpilot 会持续校准,很少需要重置。 + openpilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下8°之间。一般来说,openpilot会持续更新校准,很少需要重置。 Your device is pointed %1° %2 and %3° %4. - 您的设备指向 %1° %2 和 %3° %4。 + 您的设备校准为%1° %2、%3° %4。 down - + 朝下 up - 向上 + 朝上 left - 向左 + 朝左 right - 向右 + 朝右 @@ -245,17 +245,17 @@ Disengage to Reboot - 脱离以重新启动 + 取消openpilot以重新启动 Are you sure you want to power off? - 您确定要关闭电源吗? + 您确定要关机吗? Disengage to Power Off - 脱离以关闭电源 + 取消openpilot以关机 @@ -263,7 +263,7 @@ Drives - 驱动器 + 旅程数 @@ -278,12 +278,12 @@ PAST WEEK - 上周 + 过去一周 KM - 千米 + 公里 @@ -296,7 +296,7 @@ camera starting - 相机启动中 + 正在启动相机 @@ -309,12 +309,12 @@ Need at least - 需要至少 + 至少需要 characters! - 字符! + 个字符! @@ -322,7 +322,7 @@ Installing... - 安装... + 正在安装…… @@ -332,12 +332,12 @@ Resolving deltas: - 解决增量: + 正在处理: Updating files: - 更新文件: + 正在更新文件: @@ -401,12 +401,12 @@ CLEAR - CLEAR + 清空 Recent Destinations - 近期目的地 + 最近目的地 @@ -417,27 +417,25 @@ Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai - 使用逗号获取显示的详细路线和更多信息 -主要订阅。 立即注册:https://connect.comma.ai + 订阅comma prime以获取导航。 +立即注册:https://connect.comma.ai No home location set - 没有家 -位置集 + 家:未设定 No work location set - 没有工作 -位置集 + 工作:未设定 no recent destinations - 没有最近的目的地 + 无最近目的地 @@ -445,7 +443,7 @@ location set Map Loading - 地图加载 + 地图加载中 @@ -471,23 +469,23 @@ location set Advanced - 先进的 + 高级 Enter password - 先进的 + 输入密码 for " - 为了 " + 网络名称:" Wrong password - Wrong password + 密码错误 @@ -495,30 +493,30 @@ location set km/h - 公里/小时 + km/h mph - 英里/小时 + mph MAX - 最大限度 + 最高定速 SPEED - 速度 + SPEED LIMIT - 限制 + LIMIT @@ -544,7 +542,7 @@ location set Pair your device to your comma account - 将您的设备与您的逗号账户配对 + 将您的设备与comma账号配对 @@ -574,12 +572,12 @@ location set Become a comma prime member at connect.comma.ai - 成为 connect.comma.ai 的逗号主要会员 + 打开connect.comma.ai以注册comma prime会员 PRIME FEATURES: - 主要特点: + comma prime特权: @@ -589,7 +587,7 @@ location set 1 year of storage - 1年存储 + 1年数据存储 @@ -602,12 +600,12 @@ location set ✓ SUBSCRIBED - ✓ 订阅 + ✓ 已订阅 comma prime - 逗号素数 + comma prime @@ -617,7 +615,7 @@ location set COMMA POINTS - 逗号分 + COMMA POINTS点数 @@ -635,7 +633,7 @@ location set dashcam - 行车记录器 + 行车记录仪 @@ -673,17 +671,17 @@ location set Resetting device... - 正在重置设备... + 正在重置设备…… System Reset - 系统重置 + 恢复出厂设置 System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 触发系统重置。 按确认删除所有内容和设置。 按取消恢复启动。 + 已触发系统重置:确认以删除所有内容和设置。取消以正常启动设备。 @@ -703,7 +701,7 @@ location set Unable to mount data partition. Press confirm to reset your device. - 无法挂载数据分区。 按确认重置您的设备。 + 无法挂载数据分区。 确认以重置您的设备。 @@ -735,7 +733,7 @@ location set Toggles - 切换 + 设定 @@ -758,7 +756,7 @@ location set Power your device in a car with a harness or proceed at your own risk. - 使用安全带在汽车中为您的设备供电或自行承担风险。 + 请使用car harness线束为您的设备供电,或自行承担风险。 @@ -775,28 +773,28 @@ location set Getting Started - 入门 + 开始设置 Before we get on the road, let’s finish installation and cover some details. - 在我们上路之前,让我们完成安装并介绍一些细节。 + 开始旅程之前,让我们完成安装并介绍一些细节。 Connect to Wi-Fi - 连接到无线网络 + 连接到WiFi Back - 后退 + 返回 Continue without Wi-Fi - 在没有 Wi-Fi 的情况下继续 + 不连接WiFi并继续 @@ -811,12 +809,12 @@ location set Dashcam - 行车记录器 + Dashcam(行车记录仪) Custom Software - 定制的软件 + 自定义软件 @@ -826,12 +824,12 @@ location set for Custom Software - 定制软件 + 以下载自定义软件 Downloading... - 正在下载... + 正在下载…… @@ -841,7 +839,7 @@ location set Ensure the entered URL is valid, and the device’s internet connection is good. - 确保输入的 URL 有效,并且设备的互联网连接良好。 + 请确保互联网连接良好且输入的URL有效。 @@ -864,7 +862,7 @@ location set Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 将您的设备与 comma connect (connect.comma.ai) 配对并领取您的 comma prime 优惠。 + 将您的设备与comma connect(connect.comma.ai)配对并领取您的comma prime优惠。 @@ -878,7 +876,7 @@ location set CONNECT - 连接 + CONNECT @@ -889,49 +887,49 @@ location set ONLINE - 在线的 + 在线 ERROR - 错误 + 连接出错 TEMP - 温度 + 设备温度 HIGH - 高的 + 过热 GOOD - 好的 + 良好 OK - 好的 + 一般 VEHICLE - 车辆 + 车辆连接 NO - 未连接 + PANDA - 熊猫 + PANDA @@ -941,7 +939,7 @@ location set SEARCH - 搜索 + 搜索中 @@ -956,7 +954,7 @@ location set ETH - 以太網 + 以太网 @@ -984,32 +982,32 @@ location set Git Branch - Git 分支 + Git Branch Git Commit - Git 提交 + Git Commit OS Version - 操作系统版本 + 系统版本 Version - 版本 + 软件版本 Last Update Check - 最后更新检查 + 上次检查更新 The last time openpilot successfully checked for an update. The updater only runs while the car is off. - 上次 openpilot 成功检查更新的时间。 更新程序仅在汽车关闭时运行。 + 上一次成功检查更新的时间。更新程序仅在汽车熄火时运行。 @@ -1019,7 +1017,7 @@ location set CHECKING - 正在检查 + 正在检查更新 @@ -1039,7 +1037,7 @@ location set failed to fetch update - 未能获取更新 + 获取更新失败 @@ -1053,12 +1051,12 @@ location set SSH Keys - SSH 密钥 + SSH密钥 Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 警告:这将授予对 GitHub 设置中所有公钥的 SSH 访问权限。 切勿输入您自己以外的 GitHub 用户名。 逗号员工永远不会要求您添加他们的 GitHub 用户名。 + 警告:这将授予SSH访问权限给您GitHub设置中的所有公钥。切勿输入您自己以外的GitHub用户名。comma员工永远不会要求您添加他们的GitHub用户名。 @@ -1069,7 +1067,7 @@ location set Enter your GitHub username - 输入你的 GitHub 用户名 + 输入您的GitHub用户名 @@ -1079,12 +1077,12 @@ location set REMOVE - 消除 + 删除 Username '%1' has no keys on GitHub - 用户名“%1”在 GitHub 上没有密钥 + 用户名“%1”在GitHub上没有密钥 @@ -1094,7 +1092,7 @@ location set Username '%1' doesn't exist on GitHub - GitHub 上不存在用户名“%1” + GitHub上不存在用户名“%1” @@ -1102,7 +1100,7 @@ location set Enable SSH - 启用 SSH + 启用SSH @@ -1120,7 +1118,7 @@ location set Scroll to accept - 滑动接受 + 滑动以接受 @@ -1133,12 +1131,12 @@ location set Enable openpilot - 启用 openpilot + 启用openpilot Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. - 使用 openpilot 系统进行自适应巡航控制和车道保持驾驶员辅助。 任何时候都需要您注意使用此功能。 更改此设置在汽车断电时生效。 + 使用openpilot进行自适应巡航和车道保持辅助。使用此功能时您必须时刻保持注意力。该设置的更改在熄火时生效。 @@ -1148,7 +1146,7 @@ location set Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). - 当您的车辆在以超过 31 英里/小时(50 公里/小时)的速度行驶时在检测到的车道线上漂移而没有激活转向信号时,接收提醒以返回车道。 + 车速超过31mph(50km/h)时,若检测到车辆越过车道线且未打转向灯,系统将发出警告以提醒您返回车道。 @@ -1158,57 +1156,57 @@ location set Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. - 允许 openpilot 遵守左侧交通惯例并在右侧驾驶座上执行驾驶员监控。 + 允许openpilot遵守左侧交通惯例并在右侧驾驶座上执行驾驶员监控。 Use Metric System - 使用公制 + 使用公制单位 Display speed in km/h instead of mph. - 以公里/小时而不是英里/小时显示速度。 + 显示车速时,以km/h代替mph。 Record and Upload Driver Camera - 记录和上传司机摄像头 + 录制并上传驾驶员摄像头 Upload data from the driver facing camera and help improve the driver monitoring algorithm. - 从面向驾驶员的摄像头上传数据,帮助改进驾驶员监控算法。 + 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 Disengage On Accelerator Pedal - 踩油门解除 + 踩油门时取消控制 When enabled, pressing the accelerator pedal will disengage openpilot. - 启用后,踩下油门踏板将解除 openpilot。 + 启用后,踩下油门踏板将取消openpilot。 Show ETA in 24h format - 以 24 小时格式显示 ETA + 以24小时格式显示预计到达时间 Use 24h format instead of am/pm - 使用 24 小时制代替上午/下午 + 使用24小时制代替am/pm openpilot Longitudinal Control - openpilot 纵向控制 + openpilot纵向控制 openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! - openpilot 将禁用汽车的雷达并接管油门和刹车的控制。 警告:这会禁用 AEB! + openpilot将禁用车辆的雷达并接管油门和刹车的控制。警告:AEB将被禁用! @@ -1221,12 +1219,12 @@ location set An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 需要操作系统更新。 将您的设备连接到 Wi-Fi,以获得最快的更新体验。 下载大小约为 1GB。 + 操作系统需要更新。请将您的设备连接到WiFi以获取更快的更新体验。下载大小约为1GB。 Connect to Wi-Fi - 连接到无线网络 + 连接到WiFi @@ -1236,12 +1234,12 @@ location set Back - 后退 + 返回 Loading... - 正在加载... + 正在加载…… @@ -1260,12 +1258,12 @@ location set Scanning for networks... - 正在扫描网络... + 正在扫描网络…… CONNECTING... - 正在连接... + 正在连接…… @@ -1275,7 +1273,7 @@ location set Forget Wi-Fi Network " - 忘记 Wi-Fi 网络" + 忘记WiFi网络" From c181d475c52798462c5b2a2cf2237eebde0fa26d Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Mon, 11 Jul 2022 13:41:55 -0700 Subject: [PATCH 306/436] fix a translation line break --- selfdrive/ui/translations/main_zh-CHS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 9dade3470193ea..9ac35994e61bea 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -862,7 +862,7 @@ location set Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 将您的设备与comma connect(connect.comma.ai)配对并领取您的comma prime优惠。 + 将您的设备与comma connect (connect.comma.ai)配对并领取您的comma prime优惠。 From 30d88d6892f11bf2472d97d79e9f68bef9e01b3c Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Mon, 11 Jul 2022 13:45:45 -0700 Subject: [PATCH 307/436] update main_zh-CHS.qm --- selfdrive/ui/translations/main_zh-CHS.qm | Bin 18507 -> 18509 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 59487d263a7a7524f15a239bb7ace144137f31b0..63c17d76b7f0f99e5180d400a8143d17cbbdde55 100644 GIT binary patch delta 774 zcmWksX-HI27(MsBcW0XEIpYvI_@l;sGM5=e)U?43T&AKlHOUMy(XteROpQ=%0jUh- zDu$4#n6_DCIDeEEx89xuK{Nx{fqKS%hOQVeO~V<}hX0?lI)zw9 z(ZtM|ZsbUlWF;3l#SO%*LtcL`psPlHW-H*GfJ3*=0f|{Cd_Dz)2cn|HAMhGR&H5VP zaTMLtGeAyn{wkt;wOZM#_Z!(2U`-U)oyjF%Rqf)tM5J%Mr;U&#>UQy z3Qep|YlvG6qa5GGfX2#_#k|I1G8w(UZqRmSj2TPhD6(B}t;Ku9xUk$w&3-d3rrig2 b1sn&Gw+zK=Plm8FqTe1Y!FR}IuQUGxC?Cv+ delta 771 zcmWlXZAep59LAq>?>)1vyUl%=!wpD3Huh zj*s>j1(c*!|^D;7EvwlERZ>_M}W*J(<0d&ihj&*H7ueXDx7` zS9)=W%wj90>72WO@uu{tfMh{Y(#l*Z5IrTjB&+R&+)zO3gtPKvLjy(B%VQJyH2qti z95a(!yYNX_Y*v_Md!SnxTB{^woAP9dmJOa!o?oVQQmaIfY){+b`p&CBqK{`7!pNzZ zmt3Im@#VZ)Hw~y-UK9EVh^gf@qaG?@fVczfJH%V6nrYr6rer%C5#MDi3ldzho6P*r zE;%?|8p&2jH7yFaVqrH#k>WJ@{GMI;21vzzo2P~-L#OCdtn91sDhVu4Y|z}I;M~dv zL>SKs+|fPsmDBy$)s1%a(lpaex;P!*u(-wT+Q$7MujyhWn1z=+9kOa0qrZmxt5NP2 z%DJs(W_$$PMm0CF3`h!9^U`Ubq-mkoSOR_OSWgPII;hSK7^ts&b>Ry!`mbY zX%e*DkhmI2du)nPjh&^4WsTKjGzR?GqtjV1X0K7Am_EV%tN|0omGy_z?3Qsg^C@uD Yb_qx`jVJllI#|8%xWdPmT-PlB0NsGh%m4rY From bdfaa1d1eee77d1f9fb5b9b2632c9988caba02e3 Mon Sep 17 00:00:00 2001 From: HaraldSchafer Date: Mon, 11 Jul 2022 15:19:55 -0700 Subject: [PATCH 308/436] Ram 1500 torque tune (#25117) * torque control again * 3mss per s * no bad sensors * tweaks * Need more checks before we can do this * update refs * only ram for now Co-authored-by: Adeeb Shihadeh --- panda | 2 +- selfdrive/car/chrysler/carcontroller.py | 7 ++++--- selfdrive/car/chrysler/interface.py | 6 +++--- selfdrive/car/chrysler/values.py | 14 ++++++++++---- selfdrive/car/interfaces.py | 4 ++-- selfdrive/car/torque_data/override.yaml | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/panda b/panda index ca927fe9312651..baecd2ecc6a2a6 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit ca927fe9312651a16f13aaddca8b46af5315ede6 +Subproject commit baecd2ecc6a2a608e1305601f6f697feca69fe88 diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index e0eb979e6ab61c..a7f2d007f13650 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -16,6 +16,7 @@ def __init__(self, dbc_name, CP, VM): self.lkas_active_prev = False self.packer = CANPacker(dbc_name) + self.params = CarControllerParams(CP) def update(self, CC, CS, low_speed_alert): can_sends = [] @@ -40,8 +41,8 @@ def update(self, CC, CS, low_speed_alert): # steering if self.frame % 2 == 0: # steer torque - new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX)) - apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorqueEps, CarControllerParams) + new_steer = int(round(CC.actuators.steer * self.params.STEER_MAX)) + apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorqueEps, self.params) if not lkas_active: apply_steer = 0 self.steer_rate_limited = new_steer != apply_steer @@ -56,6 +57,6 @@ def update(self, CC, CS, low_speed_alert): self.lkas_active_prev = lkas_active new_actuators = CC.actuators.copy() - new_actuators.steer = self.apply_steer_last / CarControllerParams.STEER_MAX + new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX return new_actuators, can_sends diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 697fb9b83adc00..8826a925236148 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -46,15 +46,15 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa # Ram elif candidate == CAR.RAM_1500: + ret.steerActuatorDelay = 0.2 + ret.wheelbase = 3.88 ret.steerRatio = 16.3 ret.mass = 2493. + STD_CARGO_KG ret.maxLateralAccel = 2.4 ret.minSteerSpeed = 14.5 + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) - ret.lateralTuning.pid.kpBP, ret.lateralTuning.pid.kiBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.1], [0.02]] - ret.lateralTuning.pid.kf = 0.00003 else: raise ValueError(f"Unsupported car: {candidate}") diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index ada4f486fc06f2..f7531792fb60e6 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -26,10 +26,16 @@ class CAR: class CarControllerParams: - STEER_MAX = 261 # higher than this faults the EPS on Chrysler/Jeep. Ram DT allows more - STEER_DELTA_UP = 3 - STEER_DELTA_DOWN = 3 - STEER_ERROR_MAX = 80 + def __init__(self, CP): + self.STEER_MAX = 261 # higher than this faults the EPS on Chrysler/Jeep. Ram DT allows more + self.STEER_ERROR_MAX = 80 + + if CP.carFingerprint in RAM_CARS: + self.STEER_DELTA_UP = 5 + self.STEER_DELTA_DOWN = 5 + else: + self.STEER_DELTA_UP = 3 + self.STEER_DELTA_DOWN = 3 STEER_THRESHOLD = 120 diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 136337c5a48f01..4c7ea97dffd4c2 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -135,11 +135,11 @@ def get_std_params(candidate, fingerprint): return ret @staticmethod - def configure_torque_tune(candidate, tune, steering_angle_deadzone_deg=0.0): + def configure_torque_tune(candidate, tune, steering_angle_deadzone_deg=0.0, use_steering_angle=True): params = get_torque_params(candidate) tune.init('torque') - tune.torque.useSteeringAngle = True + tune.torque.useSteeringAngle = use_steering_angle tune.torque.kp = 1.0 / params['LAT_ACCEL_FACTOR'] tune.torque.kf = 1.0 / params['LAT_ACCEL_FACTOR'] tune.torque.ki = 0.1 / params['LAT_ACCEL_FACTOR'] diff --git a/selfdrive/car/torque_data/override.yaml b/selfdrive/car/torque_data/override.yaml index 476313df2b8461..8e6f62c4e779c9 100644 --- a/selfdrive/car/torque_data/override.yaml +++ b/selfdrive/car/torque_data/override.yaml @@ -21,7 +21,7 @@ COMMA BODY: [.nan, 1000, .nan] # Totally new cars KIA EV6 2022: [3.5, 2.5, 0.0] -RAM 1500 5TH GEN: [2.0, 2.0, 0.05] +RAM 1500 5TH GEN: [2.0, 2.0, 0.0] # Dashcam or fallback configured as ideal car mock: [10.0, 10, 0.0] diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 65ecbb4be37130..fc5f83c32a953d 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -825acfae98543c915c18d3b19a9c5d2503e431a6 \ No newline at end of file +d583bbd9643000e7f817171c583d31ae3141a652 \ No newline at end of file From 903bb405286295b48d5192cb2a7b2c01029193e3 Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Tue, 12 Jul 2022 08:51:03 +0900 Subject: [PATCH 309/436] Improve Korean translations (#25105) * kor translations fix update_translations --release qm * space * qm * map setting tr space remove * ts update fix ko_update ch_update * qm release * Update QM * Fix this translation Co-authored-by: Shane Smiskol --- selfdrive/ui/qt/maps/map_settings.cc | 2 +- selfdrive/ui/translations/main_ko.qm | Bin 19997 -> 20040 bytes selfdrive/ui/translations/main_ko.ts | 96 +++++++++++------------ selfdrive/ui/translations/main_zh-CHS.qm | Bin 18509 -> 18508 bytes selfdrive/ui/translations/main_zh-CHS.ts | 2 +- selfdrive/ui/translations/main_zh-CHT.qm | Bin 18569 -> 18568 bytes selfdrive/ui/translations/main_zh-CHT.ts | 7 +- 7 files changed, 51 insertions(+), 56 deletions(-) diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc index eaa8b1f703dfb7..d143b44e70e373 100644 --- a/selfdrive/ui/qt/maps/map_settings.cc +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -104,7 +104,7 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) { screenshot->setPixmap(pm.scaledToWidth(1080, Qt::SmoothTransformation)); no_prime_layout->addWidget(screenshot, 0, Qt::AlignHCenter); - QLabel *signup = new QLabel(tr("Get turn-by-turn directions displayed and more with a comma \nprime subscription. Sign up now: https://connect.comma.ai")); + QLabel *signup = new QLabel(tr("Get turn-by-turn directions displayed and more with a comma\nprime subscription. Sign up now: https://connect.comma.ai")); signup->setStyleSheet(R"(font-size: 45px; color: white; font-weight:300;)"); signup->setAlignment(Qt::AlignCenter); diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm index 60966cdde587ebf4a43deb8ad255f374d32ca7c8..c5c66d1e73491c2ff09d91b6fdd5200002b2a6da 100644 GIT binary patch delta 2731 zcmYLJX;c(v7Jb!S)l2opt_J}NnpG4;!61vQf`Y~+XgCT<#?c^(xCEn~s3hYCijGT zc6t;x5h(_UMlB^$<^XpAR{CuOAd1YQN!oct&K{IDZZC)+uT_bN??uz{L@1g`)BoOE zh>Xf;=DL64Km}zeu4AE`GUk1Ti+9oN6J9*q12i856Oi z5Va03XJ%M}u+YOC=)^+oVdiEA6c`f5+*KSW8lJ#B9=nmqQ={N>;FsW5h2O&-qKI5Y z3SR&vK2~HoAmi|vibEZ!`|x7Lr4kT}XjfdDyo1Q$f#UWI#1(uLUCr=J*fmA>5m&@p zu4D^Ch7h^-DLsEg&4za=eYV9RvpLF@GY~lX56aUtWny%3Ds)iI^SA7c0vla4A~VeyZq0|QaiEs_B;1Inv!2`bIf(ETcJv-?N1r@9 zPF%;827{Q}cCPe~@QQvhSGIW*l(@y6wRS_fHC)4hFO;j~u6yGpITR*x_qxEy?;GyB z4bAYtMmr`y;a(1a;h1QZ!aD<|D_0r&Lm=U9Rm|fQNSLomt-4AS7p9uapd#YD3Tyv<|A5(>h_o*=nC4C>9*6E_dGmPeiKE=6?qQE7binkKz5NtWZCH0%?>_ z)vu!a-~m>X6x4{u^IVgr1Y<{6&4Pctg2cBq+gBbS@+#8&=`;elI%qCDG-I5uRU6)i zx!%*7OEq{!f!c&$a*!FLot54Tq7B;F8=zdub?w|NC>^BJ&bK6jkV(6PhT#2^r0ux( zHq6$d?Q%pHRGImNV01dwZhn4SD>6>yiyxy?d1nG+i2|4K+xkox*YSJapvHlv{QlbG zAasl`y*mr$)$oT~QFAteuPoXF$a}%G?R=wK4DJuI@{fLB z0O_K2%*FLMkghY=HGxo!F6BWn%(PyY-GheVuhuQU&4T#`-De-WKthAg@*_$#Vz$mU z6^glC)w|SzK$u?dU4{9;+xk(lYv7f;`jjNpIx%0L7MV>np+%pb2qof{>SymQL_qIi z{gSt3X07^tFW*H5_w`k~@C=2|_02~h-PqIm8~1=?IQ=~f2(o`OsLFkD;ZsAX=Uxh$RlwL+hDM(+Vd5@BVbY!k=^K2W7CZ3C6Lgi)wn!yBHSEkEIx=nFj6oc>WfE>PZ>{@ z;iB;&#;ecZ39lC8&GG@ z<8nO-*OF_gw&+ge<+UVOH`VxcX4Xeq_J|*9Eq=b%ozl1UzBX5DG6|iTHNIC0NT~Q% zZJ0HtCX0mX>4Q5{`P3W^i2>)(~Z!!U1m zP_TH?kRp$MHmp_iO*Ku`IK0Bx!7!y_s>39ee9oFFiVh(zO+Qtfv&JK0fz6|OXj8Sd zsHs|NclbBMj1ya&%u1owB=$SyI6KKvWEtv(wyyum@V@g_wW=xk(*Bw(@u{;}tFe{b z^p?{X-CWGfO)<*F%SoOh;i5y;9uh3)upBQgatTyeDotF(EDN$%OqTJfm6i^|g0nb~k({K|c#*zX?4tQ13sMN5l^4QCXW z*NP<`2?nRutjgCUTsdiDn(L&y9!5sTS9G7zB4t!r`oXi=#LCr{YDWD|spjQLsOlbO zR{S;)6UT7vsnJt zRQAp+>uTxQTXPugW|@gxaB+IPo49&_PUYKP}Y`LGv8iO>1o`T%%~7r0!J=H z4%E;M*Dl65loraauHP;-$|mT@YPfGNf%KPzvkLb7nmZX{esa3EKaHVqq{otQ$F-r$ d{ubn-STr_Qd0y&BmNbkY7ESfUf7#SE{{dW0I-~#q delta 2737 zcmZuzX;c(v7Jb!S)l2n;$R=yCi3qZUhGCH?JMLo8Aqt~|7}Thv8BmNU$*2Rk=ad=~ z(YS!i*r=gE!Jq>jpuxCcfT#yh#wBW^CdSEVRAg?;oH_Gje$=^D-}1iu?z`{nZY^W> zlrh^}iw+XW`iVv*6UlRd^MNaX8-SkzO++l_4cSCoFj3GIU<^^PJ8%}(6c3`$b0Wnr zL_=DLly1O#z$hYH6|jLwe}+hFAsX(CrxWAji2Sgw$aXnitP3v^C9NYeYKX>{;2UD6byG z;czK3_$meHaKJUUr>?-C`IuvkpV6MsF50Bx@gRpf(u8(AVZZlDUN#=X^7EySr zESb-P60>BPHpti*D%;bk#`-0))0;pnyk2&0+!i96dRa#r;tEPxZyP)lxmd1=Dr^ipEpPH$ z1LHoHU$%Wl#B7m2&#!>M0c_HPFA*<_opV?Mv0`??I1CS2$gaHgHj&p&wlEt+l^k2l zJ|&87VvBZ-tA~qcuy4>NV-Ouu2hmH#mX*WVq2BC)J2}8C)^vFXk*_CP{eC%7vcT49 zttB(C-+bSV@%8LCKRhNHI-I>VrW&SIa(o0zk&wgbHZFs*E?k1%7o{D`Wz0dTqGuFw zx%@ep=q0z%2N@WTa|^9zU(+aRz|`O~xm=>Oz#%3rSygJjvt zXFot9d5`jC3@#7%2dV`BCbY{X)p$9W+dNgxd-M`g*Qhq}mdxvllA!uW_~jJ7Y%? z0(vgdE}m>5;`az3X{$a#8w#tmZTlcu*aq!|ZeVDSwz~zy*iN0I!UqR-=z`s@ zB7rHosQyy)^A2655%WGNy3;FwvES;Nybi&{UAm@t;7FD3{AhSV6RYdYKzllHdi$cI z6_{w$I~OB!yOnyc=LwLgS0CAjn<~s-KVcK3vpb_-7M}z+JLyYz;~oeY(C@i77M}Qv z{%_?tC~AoQ?7!dz_mlcdW82~OTL#t7xItC%2D>=4cQj*g`2ICfgwc?C2l0$o4NG$G zLLrSIaND)@sQm|q&*m+GRL>26U;2tD?!2M*{$5Bv+0d7K9N#Vr;GEl1v~q~d#krxC zgew!J^twh8&L!4UeaUx`Bv{th`ie``EiTTMZPIlTDi<||S`snNcI8>uwOpv4BfiUb z46!jSZB-3u8!B5}Ng!Ve6~}0d-czf7Gxu4>l2Gqd-DB<{p?P&BHMf%>UAJU(d)9^$dzd5aIVM(uzH8UYT* zBZ!RNMNHAfTIb#T{lycyWb5lQT_LAxu5GTCKGb(I%uaENP3mB*JvKo$|I4T9$L2)o zrp<2*V-&C3dNYCIfNie&&E7_#y;od0bOXmWC!XFV@^;=F+fsP`tQcYE&0G*?+PT|W zKk-XsQ?;Fh7N5ETOm46nAywPOFiORL2AV#O4P;Yvm8 ztFBVBlk|auo>8kv_$tz}ZGaI)$LSjF#mvS{By=UVY?mH6Ix%c>dwYYV9+t*vcuJs& zltghfO`PqV<+0APtueAIlLXV*+RWzElht*FBz!mBQqYxQ?y2<=uQ~_vZKdX(>L;~X z1J2@OA%y?0g3HAOml#Hn*0^{uGE1WPg=?I~=3-{$tHEf}BUe4cv8KM`YDwYVW?(O6 z)D=n{qy8voSScpdTgIsxeJ*8)^TL7|g|s_ND`QP(O?~3A@NV&NxK0uy6pXFa6ILpw zedd~~*5ec3)N(0n`l!Jqc|^`+xGk2vR#sdS<$xp`UB#VI9%^J`Rmrkmtc|+DS|f;? zqXXW&NcGV_GL9iuzgV`xD|vP6;5a|4=g3bKV!g$#(b1zK{{K8yrqBY)qg2YLY^xL7 z%S|MO#GaAqwC3{mMYTB)y|v}2q#1jN@f$&lX(`RezcQgYvFZYLzmJh%nqLcojhm$= z5;`-@J<^T16EYaAHcR{@ae|9LZ;{bTA7!vN_gY3 Enter new tethering password - 새 테더링 비밀번호 입력 + 새 테더링 비밀번호를 입력하세요 @@ -70,7 +70,7 @@ leave blank for automatic configuration - 자동 구성을 위해 비워둠 + 자동설정을 하려면 공백으로 두세요 @@ -92,7 +92,7 @@ You must accept the Terms and Conditions in order to use openpilot. - 당신은 반드시 약관에 동의해야만 openpilot을 사용할 수 있습니다. + openpilot을 사용하려면 이용 약관에 동의해야 합니다. @@ -102,7 +102,7 @@ Decline, uninstall %1 - 거절,삭제 %1 + 거절, %1 제거 @@ -135,7 +135,7 @@ Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - 운전자 카메라를 미리 보면서 최적의 운전자 모니터링 경험을 위해 기기 장착 위치를 최적화할수 있습니다. (차량은 반드시 닫아야 합니다) + 운전자 카메라를 미리 보면서 최적의 운전자 모니터링 경험을 위해 장치의 장착 위치를 최적화할수 있습니다. (차량연결은 해제되어있어야 합니다) @@ -150,7 +150,7 @@ Are you sure you want to reset calibration? - 캘리브레이션을 재설정하시겠습니까? + 캘리브레이션을 재설정하시겠습니까? @@ -165,12 +165,12 @@ Review the rules, features, and limitations of openpilot - openpilot의 규칙, 기능, 제한 다시보기 + openpilot의 규칙, 기능 및 제한 다시보기 Are you sure you want to review the training guide? - 트레이닝 가이드를 다시보시겠습니까? + 트레이닝 가이드를 다시보시겠습니까? @@ -185,7 +185,7 @@ Change Language - 언어변경 + 언어 변경 @@ -195,7 +195,7 @@ Select a language - 언어선택 + 언어를 선택하세요 @@ -210,37 +210,37 @@ openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot은 장치를 왼쪽 또는 오른쪽 4° 이내, 위쪽 5° 또는 아래쪽 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. + openpilot은 장치를 좌측 또는 우측은 4° 이내, 위쪽 5° 또는 아래쪽은 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. Your device is pointed %1° %2 and %3° %4. - 사용자의 기기가 %1° %2 및 %3° %4를 가리키고 있습니다. + 사용자의 장치가 %1° %2 및 %3° %4를 가리키고 있습니다. down - 아래 + 아래로 up - + 위로 left - 왼쪽 + 좌측으로 right - 오른쪽 + 우측으로 Are you sure you want to reboot? - 재부팅 하시겠습니까? + 재부팅 하시겠습니까? @@ -250,7 +250,7 @@ Are you sure you want to power off? - 전원을 종료하시겠습니까? + 전원을 종료하시겠습니까? @@ -263,7 +263,7 @@ Drives - 주행수 + 주행 @@ -273,7 +273,7 @@ ALL TIME - 전체 시간 + 전체 @@ -309,12 +309,12 @@ Need at least - 최소 필요 + 최소 characters! - 문자! + 자가 필요합니다! @@ -345,7 +345,7 @@ eta - 에타 + 도착 @@ -401,7 +401,7 @@ CLEAR - CLEAR + 삭제 @@ -411,14 +411,14 @@ Try the Navigation Beta - 네비게이션(베타)을 사용해보세요 + 네비게이션(베타)를 사용해보세요 - Get turn-by-turn directions displayed and more with a comma + Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai - 자세한 경로안내를 확인하시려면 comma prime을 구독하세요. -즉시등록:https://connect.comma.ai + 자세한 경로안내를 원하시면 comma prime을 구독하세요. +등록:https://connect.comma.ai @@ -437,7 +437,7 @@ location set no recent destinations - 최근 경로 없음 + 최근 목적지 없음 @@ -471,7 +471,7 @@ location set Advanced - 고급 + 고급 설정 @@ -775,12 +775,12 @@ location set Getting Started - 시작 + 설정 시작 Before we get on the road, let’s finish installation and cover some details. - 출발하기 전에 설치를 완료하고 몇 가지 세부 사항을 살펴보겠습니다. + 출발하기 전에 설정을 완료하고 몇 가지 세부 사항을 살펴보겠습니다. @@ -859,7 +859,7 @@ location set Finish Setup - 설치완료 + 설정 완료 @@ -869,7 +869,7 @@ location set Pair device - 페어링 + 장치 페어링 @@ -911,12 +911,12 @@ location set GOOD - 경고 + 좋음 OK - 좋음 + 경고 @@ -1009,7 +1009,7 @@ location set The last time openpilot successfully checked for an update. The updater only runs while the car is off. - 이전에 openpilot에서 업데이트를 성공적으로 확인한 시간입니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. + 최근에 openpilot이 업데이트를 성공적으로 확인했습니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. @@ -1019,22 +1019,22 @@ location set CHECKING - 검사중 + 확인중 Uninstall - 삭제 + 제거 UNINSTALL - 삭제 + 제거 Are you sure you want to uninstall? - 삭제하시겠습니까? + 제거하시겠습니까? @@ -1084,7 +1084,7 @@ location set Username '%1' has no keys on GitHub - 사용자 이름 '%1' GitHub에 키가 없습니다 + '%1'의 키가 GitHub에 없습니다 @@ -1094,7 +1094,7 @@ location set Username '%1' doesn't exist on GitHub - 사용자 이름 '%1' GitHub에 없습니다 + '%1'은 GitHub에 없습니다 @@ -1120,7 +1120,7 @@ location set Scroll to accept - 스크롤 허용 + 허용하려면 아래로 스크롤하세요 @@ -1168,12 +1168,12 @@ location set Display speed in km/h instead of mph. - mph가 아닌 km/h로 속도 표시. + mph 대신 km/h로 속도를 표시합니다. Record and Upload Driver Camera - 운전자 카메라 기록 및 업로드 + 운전자 카메라 녹화 및 업로드 @@ -1193,7 +1193,7 @@ location set Show ETA in 24h format - 24시간 형식으로 ETA 표시 + 24시간 형식으로 도착예정시간 표시 @@ -1275,7 +1275,7 @@ location set Forget Wi-Fi Network " - wifi 네트워크 저장안함" + wifi 네트워크 저장안함 " diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 63c17d76b7f0f99e5180d400a8143d17cbbdde55..45fae52b4286bb3e57938bf6c7c4ad53d3bd3ba1 100644 GIT binary patch delta 1686 zcmX9;2~1R16g@NZ|15uI{(uZ%Q3M8%MP#XfvdNZ$pal^W<5G=6QL8q#S|l!`ts9~U z!Ch1ka4EGWxYm6^EGW2PG$j~R5H(t;HI-<<9>XM)lmGsI_nmvrxv#NEajQsC2XL?@uH||?FuxqI`2qN`kN6%~{P}%x@f(+*dl1&|0Y<-tu(KUd zNDyj+xoH}N#xuk!2)6~mWgx_juB3CPBue3E&4FfEGoXKhKoe0aLt_0hV0Z-*Uz`Mj zS7MBA7GT?eKD6^Gn#I7UQ4rH2Zc;Sn-bEHz7lLjq4MUF>5tXS%FE1 zDgj+CCMRtr)o@HXyc~!gkLg#~j&-;@7Gzmi-~^WTvVd_m${U)1m=UO1{0-ofgIc>( zU`Pu(HZI}&8ihlcAKs#;-~~lUA=M9xR;1PW0G_^z-4D1P=AmfH5Lnl$c%-ZV zoD&rwxz)rDw-eSP}F|nIJ7Dvop$RHtaa?wQ_H>jvGppr#F$M1FpH;P9D@*Pw2U+h4GY=gngLYwV5(Tyi7V@5s23%hV%V&|1 zI!{ zOFctys~@8s#1FkB7!agZx@9n3x7Bi&KNT!eN4`v;f`6$KYc2rctJPB#)ZSB2SM>A& zZkg)y{=2MXzDoTe^#Ristg-k|GVfGPmD4Zmut{@l;WHALr0JS)fD&%fynjVyRA)6G zBRXmJ0ByWaEoZ(}o2(*X(>ZPSn~x;ALc4j%ZeVbsc7HYN*zMGw>~QCEx=v%9!4S>S zxtD4=F5`5uZ%eubv-P%a>NH9_G+vig$NBR1(iMWK!hT(IOA1p|u4}dZo)<^86gw<{ z9m!JGokn(;DCNI&=2XujMzOPUsi4!u=Q?R?A1(EKEbTmB!E~CX(nk|%O^>v%kru0Z zrK20RvLU5(Hc(^+f~%yfIov1;Qmr(E*T)s9)*+IF9!M`{FQ!zUdc~=glr~rIe)&2H zIp`A}=K}+L_0ym7B5+@!Uvys}@qGR2pUm98Wc$BvpEN0{B zTw++BvDR{s)~6e5;|?+dUO~o&NO~{LHa1V@IGWeV_D8C?-XlA$rvi4avZW^;2vN(U z+IRy6bjxEmQ9`?^@}jt}7+ya)e-8)T2lC#|7-nId{A(o-3W<>~{KpIoZjqZ}?l9aD zCT%ybMopc`9LsSIwJ|w7x9{Uk(`Xvg&VoVPO$&26=-t8+qsGzUw5j#QKJxA|wI%!x bL|Wzou>+2WYf1+wmXtjFBI3B6Q@H>CHZ-~4 delta 1659 zcmX9;dsK{R9Dcr;@0)3E-_UK=CDnAN`=(W-(rzRqk}T~yNU_*sH`2Nc8yj+|ovd3( zigjt7le^>6u0)|YSe=bsBIK}ImlaO!KF-WP&zblA-rxKDp6B_!mkWg}g+ifcY$~A4 z1o|!@<`B0KHxf?)VkaO615yys?>TWG;2%z$$Zs-iXx>tk;UgR-E5_oj|v5fN#5qZ-DF%`#%;va}Bu;F@GyC_$9c?jC}TL~SXC)-41A$}#lu5g^Qh zVVWsG_j(Ku-+⋘WIj-X^$kEw7^t%hQvarE6yaW z1B}ZR*OOVePk`c19c>IetGHY0#DYteVxS3dOi{W#m3Z#4(p(Tt1(THXl3DkYiOT&l ziTfLsRYym&PKWYj70(6dDQkR|Sg$F1S+0wd_Z%XFR&hoG1@0Cm=C%w5+}gxNQ^-h_ zE9Q$Yf$)psrUZsd8D>MH*@lkQHgxk8OUgUQELtpkFpX#t4>WE7%;&_zUzJnREb)Y% z-WcwPmmW3)9`53$Cx6p}?(Jer^kL@BB5A{^dDIKZP%sx5&?v>pUZkETrA?+M;gh9I z?HNF?k!E^UQ1dy`%*s79x0J{%+WAVk(>V?Pd!>b8GB0>775qdZntB_$M%mD9whg2A zNhSTt>49UrRI;9_&}B&FKdb;u_R_%%cd3z^^jjx2SFD%LxnERr-Jrmer)>_^FbN2@D(=CQ*@^}#v!Ng!4Id`uZ7T&sTF)B~{ZtA6u@ z3Mo&j-w%95(;e*M{AxJ+e_AU=(}eYQJNEOm!)Ci9tsX${6ph;OC8IM#<56PAAsMNO zeZiMQuGEZ6d_dj(HK}>jE9R>AG%B(JuDPfp) zRhtV3c`7lA&6H^i9vS)ng0{GeE_(f?-E?Xf1L>qKX&Og2-fFjBp{vSHZRN^h-lEi= z43@}Zw{@{(viz=nJT;qAca_RXieS91(i|Vc)J|Laad+QU2u(!x+ zed-3*b1l@*9!1s`X}o^pyYZZtMtxNg=cdn2eO)QV2p*%q+)NDY(l^(Vn7GcMs_^86 ziH0DTTO<-;i0E9!`8#W<4CB7p$#5)(81bE<#=M)Bjy2Q_A^LIczM?Nq~!G)|dg zzU;KOg6nVPp844fpM$$>?uY~WtK|WA`LYE4BPXt*P7aggxkElzt1p?>yel7TS$><9`7LdCUM}9S%jROYDXD)>}OW M3T}t&dqxKS50wtHa{vGU diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 9ac35994e61bea..04bd2415f0518e 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -415,7 +415,7 @@ - Get turn-by-turn directions displayed and more with a comma + Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai 订阅comma prime以获取导航。 立即注册:https://connect.comma.ai diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm index 208f29c0ec9dd45424301c70cdaf64c2e9a901bc..130bcfc92670cff84c6c6fcf359be599e4941e3e 100644 GIT binary patch delta 1686 zcmX9;3sg;M7~SXGbME8Zd(V|_H=@_Atco_OWJPd^Q z#Q?*2z@;3C5v%e3mzWhts%h`G6I72WX?6zKi&@PKV4i?&7ixf*2`HP%JiA1~=}LvY zN^x&lCinLXwq8Mif3nbSvp>-3hR~yc>W9<|DV1Hg{zBMro$I~yLd{T#akGS*;ts$w zSa|foBA|m!R5H#1oi~XA_iiHxp_{~bWhR+z5J$IR#vzNuO|=GIm?9ooPGVuQcru{? zu*?##r7*CUB-U54jE<3F!&Wy2%+W{=cfdMG@FO4ldNCuVC!~_c5#$TF!yD#8VF3lNFM%u;F zQt2fSu|-;!P(cGZTG3)_MeC7P^lp@jOPW}_$I|w@X=G@Z_Fi5M*q2C$zAB-lYo((m z*2essbn$*2*Y8RfA3tRcT>g-5M<1f!vSlTLn)h{)&H2-To@H`BHGrwO$|J_ICJ_O0 zhH?@x$?_!soj|BTp0ux&#obDL1?X+^>@Q~Ve43mct^$6?wA^1YLM&dNyx9Qd}m=5 zi@mB9qvPc_%_Qik){4GE>8^KL^_hbTj?wmc6i)^3Yx|d-0D8~WjuNQ7|0V5?rWU~0 zQ(NI!pF`$TwQrKH(`|a4y$dDl9H`srv5*-~(jAz3hXj&!&xUNLgcZ71kEx8NT=zEe z0no8Y->-|4?XX*)s3Bp?9({V_TN2IEugKiM{crVK_AriXf&TD4KYsT&=*;YYuNZ@0 zv7YTB8Dd`)J(sLGI>V^Zl(v&#NULOj1->;DfUd$8Lv7t)x+vRF?=p>pne88oqDDC<}j9~Oxaw`5^LTn z`*K$?p)loGh)fTJrz>YNcu{VvI2A9Bj}wa1#yQk!kMeNBOg{9?D3s5mw53MBQ|IV~ zr^fhOc|cp6G4(bFfnTg~`ZbBlO)xI_#>)Gzjg?PWlEALU^Mk06o4d(+iUfjxGWnKq zJ>aS-IBGV%(PWD6ca(IG@OxqzrnPTAWq|pn zvO@Nk*Vm@1ttvgzXu4EKbgVbkRg$3ey;-}{pAW{FyLmYS{+($I8L(SXB2Jc*}Zh8KT9KN3CeV+I2c|V>v`V#ssMS80tqV#oEib;lW32A9U z5*3CxD`FPM2#b{&hSSg}@-ZB2zE<0{)AP@Dp5J}nzx(=K*Y9`d77BTV!Y0oM3!q5_ z28<(S64QuVi0c8V6Ho^LvOnNkMI1qVNlfIP8QthUy)eaF_@Z!wO9UfvxjgwE81pH2 zh5`wJfbI!#4q(?s%w~KouwWNpJsnugnDu=io$L1Lg@3vPZh@4u9SC!SRMG|rDx`+N zJmd(e={PY7(q##7z6*JsD=@I2C{%FBii6Jg8eq7K05eg)2QiJy0l!Iz={O1u4#OxV z3FuLWxS(SE|HWlSlInHDFDPP>BF6o}+w5*&{L0tdUw{eXS;pfrA!#*l3Bjbjm4K3o z$+4SB)dN%Ztsu+QnD%2MkTviQ7ACthK{b|kGJ$Cpc3o@+!jn;x&N};tpur}dwD+NX z-4d=J66}Kq0-gy%uQE@d-wk2NMyelhM@Tr|pYd11w(E=sD?;-GiFua_H^mCT$s#-& z^cB#@LDZ+71ANLvul9S$LSUH~txqSj?c!8xRveHm?zpD#K$>`LJ&Ap!6;H)(1k6jt zs|if(EQ_}rX`>}kY%RBA!Y?(F#R0G#t8scN^V}JYdwv)z|5vjlfq91x(i~7p+_zCv zQ^!^@cW6%3@LWJ2O@sHUqAOybtVU_(-dZxaAkB@Xz}BA9vU?)|*JIL(Br?)wOF7aj zAgEj_iLGY?S-R0|-;K7%yV2#DR9g9#-aV0awfjr7KJ zOS<%+h4FUj(&K0JpvUjhy|5$fx8<@vh?a0mEwLDAkLl*vgY<(vzTma;p9m zV3g%~o>f4gUY=LIm*$oe-vCzj^0L`Wxj#i-K3D}jevtFOAra+bH@axL(RFAyhW#d& z`c~2d+YY(3fL&ool`Gd~Q;FB|;R~&Rf4zLBlbVb9@>#bl?5b9|r>y+0-M@zGWFUc(CK>kg;gCxLOg=i_%X@k!lZHyweV_PRHZsgR~l z_hH0Cpzk}YsQwKci~U8_lEdd~tacvYX`2G8qwOAC9i`|@oN?y}#iP`ULn12?FZpt) z70SfXZPYzLnY4y_MOZ0`Q>kx1QAs`@33&gbYy?{iJC$oKW7$e8ikxJJ*q-{lM^1o? zACWJGSB5_Sp_%`M`psQ*(QB8!q`ra;spv~@PNW;P`m!dvs`;R=&fUx!2I)@($Ye3Q zXr=6sbx_|iE1gokFbH+4IGFnk9;eUI>*t1P_c-7lk%mQAB`T6^Se;?ZXXTyY{8LKp z?Q8hy6IN{3%V>L=xA=W$bgN<9>sO;+=rVf#))*Z{%R=ppal@ujdTZn8NY*yI-Z-h4 z`CO(L7mg+CEIHM<<^5+&u-aHt$hmP|Y-}v282-^Y6|Ym4s!uUpo6IS*%~I{Y ztzx`M?VXd(=Cd)W?r)>0$7OX`D_<7>4eIFi)X8S7x+wBvHl;<)+s?uDxvuVb7|sC> zSNB))q9KZU@^8A__l(*c{tMvXY_@vACr?*rwvFJJ4rwvlKj`8El3^az#(aT;%xS6Z zR4B^4>1H-vonzjbl2+uXGuWRn-|i^mKpru-Mjrw~T;{SF;%kTNa(fCHMQ0sH2(GnV Ij_WM{0lCk!tN;K2 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 2920916ece1ccc..806dd54ad3a2ed 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -415,16 +415,11 @@ - Get turn-by-turn directions displayed and more with a comma + Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai 成為 comma 高級會員來使用導航功能 立即註冊:https://connect.comma.ai - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - 成為 comma 高級會員來使用導航功能,立即註冊:https://connect.comma.ai - No home From 2ae52e9b2218c23d4c80b36fb0b50387b6ecdd8f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 11 Jul 2022 17:20:56 -0700 Subject: [PATCH 310/436] process replay: ensure enabled for significant amount of time (#25121) * process replay: ensure enabled for significant amount of time * update refs * 10s is reasonable --- selfdrive/controls/controlsd.py | 3 ++- selfdrive/test/process_replay/process_replay.py | 11 +++++++++-- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/process_replay/test_processes.py | 16 ++++++++-------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 117509f1e6bfae..b344705f9da324 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -406,7 +406,8 @@ def data_sample(self): if not self.initialized: all_valid = CS.canValid and self.sm.all_checks() - if all_valid or self.sm.frame * DT_CTRL > 3.5 or SIMULATION: + timed_out = self.sm.frame * DT_CTRL > (6. if REPLAY else 3.5) + if all_valid or timed_out or SIMULATION: if not self.read_only: self.CI.init(self.CP, self.can_sock, self.pm.sock['sendcan']) self.initialized = True diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index c667aa388713c6..bea7dc46eec2d0 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -14,6 +14,7 @@ from cereal.services import service_list from common.params import Params from common.timeout import Timeout +from common.realtime import DT_CTRL from panda.python import ALTERNATIVE_EXPERIENCE from selfdrive.car.car_helpers import get_car, interfaces from selfdrive.test.process_replay.helpers import OpenpilotPrefix @@ -548,11 +549,17 @@ def cpp_replay_process(cfg, lr, fingerprint=None): def check_enabled(msgs): + cur_enabled_count = 0 + max_enabled_count = 0 for msg in msgs: if msg.which() == "carParams": if msg.carParams.notCar: return True elif msg.which() == "controlsState": if msg.controlsState.active: - return True - return False + cur_enabled_count += 1 + else: + cur_enabled_count = 0 + max_enabled_count = max(max_enabled_count, cur_enabled_count) + + return max_enabled_count > int(10. / DT_CTRL) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index fc5f83c32a953d..bef6956ec3592c 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -d583bbd9643000e7f817171c583d31ae3141a652 \ No newline at end of file +998b457e0d38e3639814ed81cb2d32e92d9bed8c \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 652c49db3d0b4d..91cc40f5ce4e08 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -18,14 +18,14 @@ original_segments = [ ("BODY", "937ccb7243511b65|2022-05-24--16-03-09--1"), # COMMA.BODY ("HYUNDAI", "02c45f73a2e5c6e9|2021-01-01--19-08-22--1"), # HYUNDAI.SONATA - ("HYUNDAI", "d824e27e8c60172c|2022-07-08--21-21-15--1"), # HYUNDAI.KIA_EV6 + ("HYUNDAI", "d824e27e8c60172c|2022-07-08--21-21-15--0"), # HYUNDAI.KIA_EV6 ("TOYOTA", "0982d79ebb0de295|2021-01-04--17-13-21--13"), # TOYOTA.PRIUS (INDI) ("TOYOTA2", "0982d79ebb0de295|2021-01-03--20-03-36--6"), # TOYOTA.RAV4 (LQR) ("TOYOTA3", "f7d7e3538cda1a2a|2021-08-16--08-55-34--6"), # TOYOTA.COROLLA_TSS2 ("HONDA", "eb140f119469d9ab|2021-06-12--10-46-24--27"), # HONDA.CIVIC (NIDEC) ("HONDA2", "7d2244f34d1bbcda|2021-06-25--12-25-37--26"), # HONDA.ACCORD (BOSCH) ("CHRYSLER", "4deb27de11bee626|2021-02-20--11-28-55--8"), # CHRYSLER.PACIFICA - ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--2"), # CHRYSLER.RAM_1500 + ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--3"), # CHRYSLER.RAM_1500 ("SUBARU", "4d70bc5e608678be|2021-01-15--17-02-04--5"), # SUBARU.IMPREZA ("GM", "0c58b6a25109da2b|2021-02-23--16-35-50--11"), # GM.VOLT ("NISSAN", "35336926920f3571|2021-02-12--18-38-48--46"), # NISSAN.XTRAIL @@ -39,13 +39,14 @@ segments = [ ("BODY", "regen660D86654BA|2022-07-06--14-27-15--0"), ("HYUNDAI", "regen657E25856BB|2022-07-06--14-26-51--0"), + ("HYUNDAI", "d824e27e8c60172c|2022-07-08--21-21-15--0"), ("TOYOTA", "regenBA97410FBEC|2022-07-06--14-26-49--0"), ("TOYOTA2", "regenDEDB1D9C991|2022-07-06--14-54-08--0"), ("TOYOTA3", "regenDDC1FE60734|2022-07-06--14-32-06--0"), ("HONDA", "regen17B09D158B8|2022-07-06--14-31-46--0"), ("HONDA2", "regen041739C3E9A|2022-07-06--15-08-02--0"), ("CHRYSLER", "regenBB2F9C1425C|2022-07-06--14-31-41--0"), - ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--2"), + ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--3"), ("SUBARU", "regen732B69F33B1|2022-07-06--14-36-18--0"), ("GM", "regen01D09D915B5|2022-07-06--14-36-20--0"), ("NISSAN", "regenEA6FB2773F5|2022-07-06--14-58-23--0"), @@ -65,7 +66,7 @@ def run_test_process(data): res = None if not args.upload_only: lr = LogReader.from_bytes(lr_dat) - res, log_msgs = test_process(cfg, lr, ref_log_path, args.ignore_fields, args.ignore_msgs) + res, log_msgs = test_process(cfg, lr, ref_log_path, cur_log_fn, args.ignore_fields, args.ignore_msgs) # save logs so we can upload when updating refs save_log(cur_log_fn, log_msgs) @@ -83,7 +84,7 @@ def get_log_data(segment): return (segment, f.read()) -def test_process(cfg, lr, ref_log_path, ignore_fields=None, ignore_msgs=None): +def test_process(cfg, lr, ref_log_path, new_log_path, ignore_fields=None, ignore_msgs=None): if ignore_fields is None: ignore_fields = [] if ignore_msgs is None: @@ -96,7 +97,7 @@ def test_process(cfg, lr, ref_log_path, ignore_fields=None, ignore_msgs=None): # check to make sure openpilot is engaged in the route if cfg.proc_name == "controlsd": if not check_enabled(log_msgs): - raise Exception(f"Route never enabled: {ref_log_path}") + return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs try: return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs @@ -216,8 +217,7 @@ def format_diff(results, ref_commit): results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) for (segment, proc, subtest_name, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): - if isinstance(result, list): - results[segment][proc + subtest_name] = result + results[segment][proc + subtest_name] = result diff1, diff2, failed = format_diff(results, ref_commit) if not upload: From 29c8e5d227eacae36dbc4357bf5328f22668d5ef Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 11 Jul 2022 17:26:59 -0700 Subject: [PATCH 311/436] Chrysler: increase Ram torque rate limit --- selfdrive/car/chrysler/values.py | 4 ++-- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index f7531792fb60e6..69dade4b6484e9 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -31,8 +31,8 @@ def __init__(self, CP): self.STEER_ERROR_MAX = 80 if CP.carFingerprint in RAM_CARS: - self.STEER_DELTA_UP = 5 - self.STEER_DELTA_DOWN = 5 + self.STEER_DELTA_UP = 6 + self.STEER_DELTA_DOWN = 6 else: self.STEER_DELTA_UP = 3 self.STEER_DELTA_DOWN = 3 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index bef6956ec3592c..d98ae9651602fa 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -998b457e0d38e3639814ed81cb2d32e92d9bed8c \ No newline at end of file +2ae52e9b2218c23d4c80b36fb0b50387b6ecdd8f \ No newline at end of file From 045c881e1ffeb5e7bcb8e1ee78d4a42caee1be4d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 11 Jul 2022 21:11:12 -0700 Subject: [PATCH 312/436] couple more stinger MYs supported --- docs/CARS.md | 2 +- selfdrive/car/hyundai/values.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index eb570faa7ec4ce..5e185ed278af65 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -105,7 +105,7 @@ How We Rate The Cars |Kia|Seltos 2021|SCC + LKAS|||||| |Kia|Sorento 2018|SCC + LKAS|||||| |Kia|Sorento 2019|SCC + LKAS|||||| -|Kia|Stinger 2018|SCC + LKAS|||||| +|Kia|Stinger 2018-20|SCC + LKAS|||||| |Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| |Lexus|NX 2018-19|All|[3](#footnotes)||||| diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 4b3acf3f2703c1..2e6a2017ea388c 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -151,7 +151,7 @@ class HyundaiCarInfo(CarInfo): HyundaiCarInfo("Kia Sorento 2018", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_c), HyundaiCarInfo("Kia Sorento 2019", video_link="https://www.youtube.com/watch?v=Fkh3s6WHJz8", harness=Harness.hyundai_e), ], - CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), + CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.hyundai_p), From 614b3a01f89eabfa9ea26ed2a367592939f78e11 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 11 Jul 2022 22:10:06 -0700 Subject: [PATCH 313/436] Chrysler: limit buttons to 20Hz (#25125) * Chrysler: limit buttons to 10Hz * cleanup * 20hz --- selfdrive/car/chrysler/carcontroller.py | 20 +++++++++++++++----- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index a7f2d007f13650..8156e7841eb308 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -1,4 +1,5 @@ from opendbc.can.packer import CANPacker +from common.realtime import DT_CTRL from selfdrive.car import apply_toyota_steer_torque_limits from selfdrive.car.chrysler.chryslercan import create_lkas_hud, create_lkas_command, create_cruise_buttons from selfdrive.car.chrysler.values import RAM_CARS, CarControllerParams @@ -14,6 +15,7 @@ def __init__(self, dbc_name, CP, VM): self.hud_count = 0 self.last_lkas_falling_edge = 0 self.lkas_active_prev = False + self.last_button_frame = 0 self.packer = CANPacker(dbc_name) self.params = CarControllerParams(CP) @@ -26,11 +28,19 @@ def update(self, CC, CS, low_speed_alert): # *** control msgs *** - das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0 - if CC.cruiseControl.cancel: - can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True)) - elif CC.enabled and CS.out.cruiseState.standstill: - can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True)) + # cruise buttons + if (self.frame - self.last_button_frame)*DT_CTRL > 0.05: + das_bus = 2 if self.CP.carFingerprint in RAM_CARS else 0 + + # ACC cancellation + if CC.cruiseControl.cancel: + self.last_button_frame = self.frame + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, cancel=True)) + + # ACC resume from standstill + elif CC.cruiseControl.resume: + self.last_button_frame = self.frame + can_sends.append(create_cruise_buttons(self.packer, CS.button_counter + 1, das_bus, resume=True)) # HUD alerts if self.frame % 25 == 0: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index d98ae9651602fa..b165b163ba5bcf 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2ae52e9b2218c23d4c80b36fb0b50387b6ecdd8f \ No newline at end of file +11e721366f1c177a84e6cb8b48171113ac3b54f9 \ No newline at end of file From 4d7b7483d70a61cf0fb938107df55c24f27da1b0 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 12 Jul 2022 12:36:57 +0200 Subject: [PATCH 314/436] Replay: tolerances per field (#25116) * tolerances per field in replay * refactor * Remove laikad parameters * Small comment change --- selfdrive/test/process_replay/compare_logs.py | 22 ++++++++++++++++--- .../test/process_replay/process_replay.py | 2 +- .../test/process_replay/test_processes.py | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py index 057e46cd9c78be..bf6daf5fed5aa9 100755 --- a/selfdrive/test/process_replay/compare_logs.py +++ b/selfdrive/test/process_replay/compare_logs.py @@ -46,11 +46,25 @@ def remove_ignored_fields(msg, ignore): return msg.as_reader() -def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=None): +def get_field_tolerance(diff_field, field_tolerances): + diff_field_str = diff_field[0] + for s in diff_field[1:]: + # loop until number in field + if not isinstance(s, str): + break + diff_field_str += '.'+s + if diff_field_str in field_tolerances: + return field_tolerances[diff_field_str] + + +def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=None, field_tolerances=None): if ignore_fields is None: ignore_fields = [] if ignore_msgs is None: ignore_msgs = [] + if field_tolerances is None: + field_tolerances = {} + default_tolerance = EPSILON if tolerance is None else tolerance log1, log2 = (list(filter(lambda m: m.which() not in ignore_msgs, log)) for log in (log1, log2)) @@ -72,7 +86,6 @@ def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=Non msg1_dict = msg1.to_dict(verbose=True) msg2_dict = msg2.to_dict(verbose=True) - tolerance = EPSILON if tolerance is None else tolerance dd = dictdiffer.diff(msg1_dict, msg2_dict, ignore=ignore_fields) # Dictdiffer only supports relative tolerance, we also want to check for absolute @@ -80,10 +93,13 @@ def compare_logs(log1, log2, ignore_fields=None, ignore_msgs=None, tolerance=Non def outside_tolerance(diff): try: if diff[0] == "change": + field_tolerance = default_tolerance + if (tol := get_field_tolerance(diff[1], field_tolerances)) is not None: + field_tolerance = tol a, b = diff[2] finite = math.isfinite(a) and math.isfinite(b) if finite and isinstance(a, numbers.Number) and isinstance(b, numbers.Number): - return abs(a - b) > max(tolerance, tolerance * max(abs(a), abs(b))) + return abs(a - b) > max(field_tolerance, field_tolerance * max(abs(a), abs(b))) except TypeError: pass return True diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index bea7dc46eec2d0..0c642cde178999 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -28,7 +28,7 @@ PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'init_callback', 'should_recv_callback', 'tolerance', 'fake_pubsubmaster', 'submaster_config', 'environ', 'subtest_name'], defaults=({}, {}, "")) +ProcessConfig = namedtuple('ProcessConfig', ['proc_name', 'pub_sub', 'ignore', 'init_callback', 'should_recv_callback', 'tolerance', 'fake_pubsubmaster', 'submaster_config', 'environ', 'subtest_name', "field_tolerances"], defaults=({}, {}, "", {})) def wait_for_event(evt): diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 91cc40f5ce4e08..d8cd1fd57a04a9 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -100,7 +100,7 @@ def test_process(cfg, lr, ref_log_path, new_log_path, ignore_fields=None, ignore return f"Route did not enable at all or for long enough: {new_log_path}", log_msgs try: - return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance), log_msgs + return compare_logs(ref_log_msgs, log_msgs, ignore_fields + cfg.ignore, ignore_msgs, cfg.tolerance, cfg.field_tolerances), log_msgs except Exception as e: return str(e), log_msgs From f0b5ff5c1addff7932fe86a2874a23d87b5eb5f3 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 12 Jul 2022 14:03:35 +0200 Subject: [PATCH 315/436] Replay: Fix --upload-only (#25127) Add checking for list back --- selfdrive/test/process_replay/test_processes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index d8cd1fd57a04a9..77d73a4ff4afe5 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -217,7 +217,8 @@ def format_diff(results, ref_commit): results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) for (segment, proc, subtest_name, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): - results[segment][proc + subtest_name] = result + if isinstance(result, list): + results[segment][proc + subtest_name] = result diff1, diff2, failed = format_diff(results, ref_commit) if not upload: From 205f6f7414f502248082949addac25a215c73d59 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 12 Jul 2022 16:09:21 +0200 Subject: [PATCH 316/436] casync: manifest compare script (#25129) * casync compare script * typo * cleanup output --- .../tici/tests/compare_casync_manifest.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100755 system/hardware/tici/tests/compare_casync_manifest.py diff --git a/system/hardware/tici/tests/compare_casync_manifest.py b/system/hardware/tici/tests/compare_casync_manifest.py new file mode 100755 index 00000000000000..5e5fa24556e4fd --- /dev/null +++ b/system/hardware/tici/tests/compare_casync_manifest.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +import argparse +import collections +import multiprocessing +import os +from typing import Dict, List + +import requests +from tqdm import tqdm + +import system.hardware.tici.casync as casync + + +def get_chunk_download_size(chunk): + sha = chunk.sha.hex() + path = os.path.join(remote_url, sha[:4], sha + ".cacnk") + if os.path.isfile(path): + return os.path.getsize(path) + else: + r = requests.head(path) + r.raise_for_status() + return int(r.headers['content-length']) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description='Compute overlap between two casync manifests') + parser.add_argument('frm') + parser.add_argument('to') + args = parser.parse_args() + + frm = casync.parse_caibx(args.frm) + to = casync.parse_caibx(args.to) + remote_url = args.to.replace('.caibx', '') + + most_common = collections.Counter(t.sha for t in to).most_common(1)[0][0] + + frm_dict = casync.build_chunk_dict(frm) + + # Get content-length for each chunk + with multiprocessing.Pool() as pool: + szs = list(tqdm(pool.imap(get_chunk_download_size, to), total=len(to))) + chunk_sizes = {t.sha: sz for (t, sz) in zip(to, szs)} + + sources: Dict[str, List[int]] = { + 'seed': [], + 'remote_uncompressed': [], + 'remote_compressed': [], + } + + for chunk in to: + # Assume most common chunk is the zero chunk + if chunk.sha == most_common: + continue + + if chunk.sha in frm_dict: + sources['seed'].append(chunk.length) + else: + sources['remote_uncompressed'].append(chunk.length) + sources['remote_compressed'].append(chunk_sizes[chunk.sha]) + + print() + print("Update statistics (excluding zeros)") + print() + print("Download only with no seed:") + print(f" Remote (uncompressed)\t\t{sum(sources['seed'] + sources['remote_uncompressed']) / 1000 / 1000:.2f} MB\tn = {len(to)}") + print(f" Remote (compressed download)\t{sum(chunk_sizes.values()) / 1000 / 1000:.2f} MB\tn = {len(to)}") + print() + print("Upgrade with seed partition:") + print(f" Seed (uncompressed)\t\t{sum(sources['seed']) / 1000 / 1000:.2f} MB\t\t\t\tn = {len(sources['seed'])}") + sz, n = sum(sources['remote_uncompressed']), len(sources['remote_uncompressed']) + print(f" Remote (uncompressed)\t\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") + sz, n = sum(sources['remote_compressed']), len(sources['remote_compressed']) + print(f" Remote (compressed download)\t{sz / 1000 / 1000:.2f} MB\t(avg {sz / 1000 / 1000 / n:4f} MB)\tn = {n}") From 105afee4a21ad008fa741f19c5a62a6d52fdd773 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Tue, 12 Jul 2022 18:11:47 +0200 Subject: [PATCH 317/436] Laikad: set cache dir to comma_download_cache (#25131) * Cache downloads for process replay * set cache dir permanent * Create constant --- selfdrive/locationd/laikad.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 0954cb4c9f03cb..4868e8ae5235e3 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -27,6 +27,7 @@ MAX_TIME_GAP = 10 EPHEMERIS_CACHE = 'LaikadEphemeris' +DOWNLOADS_CACHE_FOLDER = "/tmp/comma_download_cache" CACHE_VERSION = 0.1 POS_FIX_RESIDUAL_THRESHOLD = 100.0 @@ -42,7 +43,7 @@ def __init__(self, valid_const=("GPS", "GLONASS"), auto_fetch_orbits=True, auto_ valid_ephem_types: Valid ephemeris types to be used by AstroDog save_ephemeris: If true saves and loads nav and orbit ephemeris to cache. """ - self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True) + self.astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, clear_old_ephemeris=True, cache_dir=DOWNLOADS_CACHE_FOLDER) self.gnss_kf = GNSSKalman(GENERATED_DIR, cython=True) self.auto_fetch_orbits = auto_fetch_orbits @@ -183,7 +184,7 @@ def init_gnss_localizer(self, est_pos): def fetch_orbits(self, t: GPSTime, block): # Download new orbits if 1 hour of orbits data left if t + SECS_IN_HR not in self.astro_dog.orbit_fetched_times and (self.last_fetch_orbits_t is None or abs(t - self.last_fetch_orbits_t) > SECS_IN_MIN): - astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types + astro_dog_vars = self.astro_dog.valid_const, self.astro_dog.auto_update, self.astro_dog.valid_ephem_types, self.astro_dog.cache_dir ret = None if block: # Used for testing purposes @@ -203,8 +204,8 @@ def fetch_orbits(self, t: GPSTime, block): self.cache_ephemeris(t=t) -def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types): - astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types) +def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types, cache_dir): + astro_dog = AstroDog(valid_const=valid_const, auto_update=auto_update, valid_ephem_types=valid_ephem_types, cache_dir=cache_dir) cloudlog.info(f"Start to download/parse orbits for time {t.as_datetime()}") start_time = time.monotonic() try: @@ -301,6 +302,7 @@ def main(sm=None, pm=None): replay = "REPLAY" in os.environ use_internet = "LAIKAD_NO_INTERNET" not in os.environ laikad = Laikad(save_ephemeris=not replay, auto_fetch_orbits=use_internet) + while True: sm.update() From 780c60324bf19762e9b72da38be0a9f09d624efd Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 12 Jul 2022 11:12:35 -0700 Subject: [PATCH 318/436] process replay: fix string failures after #25127 --- selfdrive/test/process_replay/test_processes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 77d73a4ff4afe5..4ebb0701dd44aa 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -217,7 +217,7 @@ def format_diff(results, ref_commit): results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) for (segment, proc, subtest_name, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): - if isinstance(result, list): + if not args.upload_only: results[segment][proc + subtest_name] = result diff1, diff2, failed = format_diff(results, ref_commit) From 1f17f812cfa74c1db2ceba05dae4946b5e472e2a Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Tue, 12 Jul 2022 15:45:43 -0500 Subject: [PATCH 319/436] Add missing RAV4H_TSS2_2022 engine f/w (#25111) `@Rocks#8913` 2021 RAV4 Hybrid (Italy) DongleID/route 081a1d5f242294c0|2022-07-10--17-39-33 Continental camera p/n 8646C-0R090... https://discord.com/channels/469524606043160576/524327905937850394/995781101903679558 --- selfdrive/car/toyota/values.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 2a03999342d03d..723fa85820db34 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1381,11 +1381,12 @@ class ToyotaCarInfo(CarInfo): b'8965B42172\x00\x00\x00\x00\x00\x00', ], (Ecu.engine, 0x700, None): [ - b'\x01896634A62000\x00\x00\x00\x00', - b'\x01896634A08000\x00\x00\x00\x00', - b'\x01896634A61000\x00\x00\x00\x00', b'\x01896634A02001\x00\x00\x00\x00', b'\x01896634A03000\x00\x00\x00\x00', + b'\x01896634A08000\x00\x00\x00\x00', + b'\x01896634A61000\x00\x00\x00\x00', + b'\x01896634A62000\x00\x00\x00\x00', + b'\x01896634A63000\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F0R01100\x00\x00\x00\x00', From ecac734160f719f59364a637b7cd781460456845 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 12 Jul 2022 13:51:25 -0700 Subject: [PATCH 320/436] update compatibility docs with VIN data (#25134) * start with genesis * chrysler * honda * toyota * subaru --- docs/CARS.md | 46 ++++++++++++++++---------------- selfdrive/car/chrysler/values.py | 4 +-- selfdrive/car/honda/values.py | 20 +++++++------- selfdrive/car/hyundai/values.py | 6 ++--- selfdrive/car/subaru/values.py | 6 ++--- selfdrive/car/toyota/values.py | 10 +++---- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 5e185ed278af65..c3efe87515f4ed 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -49,12 +49,12 @@ How We Rate The Cars |Kia|Niro Electric 2021|All|||||| |Kia|Niro Electric 2022|All|||||| |Kia|Telluride 2020|SCC + LKAS|||||| -|Lexus|ES 2019-21|All|||||| +|Lexus|ES 2019-22|All|||||| |Lexus|ES Hybrid 2019-22|All|||||| -|Lexus|NX 2020|All|||||| -|Lexus|NX Hybrid 2020|All|||||| +|Lexus|NX 2020-21|All|||||| +|Lexus|NX Hybrid 2020-21|All|||||| |Lexus|RX 2020-22|All|||||| -|Lexus|UX Hybrid 2019-21|All|||||| +|Lexus|UX Hybrid 2019-22|All|||||| |Toyota|Avalon 2022|All|||||| |Toyota|Avalon Hybrid 2022|All|||||| |Toyota|Camry 2021-22|All||[4](#footnotes)|||| @@ -80,8 +80,8 @@ How We Rate The Cars |Audi|RS3 2018|ACC + Lane Assist|||||| |Audi|S3 2015-17|ACC + Lane Assist|||||| |Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| -|Genesis|G70 2018|All|||||| -|Genesis|G80 2018|All|||||| +|Genesis|G70 2018-19|All|||||| +|Genesis|G80 2017-19|All|||||| |Hyundai|Elantra 2021-22|SCC + LKAS|||||| |Hyundai|Elantra Hybrid 2021-22|SCC + LKAS|||||| |Hyundai|Ioniq Electric 2020|SCC + LKAS|||||| @@ -117,10 +117,10 @@ How We Rate The Cars |Nissan|X-Trail 2017|ProPILOT|||||| |SEAT|Ateca 2018|Driver Assistance|||||| |SEAT|Leon 2014-20|Driver Assistance|||||| -|Subaru|Ascent 2019-20|All|||||| +|Subaru|Ascent 2019-21|All|||||| |Subaru|Crosstrek 2020-21|EyeSight|||||| -|Subaru|Forester 2019-21|All|||||| -|Subaru|Impreza 2020-21|EyeSight|||||| +|Subaru|Forester 2019-22|All|||||| +|Subaru|Impreza 2020-22|EyeSight|||||| |Subaru|XV 2020-21|EyeSight|||||| |Toyota|Alphard 2019-20|All|||||| |Toyota|Alphard Hybrid 2021|All|||||| @@ -151,7 +151,7 @@ How We Rate The Cars |---|---|---|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-19|AcuraWatch Plus|||||| |Acura|RDX 2016-18|AcuraWatch Plus|||||| -|Acura|RDX 2019-21|All|||||| +|Acura|RDX 2019-22|All|||||| |Audi|Q2 2018|ACC + Lane Assist|||||| |Audi|Q3 2020-21|ACC + Lane Assist|||||| |Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| @@ -159,27 +159,27 @@ How We Rate The Cars |Chrysler|Pacifica 2019-20|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| -|Genesis|G90 2018|All|||||| +|Genesis|G90 2017-18|All|||||| |GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| -|Honda|Accord 2018-21|All|||||| -|Honda|Accord Hybrid 2018-21|All|||||| +|Honda|Accord 2016-22|All|||||| +|Honda|Accord Hybrid 2018-22|All|||||| |Honda|Civic 2016-18|Honda Sensing|||||| -|Honda|Civic 2019-20|All|||[2](#footnotes)||| +|Honda|Civic 2019-21|All|||[2](#footnotes)||| |Honda|Civic 2022|All|||||| |Honda|Civic Hatchback 2017-21|Honda Sensing|||||| |Honda|Civic Hatchback 2022|All|||||| |Honda|CR-V 2015-16|Touring|||||| -|Honda|CR-V 2017-21|Honda Sensing|||||| +|Honda|CR-V 2017-22|Honda Sensing|||||| |Honda|CR-V Hybrid 2017-19|Honda Sensing|||||| |Honda|e 2020|All|||||| -|Honda|Fit 2018-19|Honda Sensing|||||| +|Honda|Fit 2018-20|Honda Sensing|||||| |Honda|Freed 2020|Honda Sensing|||||| -|Honda|HR-V 2019-20|Honda Sensing|||||| -|Honda|Insight 2019-21|All|||||| +|Honda|HR-V 2019-22|Honda Sensing|||||| +|Honda|Insight 2019-22|All|||||| |Honda|Inspire 2018|All|||||| -|Honda|Odyssey 2018-20|Honda Sensing|||||| +|Honda|Odyssey 2018-22|Honda Sensing|||||| |Honda|Passport 2019-21|All|||||| -|Honda|Pilot 2016-21|Honda Sensing|||||| +|Honda|Pilot 2016-22|Honda Sensing|||||| |Honda|Ridgeline 2017-22|Honda Sensing|||||| |Hyundai|Elantra 2017-19|SCC + LKAS|||||| |Hyundai|Genesis 2015-16|SCC + LKAS|||||| @@ -190,16 +190,16 @@ How We Rate The Cars |Hyundai|Tucson 2021|SCC + LKAS|||||| |Hyundai|Veloster 2019-20|SCC + LKAS|||||| |Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| -|Jeep|Grand Cherokee 2019-20|Adaptive Cruise|||||| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise|||||| |Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| |Kia|Optima 2017|SCC + LKAS|||||| |Lexus|IS 2017-19|All|||||| -|Lexus|RC 2020|All|||||| +|Lexus|RC 2017-2020|All|||||| |Lexus|RX 2016-18|All|[3](#footnotes)||||| |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| |Mazda|CX-5 2022|All|||||| |Mazda|CX-9 2021|All|||||| -|Ram|1500 2019-21|Adaptive Cruise|||||| +|Ram|1500 2019-22|Adaptive Cruise|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| |Subaru|XV 2018-19|EyeSight|||||| diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 69dade4b6484e9..80baba9bd68a5d 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -53,8 +53,8 @@ class ChryslerCarInfo(CarInfo): CAR.PACIFICA_2018: ChryslerCarInfo("Chrysler Pacifica 2017-18"), CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2019-20"), CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), - CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-20", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), - CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-21"), + CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), + CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-22"), } # Unique CAN messages: diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index c6e20f2d835ca1..bfa42bd5095732 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -109,13 +109,13 @@ class HondaCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.ACCORD: [ - HondaCarInfo("Honda Accord 2018-21", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Accord 2016-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), ], - CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), CAR.CIVIC: HondaCarInfo("Honda Civic 2016-18", harness=Harness.nidec), CAR.CIVIC_BOSCH: [ - HondaCarInfo("Honda Civic 2019-20", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Civic 2019-21", "All", video_link="https://www.youtube.com/watch?v=4Iz1Mz5LGF8", footnotes=[Footnote.CIVIC_DIESEL], min_steer_speed=2. * CV.MPH_TO_MS, harness=Harness.bosch_a), HondaCarInfo("Honda Civic Hatchback 2017-21", harness=Harness.bosch_a), ], CAR.CIVIC_BOSCH_DIESEL: None, # same platform @@ -125,20 +125,20 @@ class HondaCarInfo(CarInfo): ], CAR.ACURA_ILX: HondaCarInfo("Acura ILX 2016-19", "AcuraWatch Plus", min_steer_speed=25. * CV.MPH_TO_MS, harness=Harness.nidec), CAR.CRV: HondaCarInfo("Honda CR-V 2015-16", "Touring", harness=Harness.nidec), - CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-21", harness=Harness.bosch_a), + CAR.CRV_5G: HondaCarInfo("Honda CR-V 2017-22", harness=Harness.bosch_a), CAR.CRV_EU: None, # HondaCarInfo("Honda CR-V EU", "Touring"), # Euro version of CRV Touring CAR.CRV_HYBRID: HondaCarInfo("Honda CR-V Hybrid 2017-19", harness=Harness.bosch_a), - CAR.FIT: HondaCarInfo("Honda Fit 2018-19", harness=Harness.nidec), + CAR.FIT: HondaCarInfo("Honda Fit 2018-20", harness=Harness.nidec), CAR.FREED: HondaCarInfo("Honda Freed 2020", harness=Harness.nidec), - CAR.HRV: HondaCarInfo("Honda HR-V 2019-20", harness=Harness.nidec), - CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-20", min_steer_speed=0., harness=Harness.nidec), + CAR.HRV: HondaCarInfo("Honda HR-V 2019-22", harness=Harness.nidec), + CAR.ODYSSEY: HondaCarInfo("Honda Odyssey 2018-22", min_steer_speed=0., harness=Harness.nidec), CAR.ODYSSEY_CHN: None, # Chinese version of Odyssey CAR.ACURA_RDX: HondaCarInfo("Acura RDX 2016-18", "AcuraWatch Plus", harness=Harness.nidec), - CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), - CAR.PILOT: HondaCarInfo("Honda Pilot 2016-21", harness=Harness.nidec), + CAR.ACURA_RDX_3G: HondaCarInfo("Acura RDX 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.PILOT: HondaCarInfo("Honda Pilot 2016-22", harness=Harness.nidec), CAR.PASSPORT: HondaCarInfo("Honda Passport 2019-21", "All", harness=Harness.nidec), CAR.RIDGELINE: HondaCarInfo("Honda Ridgeline 2017-22", harness=Harness.nidec), - CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-21", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + CAR.INSIGHT: HondaCarInfo("Honda Insight 2019-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), CAR.HONDA_E: HondaCarInfo("Honda e 2020", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), } diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 2e6a2017ea388c..6e184ce9ef11bf 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -156,10 +156,10 @@ class HyundaiCarInfo(CarInfo): CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.hyundai_p), # Genesis - CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018", "All", harness=Harness.hyundai_f), + CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", harness=Harness.hyundai_f), CAR.GENESIS_G70_2020: HyundaiCarInfo("Genesis G70 2020", "All", harness=Harness.hyundai_f), - CAR.GENESIS_G80: HyundaiCarInfo("Genesis G80 2018", "All", harness=Harness.hyundai_h), - CAR.GENESIS_G90: HyundaiCarInfo("Genesis G90 2018", "All", harness=Harness.hyundai_c), + CAR.GENESIS_G80: HyundaiCarInfo("Genesis G80 2017-19", "All", harness=Harness.hyundai_h), + CAR.GENESIS_G90: HyundaiCarInfo("Genesis G90 2017-18", "All", harness=Harness.hyundai_c), } class Buttons: diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index ea923b1b5027ed..8fac934285debf 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -41,18 +41,18 @@ class SubaruCarInfo(CarInfo): CAR_INFO: Dict[str, Union[SubaruCarInfo, List[SubaruCarInfo]]] = { - CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-20", "All"), + CAR.ASCENT: SubaruCarInfo("Subaru Ascent 2019-21", "All"), CAR.IMPREZA: [ SubaruCarInfo("Subaru Impreza 2017-19"), SubaruCarInfo("Subaru Crosstrek 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), SubaruCarInfo("Subaru XV 2018-19", video_link="https://youtu.be/Agww7oE1k-s?t=26"), ], CAR.IMPREZA_2020: [ - SubaruCarInfo("Subaru Impreza 2020-21"), + SubaruCarInfo("Subaru Impreza 2020-22"), SubaruCarInfo("Subaru Crosstrek 2020-21"), SubaruCarInfo("Subaru XV 2020-21"), ], - CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), + CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-22", "All"), CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), CAR.LEGACY_PREGLOBAL: SubaruCarInfo("Subaru Legacy 2015-18"), CAR.OUTBACK_PREGLOBAL: SubaruCarInfo("Subaru Outback 2015-17"), diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 723fa85820db34..f47ab87040248b 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -124,7 +124,7 @@ class ToyotaCarInfo(CarInfo): ], CAR.COROLLAH_TSS2: [ ToyotaCarInfo("Toyota Corolla Hybrid 2020-22"), - ToyotaCarInfo("Lexus UX Hybrid 2019-21"), + ToyotaCarInfo("Lexus UX Hybrid 2019-22"), ], CAR.HIGHLANDER: ToyotaCarInfo("Toyota Highlander 2017-19", video_link="https://www.youtube.com/watch?v=0wS0wXSLzoo", footnotes=[Footnote.DSU]), CAR.HIGHLANDER_TSS2: ToyotaCarInfo("Toyota Highlander 2020-22"), @@ -151,14 +151,14 @@ class ToyotaCarInfo(CarInfo): # Lexus CAR.LEXUS_CTH: ToyotaCarInfo("Lexus CT Hybrid 2017-18", "LSS", footnotes=[Footnote.DSU]), CAR.LEXUS_ESH: ToyotaCarInfo("Lexus ES Hybrid 2017-18", "LSS", footnotes=[Footnote.DSU]), - CAR.LEXUS_ES_TSS2: ToyotaCarInfo("Lexus ES 2019-21"), + CAR.LEXUS_ES_TSS2: ToyotaCarInfo("Lexus ES 2019-22"), CAR.LEXUS_ESH_TSS2: ToyotaCarInfo("Lexus ES Hybrid 2019-22", video_link="https://youtu.be/BZ29osRVJeg?t=12"), CAR.LEXUS_IS: ToyotaCarInfo("Lexus IS 2017-19"), CAR.LEXUS_NX: ToyotaCarInfo("Lexus NX 2018-19", footnotes=[Footnote.DSU]), CAR.LEXUS_NXH: ToyotaCarInfo("Lexus NX Hybrid 2018-19", footnotes=[Footnote.DSU]), - CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020"), - CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020"), - CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2020"), + CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020-21"), + CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020-21"), + CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2017-2020"), CAR.LEXUS_RX: ToyotaCarInfo("Lexus RX 2016-18", footnotes=[Footnote.DSU]), CAR.LEXUS_RXH: ToyotaCarInfo("Lexus RX Hybrid 2016-19", footnotes=[Footnote.DSU]), CAR.LEXUS_RX_TSS2: ToyotaCarInfo("Lexus RX 2020-22"), From ea449f1fe0bbff0eff5b12d64f0b5e75b7983998 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 12 Jul 2022 14:08:17 -0700 Subject: [PATCH 321/436] Use upstream wait-on-check-action action (#25126) Use upstream lewagon action --- .github/workflows/prebuilt.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index 7acc8a2254f1e7..99d9694f24ae02 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -25,7 +25,7 @@ jobs: IMAGE_NAME: openpilot-prebuilt steps: - name: Wait for green check mark - uses: commaai/wait-on-check-action@f16fc3bb6cd4886520b4e9328db1d42104d5cadc + uses: lewagon/wait-on-check-action@e2558238c09778af25867eb5de5a3ce4bbae3dcd with: ref: master wait-interval: 30 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fb5a37eeef23dc..8df89dcc384a1a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'commaai/openpilot' steps: - name: Wait for green check mark - uses: commaai/wait-on-check-action@f16fc3bb6cd4886520b4e9328db1d42104d5cadc + uses: lewagon/wait-on-check-action@e2558238c09778af25867eb5de5a3ce4bbae3dcd with: ref: master wait-interval: 30 From b632d56244c5fd6821477a642d6f151ea4b13b20 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Tue, 12 Jul 2022 16:39:13 -0700 Subject: [PATCH 322/436] UI: change wording for dcam preview description (#25142) --- selfdrive/ui/qt/offroad/settings.cc | 2 +- selfdrive/ui/translations/main_ja.ts | 4 ++-- selfdrive/ui/translations/main_ko.qm | Bin 20040 -> 19981 bytes selfdrive/ui/translations/main_ko.ts | 4 ++-- selfdrive/ui/translations/main_zh-CHS.qm | Bin 18508 -> 18469 bytes selfdrive/ui/translations/main_zh-CHS.ts | 4 ++-- selfdrive/ui/translations/main_zh-CHT.qm | Bin 18568 -> 18509 bytes selfdrive/ui/translations/main_zh-CHT.ts | 6 +++--- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index d5b8d4bbd132c7..9aeb966ccfe32e 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -102,7 +102,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { // offroad-only buttons auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"), - tr("Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off)")); + tr("Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off)")); connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); addItem(dcamBtn); diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 5c0f54a3149b6b..f3b8733128cfcd 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -134,8 +134,8 @@ - Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - ドライバーカメラのプレビューにより、デバイスの取り付け位置を最適化し、最高のドライバーモニタリング体験を提供します。(車両の電源を切る必要があります) + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) + ドライバー向けカメラをプレビューする、ドライバーモニタリングの視認性を確保します。(車両の電源を切る必要があります) diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm index c5c66d1e73491c2ff09d91b6fdd5200002b2a6da..40b0bb65a38d65ee901ac7a85257e88868b4513c 100644 GIT binary patch delta 1903 zcmX9;Yfx2H7+v?=bMEupb1%xnv5>m`N23+hBMX66Gk z3REhz6o^d786aOtzL1B83 z7}gP&0zTV_{I|2PWxJT56j%%-hyzN)w4{`mcz^qb;mDd1`3bBpIE`T7QFmrF$=$mg~vO&eh5!!L78*TA!^bT;N{|&LK<{_){5O-h8BF+`-f7nVpYQ*N(Yk&kr zJS0;W<(_!v*Iv$Ni)U`#p^!uM;??QRw4YTnO(5ZMsgk8MpZsa1I3<_@*`@i5DafQ~ z$zl4APU(|!f@;ZAg_QHeQKmTJKivPdwEDGGj9(+Y8KtlX;-u1TEX0)WM)kfM10viQ zA1hUb)=)_ARH^C%IzkSYYTjE%CMu+T?S15Dnsj0?h|Sv|o%BCP0rXPuB^DICUFu)k zNud+nIBTEuXpjYtgHG^IqjPd}%3V7dd`UOuW&#<^)y-~f0j53BWoXFz$Thm^hmV24 zak@vzJtef*Q~H1qQWYxbYlpAp%?9=Ra{HOMKz}!NH>sPb|K}DN(caPjJNY^=qSFu; z(#n_Ph#^VKLTwif*|-0t}eJv?V*kH_9K#E8GXv}Jh1x9xpw}8$D>mE(1$ipLak)DlzWk!t$UVXC_DA_eI(?d+nk;&@ZTh6?gi zZ8{t-u~X-iza{yUY&P9^)kQX7*6e?qt2T4su~W>YF(+K!2n>xgFTBb=5B}bqe_kX> zbIfasJV}nl-1a+JAN`cM<2jPy{f_K;jM>7^$^MO;A5|%bOc8_PRRkCM~3^1KrNdGhjkEMMx9w?BG`2kep?KVex&5b#&hFs zW#gyp-_QYN$MqSMJx}?nhKZv8R$3lV?h$3m*%?35zbCAQKlt()lB}LH*|9PGR_|XQ z0}}-6oNGKUs>Pb@qJNDxYp!D;f`T{_~&j z{9m2@f7hRVh_i~*uYh>1WEv2%~R&*08W0Nkh}Y*$*w{O+dc-;TOjNo z1XK!y=6EKpfYA9j;zsN;4^VTD54}v6}kL z71PY&q-_%mma!po)`%t2MZok2@r|%r>U3Rv!|Nz%Gx0I=zY;gTzJc+L;+yei-hs{H zjt@yhsvE=L*f9)QHHP!w65UZXY-B*S=zgD$kXMQ|?{1|M*Tt_}Z&RO*V$*0CFK@kg zC75@@Cspk0C!z3vigzm7+2}1}nEpT<870Bz61D2!JUXjZYkm|>1wYUx-Or$cc5SBT z0x&O5yF^9Zr#5Pj4L=1!OSO&B2V8)`M>~?+L#yTMLLwKph>aHXX_3vGAdmIg8WQ&P*IR#jLWytatJfX`g0|~FIl(&q zKKe6vL;0R#(3$>32fbzpb?bRmA`B_NxJr03mEo0~K{9PJE3cDrAsub&TeLWjxqGoRT4@vX56ael-O)sI>4bYMQ2=nLm=@A<2&p8 zsG!N%@*`U`E#KJoA{7g`Ap2b+fjC)q1$+1*;)eWe@ zl}gMjlk@knU~r{e`jVGer~J{#A9;d*%bwl5Ys!am`w=QPxQ5R?ZQZfw zFGH!j(9_$R4y9G-aq@p{SAjRKwf0)1cde_yTg#jMue_dRO%u4bz3Ggn%`=E-D2TWg zajyO9&wJ{9rs}sXbXeEftn2rb&Yzg;EK^okSCuPeG4b? - Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - 운전자 카메라를 미리 보면서 최적의 운전자 모니터링 경험을 위해 장치의 장착 위치를 최적화할수 있습니다. (차량연결은 해제되어있어야 합니다) + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) + 운전자 모니터링이 좋은 가시성을 갖도록 운전자를 향한 카메라를 미리 봅니다. (차량연결은 해제되어있어야 합니다) diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 45fae52b4286bb3e57938bf6c7c4ad53d3bd3ba1..2c7963cf17a496e356fb5b0372b565ac82514661 100644 GIT binary patch delta 1858 zcmX9;c~n$o9KAF1-Yjos-a{S41!UL+6j=%v1Q9VYutb!QLNf^(#t;$)lw8Qv6e+{N zB*hgF%gl4ousB+pNTOkJva&doq6lb2+bI&ku7~;O&ij_%cfWh@@B6AmRb8SgaV>lb zP!9kT0s)PUxPZ8tm_yu6ECU42%LYIU0w#V*3-2adpVN7n%}{jv0X6XzdbWtu1VzlAje+;5J+q03i+C=+TO&jbEZXc!UC zi=0Bo_B_rPt44+epb*~6RipNMQ|lj9lXuY1kWs3HMt{IZquTQw$EOXbt|kgBoUFR8 zt_0kys_w@(17rSDOSX%EUxnJIqn-P%t79YwiS1M`apH}GFRJ%lV;0;p)u-PivBx6S zpT*K^WvTkx1lAoVs@s}Uo&W-Ot8W~12F509gg_U-5}oy9Y>C6; zg0=VKzbj=T|$hyUYlXSfc4^z5y-M@FKh^AiGAKnSL-qX+a zZ{W*Or;pQ+s8Xg+@98JOB>h`?dpK{?A2`N3BUb4@?(pJwfI(+o#^lU0cva}xB0h#$ zf7-bI4Z}0@Cf%m=0fwYalq~9~A!P|A9`9;MYm6o#7gL1Z9!{LWz`rULcPjioAe9Z$NT1VE`I$-{-y>CAe};y%Nc)>xtf0%3!u zdC|P%^m0?uF4ik_&oHf=Px|6w)9(9=S)j&LyOZrRuE5lEkfH=nFn!fZ4E)~I+DL-J z4zsq}n|QNz z|N8wX@9T`B?LP7cRXk|&X!-0vwA=NRd^L)1w2MX2|IU|B*Qi)#v12E9D32lK0cP06x#gixxvO3C#tv^hh0FFlKrJybq_4W6w{|v98R{vOBVK>{&Kz#`0XNEjv5Sns3jwr`j{^ Mj)Id#P8X*B4>phSf&c&j delta 1954 zcmX9;4Nz3)6+Qd*z5RK+`yS{@tWjAPP!PnWK@>$nXdt2m5kw|v%m6RzCbA$dAWd{* z#3+#>4jPr9!3Z&_wV6Sqi7{#vu^2&R#e*;unC(?}`hMrUQ}xCe8t(W)SnaM;k#W z_ZuDp0XqTX1mdT}c;Y{ZX8~I+V7&;090n#|0|I{sOyRyIfvJ4p#=w2R^VNW>14!S> zLNkFy*~B-2tQaQwjkpR33M6jkdMB{@01K1>Yo8K-1$@KbckK8%*#9lWE$;#I9zxvH z2k2yo?Xe764zcSqVk^WeA`m%g>wNW04pV>jRha{F^RgYjtC%Zs;==m z*W*KV-8mu?`*k;jgTRC|-M#0kfoI1FvikxMeqIRczspN=*zR1c;<`x!wQ*4Df+hKisBS>4Sao|I z;J7QkQ9wF|DshYWUm)(HxM#sB+A($nRp$uW&y1iWL~LpqB&&Ji2Yp4v4dP#~>;jzU z#AC0uP*1n`xs`LV^@?Br*h8B-#IJv%)%}4F#M_C-Xg|Ls$5HUP|B-Cl)&R4*r4%KM zw3kVFFLNMqDB((kB;hQJ-Mx+)yYF?{?|@q;Ke` z`%+IY358@xKWyp*LNAYCVvzKBhykSF_c=incGNiSA3C!MT z$k$Q#Fwt;u@F_59nc-CQUO$)eqqvnenO(_O@A(L1jg5y-apJdqxYCT z?GNF5w%KS~Ne8VmhcuaZR2G@%KlHm9aKyZHVIM~jWzOA1>5{IP^OsZV$tmWdj${&= zVy*)nhWE|adNSy!1Loeq5?)J(<@u4**`XpAUF~AWX>!dyUOB-9#JTMBfV}OF%J&X= z=TnY2?54cu)ImDcE;rp+%25r<`~6*qL}sn>IX43(QErzf@^<-3 zZVyTzq3iP9LLa3Iv*?a*pwyL?kh2#^D9Dm}vj!M7)w1F?FNKixmNnmrBwl0LSYc=U z6HCW^j%Z4r_Om1qEn5AP+W29zYMnN>ie`CaO-_H>Y-SZ6b| zhO%I~ZTf_Nl1RKQeuzY$J#9O}s|`-O-*%#s5AjvDcISumLbk0vnK&iJb}4~gkPB_s z@_0P$Z!2RyZsq!*GHweM80}D;gDJoagEH$@6wUWQSy)R2N9QYRlAouUqm-KWc-SML z?7NdhZ!A*&(!x43l9Vrhp(n=oDBVd{Y4+SXs_6mmNn?j|X9m-<2< z6UMx&dfok;omZ{Dv6&uNrS8?dBsN(+Q~Ez3;e^_IcRxEBP;aIFjqgqmFn`q1*~V&< zZgImIb!>8Zct%~VUi0PGY`swP?MokK^lv(`aa6w=~!SLKm&lR+lvoiu wto8UjC0@rXC1s8RZLQByROYGB%DBVp@s*Sf@6pzkYGoeHTc{l^2sl6U|7&9g_5c6? diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 04bd2415f0518e..d9377054a593a7 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -134,8 +134,8 @@ - Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - 打开并预览驾驶员摄像头,用于调整安装角度以获得最优驾驶员监控体验。仅熄火时可用。 + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) + 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。仅熄火时可用。 diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm index 130bcfc92670cff84c6c6fcf359be599e4941e3e..0d448f2080322661cb712bd3c06b4049fabbf935 100644 GIT binary patch delta 1928 zcmX9;3s93+8a+w=KhHmZP=oDKfd~N+1cJo}2!c?s;I4q6wMF}&U`Ry}2%v+STA!3^ zrT73vtqQd-*BP|fwZ(NSVxbMTQ*7;I=_-n-W0$SnSzKN0o=!58Imx}>{l0V0cfPy5 zLE+w|*fq&z1(ajJ)cb(S4uqSDHN@$}eZ*`);J$GL5Tk(z2XPLugP6~lm%JDh1gM$A zCk@c}6IT%9h}($gc|Hq}E&;)@KxjYLdx0s8TjVp!9}FC~5J=k$*n)w~?JRr__+u^$ zDS(_9CLw{GV!-zc;ufxF0wqV7i1C$=iSGkW?oXWE@N=LmAHuc+z}z7SO*a9B455pa z!v`R^Kjq<22$uyQ;26X=Ex@!~h$oH%h9yvU{6yRUjb$m5$ z@(MPxFtFr!E1*lj(v01V8^#OouLF|Ou>5a5+&_v{g_PE}9BamS-WY}>y?sD(7}}jL zvTZ=u1WFrFf}uC7xL>K57&i?rSbY^w9AvF5)0+W5k5X^H2!s_VLxu){m;z{~qN)PyRDNRG{+OyS~6v&!~i`Cjj$!mH$1F0b5l;^+~+h z1yxlJ6VDt~eI%24#12(^2hC$?c~SOw(Y(%!f$c&|>jO6Kq;T|RA#qsv`0^egXn}Bg zWh?buCj3QWUyO%@uWos`{(F=@nnIwo#`(@wML_r7I%R;#xm(?TR(L_EfPpR^}YI#QMD?qI=tm z0hhcO_{fV%`^A=sRyNZ7vDor99buRzwr<@*B|a5TUb;qoUKhU@vvPRr#S4?arK8$J z&j1Mp{UF}h)Wb$U@5Lm8_;8E_qux;~C+E>wU21tGiV6m+6Yiu_!CUJ2?VUh;iaK9G z-K`DkV-Fs4e^C7}>xzp`aBG65QmQHUG;MyH*zsu1$9VS-QiOc@Cz}K=i~MD!zQjT*6)7I)`k@6n?65A zLT-J_)x~U4jsB3EtyVqKpOCljreve~vo?{#8{W8He8RO_fBRJ@1v+g|bZp>mi6Qv> zMOyHbA^oR1z6Bn`@&M>e1raL=)T8BgpL@#{1f#t|6MYlXSv8K zX>vQ)L-tCMiEHVIPANT=4Ndq_%ACC%i0hXYr10ADuS-kzFrOjUr|5R+YY#E%Mak1mg2FpSb(@u=t}#aY|3D&%#<(#OeX7oQB9{A; z4;s(Z5aU9PT|w{Bs%gfq6k_O?#@+-rULS7kU&z`w9%4N8f3q1RABtKa?pcR z>e49BzQ&gEn6)IJnLsWhftSGTb9 z#ir(>QcCuV>94E*N69vr25ukX46HF-OaC)4=Y|8AH{n#g=B&mjHD77=-kyR`A75pYG99nZWYapjOr^c>(qwM+hb$c87Y|8A5 z&xMt5UcGWfO72Kim`zEq(q#OWH2%TI8dFqSSyfSFaTdSgv=ml2s*5TttIA3p&aw(e fsohfiN~OhKR#s@Kc2qhF93>9t+EaBtzs>qT^2rE( delta 2026 zcmX|B3sjV48h+-V|DVf0|I80^v10?s1I0^;v8|aMEKJ0_gd`?rCv4qiaY@13X6{;NBVBMG4!b+&%z0+M?|a|x`JVTA z-}cR#jaxLEM|xfcgnnRb6rjx|+K45@JmOAbB_ML&G8T}=1LNw5Q;Gj1X7LQ;2Qg$3 zpnCv#tp)S}#CM4C#IwZHfVl!tz5qh=fl(cRw*-vlxw+o4{K13X<-jv#K+H=(+D2ad z3-IgT@S!CM>3h~xz6tjUgE{`s|JWS05|vjS8cf$ppcEGk`D`gXq@f`{HeT%uT{ltJO z%o9H6x()NP-{8elv7o*VFqU9p>NZ{&f*1bqDv7VfqBCvW??PTq2;)CMX+Pu5cD!@p z5|C&|gPSiOGZig9ELmVZdMk>!|Dnb|J`x^;WoVw*8%C|a)l8~lJ!85wnXO}hum_r5 zSGXQ;&|I1)GI5FKs&D`Zh|=77stkD2UyyUp0TFwI@ZRgdggruv>?W~Y!i!#faZH)8 zr^CqedBU+xBo-?PpQTm-YN_x|Ci4c0LRVYXuYl+mgzo)*Oz6>y(Zd1XIog2lS@eK2 z+K|cwzHC5Sl*z zSKDdZH^q;a*HO=%;&Fv_G5=Zo@@6O3d&DpA{=hDR3cjS9b|-}j-qg)% zI0;O7O_!yi?qL^o2l@tp-~e57bd871UAl+1E3}$HA2NoLMU2)T9I=isT&6$bx5!@fhz z<5Ok$q&JlRXBqWoPLseyV`!~`ts)we9(ZyYaKJc!b`MJsV_dL~(k1DQSuaxRQ5s`T zYci+DBV!flFzhpSbf(i$Ym8mq95d1hIcXe6mTrWcbGe-__Lj@H< z(;LOUjQ_>d`aMfDdaUXE94h8FT=6|k0#P3+p5O*O6!{?Y_t+s~+>Q68?x^Y;Fm8k6uVJwHE6H@BM>Zzi1h;feQF2mXN-gRO*sta(5&z-ej4*i3B+)AW35-oqM<2{oMmXrUYCq|yJTuQtQ3||nS8vemKsc%$$li1&r{-XNd z9N^d~QlIT%!U?geE4P;lC99jSuBQix$~_{9V-f~`2}ugp7Xcd<1xF# znwMYXaywnQ)`I+1PPa4PWnGeAXw7!4a{o7)CtOZ9zXpb D32iIQ diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 806dd54ad3a2ed..efd893ec4c9f1a 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -125,7 +125,7 @@ Driver Camera - 駕駛監控 + 駕駛員攝像頭 @@ -134,8 +134,8 @@ - Preview the driver facing camera to help optimize device mounting position for best driver monitoring experience. (vehicle must be off) - 預覽駕駛監控鏡頭畫面,方便調整設備安裝的位置,以提供更準確的駕駛監控。(車子必須保持在熄火的狀態) + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) + 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。僅在熄火時可用。 From ee6dc0311818fd97cb78c58fb9a47267385312a0 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 12 Jul 2022 17:25:54 -0700 Subject: [PATCH 323/436] fix accord years --- docs/CARS.md | 2 +- selfdrive/car/honda/values.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index c3efe87515f4ed..ec7da7a845118b 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -161,7 +161,7 @@ How We Rate The Cars |Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| |Genesis|G90 2017-18|All|||||| |GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| -|Honda|Accord 2016-22|All|||||| +|Honda|Accord 2018-22|All|||||| |Honda|Accord Hybrid 2018-22|All|||||| |Honda|Civic 2016-18|Honda Sensing|||||| |Honda|Civic 2019-21|All|||[2](#footnotes)||| diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index bfa42bd5095732..b8417ee19bb080 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -109,7 +109,7 @@ class HondaCarInfo(CarInfo): CAR_INFO: Dict[str, Optional[Union[HondaCarInfo, List[HondaCarInfo]]]] = { CAR.ACCORD: [ - HondaCarInfo("Honda Accord 2016-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), + HondaCarInfo("Honda Accord 2018-22", "All", video_link="https://www.youtube.com/watch?v=mrUwlj3Mi58", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), HondaCarInfo("Honda Inspire 2018", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), ], CAR.ACCORDH: HondaCarInfo("Honda Accord Hybrid 2018-22", "All", min_steer_speed=3. * CV.MPH_TO_MS, harness=Harness.bosch_a), From 00bb07f6248010afb197854257c2958433159990 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 12 Jul 2022 17:45:00 -0700 Subject: [PATCH 324/436] fw_versions.py: fix debug scanning (#25144) * Fix scanning all requests * fix replace --- selfdrive/car/fw_versions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index c4b158aebb1461..a8f5357f0e408e 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -394,7 +394,7 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=Fa brand_matches = get_brand_ecu_matches(ecu_rx_addrs) for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True): - car_fw = get_fw_versions(logcan, sendcan, brand=brand, timeout=timeout, debug=debug, progress=progress) + car_fw = get_fw_versions(logcan, sendcan, query_brand=brand, timeout=timeout, debug=debug, progress=progress) all_car_fw.extend(car_fw) matches = match_fw_to_car_exact(build_fw_dict(car_fw)) if len(matches) == 1: @@ -403,10 +403,10 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=Fa return all_car_fw -def get_fw_versions(logcan, sendcan, brand=None, extra=None, timeout=0.1, debug=False, progress=False): +def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, debug=False, progress=False): versions = get_interface_attr('FW_VERSIONS', ignore_none=True) - if brand is not None: - versions = {brand: versions[brand]} + if query_brand is not None: + versions = {query_brand: versions[query_brand]} if extra is not None: versions.update(extra) @@ -434,7 +434,7 @@ def get_fw_versions(logcan, sendcan, brand=None, extra=None, timeout=0.1, debug= addrs.insert(0, parallel_addrs) fw_versions = {} - requests = [r for r in REQUESTS if brand is None or r.brand == brand] + requests = [r for r in REQUESTS if query_brand is None or r.brand == query_brand] for addr in tqdm(addrs, disable=not progress): for addr_chunk in chunks(addr): for r in requests: @@ -503,7 +503,7 @@ def get_fw_versions(logcan, sendcan, brand=None, extra=None, timeout=0.1, debug= print() t = time.time() - fw_vers = get_fw_versions(logcan, sendcan, brand=args.brand, extra=extra, debug=args.debug, progress=True) + fw_vers = get_fw_versions(logcan, sendcan, query_brand=args.brand, extra=extra, debug=args.debug, progress=True) _, candidates = match_fw_to_car(fw_vers) print() From 44c6ca7eb4c9ea83aca18d4f648764cabdad8861 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 12 Jul 2022 18:23:48 -0700 Subject: [PATCH 325/436] EV6: reject fake cruise engagements (#25143) * EV6: reject fake cruise engagements * bump panda * raise to 8 * update refs * bump panda Co-authored-by: Comma Device --- opendbc | 2 +- panda | 2 +- selfdrive/car/hyundai/carcontroller.py | 4 ++-- selfdrive/car/hyundai/carstate.py | 11 ++++++++--- selfdrive/car/hyundai/hda2can.py | 8 +++----- selfdrive/car/hyundai/interface.py | 5 ++--- selfdrive/car/hyundai/values.py | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 8 files changed, 19 insertions(+), 17 deletions(-) diff --git a/opendbc b/opendbc index 81148db67fd00d..3fb3f5e82129ad 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 81148db67fd00d4e2a107b5b8269c532436edf2b +Subproject commit 3fb3f5e82129ad76232bcdca10632ed0566b20f8 diff --git a/panda b/panda index baecd2ecc6a2a6..2abeab913f6432 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit baecd2ecc6a2a608e1305601f6f697feca69fe88 +Subproject commit 2abeab913f6432e4327b07e247b8a46994ac77a1 diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index a878ad3274c694..d0d9c4083981c9 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -82,12 +82,12 @@ def update(self, CC, CS): if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: if CC.cruiseControl.cancel: for _ in range(20): - can_sends.append(hda2can.create_buttons(self.packer, CS.buttons_counter+1, True, False)) + can_sends.append(hda2can.create_buttons(self.packer, CS.buttons_counter+1, Buttons.CANCEL)) self.last_button_frame = self.frame # cruise standstill resume elif CC.cruiseControl.resume: - can_sends.append(hda2can.create_buttons(self.packer, CS.buttons_counter+1, False, True)) + can_sends.append(hda2can.create_buttons(self.packer, CS.buttons_counter+1, Buttons.RES_ACCEL)) self.last_button_frame = self.frame else: diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index a10cdadbca75d6..8afd851f00e60b 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -8,7 +8,7 @@ from selfdrive.car.hyundai.values import DBC, FEATURES, HDA2_CAR, EV_CAR, HYBRID_CAR, Buttons, CarControllerParams from selfdrive.car.interfaces import CarStateBase -PREV_BUTTON_SAMPLES = 4 +PREV_BUTTON_SAMPLES = 8 class CarState(CarStateBase): @@ -171,7 +171,10 @@ def update_hda2(self, cp, cp_cam): speed_factor = CV.MPH_TO_MS if cp.vl["CLUSTER_INFO"]["DISTANCE_UNIT"] == 1 else CV.KPH_TO_MS ret.cruiseState.speed = cp.vl["CRUISE_INFO"]["SET_SPEED"] * speed_factor - self.buttons_counter = cp.vl["CRUISE_BUTTONS"]["_COUNTER"] + self.cruise_buttons.extend(cp.vl_all["CRUISE_BUTTONS"]["CRUISE_BUTTONS"]) + self.main_buttons.extend(cp.vl_all["CRUISE_BUTTONS"]["ADAPTIVE_CRUISE_MAIN_BTN"]) + self.buttons_counter = cp.vl["CRUISE_BUTTONS"]["COUNTER"] + self.cam_0x2a4 = copy.copy(cp_cam.vl["CAM_0x2a4"]) return ret @@ -362,7 +365,9 @@ def get_can_parser_hda2(CP): ("CRUISE_ACTIVE", "SCC1"), ("SET_SPEED", "CRUISE_INFO"), ("CRUISE_STANDSTILL", "CRUISE_INFO"), - ("_COUNTER", "CRUISE_BUTTONS"), + ("COUNTER", "CRUISE_BUTTONS"), + ("CRUISE_BUTTONS", "CRUISE_BUTTONS"), + ("ADAPTIVE_CRUISE_MAIN_BTN", "CRUISE_BUTTONS"), ("DISTANCE_UNIT", "CLUSTER_INFO"), diff --git a/selfdrive/car/hyundai/hda2can.py b/selfdrive/car/hyundai/hda2can.py index 437f5cf5388334..9a9e477cf50dbb 100644 --- a/selfdrive/car/hyundai/hda2can.py +++ b/selfdrive/car/hyundai/hda2can.py @@ -18,11 +18,9 @@ def create_cam_0x2a4(packer, frame, camera_values): }) return packer.make_can_msg("CAM_0x2a4", 4, camera_values, frame % 255) -def create_buttons(packer, cnt, cancel, resume): +def create_buttons(packer, cnt, btn): values = { - "_COUNTER": cnt % 0xf, "SET_ME_1": 1, - "DISTANCE_BTN": 1 if resume else 0, - "PAUSE_RESUME_BTN": 1 if cancel else 0, + "CRUISE_BUTTONS": btn, } - return packer.make_can_msg("CRUISE_BUTTONS", 5, values) + return packer.make_can_msg("CRUISE_BUTTONS", 5, values, cnt % 0xf) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index 069b0e74e54d7e..a32ee2c0ab8638 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -2,7 +2,7 @@ from cereal import car from panda import Panda from common.conversions import Conversions as CV -from selfdrive.car.hyundai.values import CAR, DBC, HDA2_CAR, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, Buttons, CarControllerParams +from selfdrive.car.hyundai.values import CAR, DBC, EV_CAR, HYBRID_CAR, LEGACY_SAFETY_MODE_CAR, Buttons, CarControllerParams from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR from selfdrive.car import STD_CARGO_KG, create_button_enable_events, create_button_event, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase @@ -321,8 +321,7 @@ def _update(self, c): # To avoid re-engaging when openpilot cancels, check user engagement intention via buttons # Main button also can trigger an engagement on these cars allow_enable = any(btn in ENABLE_BUTTONS for btn in self.CS.cruise_buttons) or any(self.CS.main_buttons) - allow_enable = allow_enable or self.CP.carFingerprint in HDA2_CAR - events = self.create_common_events(ret, pcm_enable=self.CS.CP.pcmCruise, allow_enable=allow_enable or True) + events = self.create_common_events(ret, pcm_enable=self.CS.CP.pcmCruise, allow_enable=allow_enable) if self.CS.brake_error: events.add(EventName.brakeUnavailable) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 6e184ce9ef11bf..ffa29c60d45a86 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -167,7 +167,7 @@ class Buttons: RES_ACCEL = 1 SET_DECEL = 2 GAP_DIST = 3 - CANCEL = 4 + CANCEL = 4 # on newer models, this is a pause/resume button FINGERPRINTS = { CAR.ELANTRA: [{ diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b165b163ba5bcf..e77a38de5f180f 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -11e721366f1c177a84e6cb8b48171113ac3b54f9 \ No newline at end of file +5efbbdf69e16db3d989bfaf62d10e958e80b9ca2 \ No newline at end of file From aadaaabd54988a286704ef2bea0bacf4bd62fa8b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 12 Jul 2022 18:58:46 -0700 Subject: [PATCH 326/436] compatibility docs: print diff from PR (#24941) * print docs diff * revert car changes * cause a diff * temp so it works * text diff * tier inline is a bit too much * comments * fix * use paths * fix * temp * temp * diff * fix * remove something * more text diff * Delete comment if outdated * Smaller diff * remove * no diff * Don't try to run on fork PRs * cause some errors * Fix * Fix * Doesn't support env in job if, only step if * in case file was moved, don't throw error * See if this does what I think it does * See if this does what I think it does * should work * change something * revert * uncomment * no comment * this shouldn't fail * rename to base * Remove true * Remove other true --- .github/workflows/selfdrive_tests.yaml | 71 +++++++++++++++++++- selfdrive/debug/dump_car_info.py | 18 ++++++ selfdrive/debug/print_docs_diff.py | 90 ++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 3 deletions(-) create mode 100755 selfdrive/debug/dump_car_info.py create mode 100755 selfdrive/debug/print_docs_diff.py diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 99a21b58f37a86..298ea5fb499266 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -10,6 +10,7 @@ env: CL_BASE_IMAGE: openpilot-base-cl DOCKER_REGISTRY: ghcr.io/commaai AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} + HAS_AZURE_TOKEN: $AZURE_TOKEN != '' DOCKER_LOGIN: docker login ghcr.io -u adeebshihadeh -p ${{ secrets.CONTAINER_TOKEN }} BUILD: | @@ -17,12 +18,12 @@ env: docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . - RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache $BASE_IMAGE /bin/sh -c + RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c BUILD_CL: | docker pull $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest || true docker build --cache-from $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $DOCKER_REGISTRY/$CL_BASE_IMAGE:latest -t $CL_BASE_IMAGE:latest -f Dockerfile.openpilot_base_cl . - RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache $CL_BASE_IMAGE /bin/sh -c + RUN_CL: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $CL_BASE_IMAGE /bin/sh -c UNIT_TEST: coverage run --append -m unittest discover @@ -365,7 +366,7 @@ jobs: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - name: Upload reference logs - if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} + if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.HAS_AZURE_TOKEN }} run: | ${{ env.RUN }} "scons -j$(nproc) && \ CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" @@ -510,3 +511,67 @@ jobs: run: | $DOCKER_LOGIN docker push $DOCKER_REGISTRY/openpilot-docs:latest + + car_docs_diff: + name: comment on PR with car docs diff + runs-on: ubuntu-20.04 + timeout-minutes: 50 + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v3 + with: + submodules: true + ref: ${{ github.event.pull_request.base.ref }} + - name: Cache scons + id: scons-cache + # TODO: Change the version to the released version when https://github.com/actions/cache/pull/489 (or 571) is merged. + uses: actions/cache@03e00da99d75a2204924908e1cca7902cafce66b + env: + CACHE_SKIP_SAVE: true + with: + path: /tmp/scons_cache + key: scons-${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }}- + restore-keys: | + scons-${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }}- + scons- + - name: Build Docker image + run: eval "$BUILD" + - name: Get base car info + run: | + ${{ env.RUN }} "scons -j$(nproc) && python selfdrive/debug/dump_car_info.py --path /tmp/openpilot_cache/base_car_info" + sudo chown -R $USER:$USER ${{ github.workspace }} + - uses: actions/checkout@v3 + with: + submodules: true + - name: Save car docs diff + id: save_diff + run: | + ${{ env.RUN }} "scons -j$(nproc)" + output=$(${{ env.RUN }} "python selfdrive/debug/print_docs_diff.py --path /tmp/openpilot_cache/base_car_info") + output="${output//$'\n'/'%0A'}" + echo "::set-output name=diff::$output" + - name: Find comment + if: env.HAS_AZURE_TOKEN + uses: peter-evans/find-comment@v1 + id: fc + with: + issue-number: ${{ github.event.pull_request.number }} + body-includes: This PR makes changes to + - name: Update comment + if: steps.save_diff.outputs.diff != '' && env.HAS_AZURE_TOKEN + uses: peter-evans/create-or-update-comment@v1 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: "${{ steps.save_diff.outputs.diff }}" + edit-mode: replace + - name: Delete comment + if: steps.fc.outputs.comment-id != '' && steps.save_diff.outputs.diff == '' && env.HAS_AZURE_TOKEN + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: ${{ steps.fc.outputs.comment-id }} + }) diff --git a/selfdrive/debug/dump_car_info.py b/selfdrive/debug/dump_car_info.py new file mode 100755 index 00000000000000..c9a21c2848d60c --- /dev/null +++ b/selfdrive/debug/dump_car_info.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import argparse +import pickle + +from selfdrive.car.docs import get_all_car_info + + +def dump_car_info(path): + with open(path, 'wb') as f: + pickle.dump(get_all_car_info(), f) + print(f'Dumping car info to {path}') + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", required=True) + args = parser.parse_args() + dump_car_info(args.path) diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py new file mode 100755 index 00000000000000..5cf3867b2d8d8a --- /dev/null +++ b/selfdrive/debug/print_docs_diff.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import argparse +import pickle + +from selfdrive.car.docs import get_all_car_info +from selfdrive.car.docs_definitions import Column + +STAR_ICON = '' +COLUMNS = "|" + "|".join([column.value for column in Column]) + "|" +COLUMN_HEADER = "|---|---|---|:---:|:---:|:---:|:---:|:---:|" +ARROW_SYMBOL = "➡️" + + +def load_base_car_info(path): + with open(path, "rb") as f: + return pickle.load(f) + + +def get_star_diff(base_car, new_car): + return [column for column, value in base_car.row.items() if value != new_car.row[column]] + + +def format_row(builder): + return "|" + "|".join(builder) + "|" + + +def print_car_info_diff(path): + base_car_info = {f"{i.make} {i.model}": i for i in load_base_car_info(path)} + new_car_info = {f"{i.make} {i.model}": i for i in get_all_car_info()} + + tier_changes = [] + star_changes = [] + removals = [] + additions = [] + + # Changes (tier + stars) + for base_car_model, base_car in base_car_info.items(): + if base_car_model not in new_car_info: + continue + + new_car = new_car_info[base_car_model] + + # Tier changes + if base_car.tier != new_car.tier: + tier_changes.append(f"- Tier for {base_car.make} {base_car.model} changed! ({base_car.tier.name.title()} {ARROW_SYMBOL} {new_car.tier.name.title()})") + + # Star changes + diff = get_star_diff(base_car, new_car) + if not len(diff): + continue + + row_builder = [] + for column in list(Column): + if column not in diff: + row_builder.append(new_car.get_column(column, STAR_ICON, "{}")) + else: + row_builder.append(base_car.get_column(column, STAR_ICON, "{}") + ARROW_SYMBOL + new_car.get_column(column, STAR_ICON, "{}")) + + star_changes.append(format_row(row_builder)) + + # Removals + for model in set(base_car_info) - set(new_car_info): + car_info = base_car_info[model] + removals.append(format_row([car_info.get_column(column, STAR_ICON, "{}") for column in Column])) + + # Additions + for model in set(new_car_info) - set(base_car_info): + car_info = new_car_info[model] + additions.append(format_row([car_info.get_column(column, STAR_ICON, "{}") for column in Column])) + + # Print diff + if len(star_changes) or len(tier_changes) or len(removals) or len(additions): + markdown_builder = ["### ⚠️ This PR makes changes to [CARS.md](../blob/master/docs/CARS.md) ⚠️"] + + for title, category in (("## 🏅 Tier Changes", tier_changes), ("## 🔀 Star Changes", star_changes), ("## ❌ Removed", removals), ("## ➕ Added", additions)): + if len(category): + markdown_builder.append(title) + if "Tier" not in title: + markdown_builder.append(COLUMNS) + markdown_builder.append(COLUMN_HEADER) + markdown_builder.extend(category) + + print("\n".join(markdown_builder)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--path", required=True) + args = parser.parse_args() + print_car_info_diff(args.path) From 01de46ad82358efa12797a50f501d902ca711547 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 12 Jul 2022 19:25:03 -0700 Subject: [PATCH 327/436] Corolla Cross: Update minimum enable speed (#25132) * Update min steer speed for intl. Corolla Cross 27 km/h, thanks to Ale Sato * update docs --- docs/CARS.md | 6 +++--- selfdrive/car/toyota/values.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index ec7da7a845118b..e24672e9bc881f 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -35,7 +35,7 @@ How We Rate The Cars **All supported cars can move between the tiers as support changes.** -# Gold - 31 cars +# Gold - 30 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -60,7 +60,6 @@ How We Rate The Cars |Toyota|Camry 2021-22|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2021-22|All|||||| |Toyota|Corolla 2020-22|All|||||| -|Toyota|Corolla Cross 2020-21 (Non-US only)|All|||||| |Toyota|Corolla Hatchback 2019-22|All|||||| |Toyota|Corolla Hybrid 2020-22|All|||||| |Toyota|Highlander 2020-22|All|||||| @@ -71,7 +70,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 69 cars +# Silver - 70 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -126,6 +125,7 @@ How We Rate The Cars |Toyota|Alphard Hybrid 2021|All|||||| |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| +|Toyota|Corolla Cross 2020-21 (Non-US only)|All|||||| |Toyota|Highlander 2017-19|All|[3](#footnotes)||||| |Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| |Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index f47ab87040248b..f40a58b5a74c60 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -119,7 +119,7 @@ class ToyotaCarInfo(CarInfo): CAR.COROLLA: ToyotaCarInfo("Toyota Corolla 2017-19", footnotes=[Footnote.DSU]), CAR.COROLLA_TSS2: [ ToyotaCarInfo("Toyota Corolla 2020-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), - ToyotaCarInfo("Toyota Corolla Cross 2020-21 (Non-US only)"), + ToyotaCarInfo("Toyota Corolla Cross 2020-21 (Non-US only)", min_enable_speed=7.5), ToyotaCarInfo("Toyota Corolla Hatchback 2019-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), ], CAR.COROLLAH_TSS2: [ From 906a8a912cbd39863f43582677f2435f11ecb904 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 13 Jul 2022 04:28:48 +0200 Subject: [PATCH 328/436] casync: only when run from updater (#25130) * casync: only when run from updater * also here --- system/hardware/tici/agnos.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/hardware/tici/agnos.py b/system/hardware/tici/agnos.py index 750aa630aea249..ca2498a00c4208 100755 --- a/system/hardware/tici/agnos.py +++ b/system/hardware/tici/agnos.py @@ -222,7 +222,7 @@ def progress(cur): raise Exception(f"Raw hash mismatch '{partition['hash_raw'].lower()}'") -def flash_partition(target_slot_number: int, partition: dict, cloudlog): +def flash_partition(target_slot_number: int, partition: dict, cloudlog, standalone=False): cloudlog.info(f"Downloading and writing {partition['name']}") if verify_partition(target_slot_number, partition): @@ -236,7 +236,7 @@ def flash_partition(target_slot_number: int, partition: dict, cloudlog): path = get_partition_path(target_slot_number, partition) - if 'casync_caibx' in partition: + if ('casync_caibx' in partition) and not standalone: extract_casync_image(target_slot_number, partition, cloudlog) else: extract_compressed_image(target_slot_number, partition, cloudlog) @@ -263,7 +263,7 @@ def swap(manifest_path: str, target_slot_number: int, cloudlog) -> None: cloudlog.error(f"Swap failed {out}") -def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog) -> None: +def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog, standalone=False) -> None: update = json.load(open(manifest_path)) cloudlog.info(f"Target slot {target_slot_number}") @@ -276,7 +276,7 @@ def flash_agnos_update(manifest_path: str, target_slot_number: int, cloudlog) -> for retries in range(10): try: - flash_partition(target_slot_number, partition, cloudlog) + flash_partition(target_slot_number, partition, cloudlog, standalone) success = True break @@ -320,9 +320,9 @@ def verify_agnos_update(manifest_path: str, target_slot_number: int) -> bool: elif args.swap: while not verify_agnos_update(args.manifest, target_slot_number): logging.error("Verification failed. Flashing AGNOS") - flash_agnos_update(args.manifest, target_slot_number, logging) + flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True) logging.warning(f"Verification succeeded. Swapping to slot {target_slot_number}") swap(args.manifest, target_slot_number, logging) else: - flash_agnos_update(args.manifest, target_slot_number, logging) + flash_agnos_update(args.manifest, target_slot_number, logging, standalone=True) From 0eab1ed817ece20b25de7438d32238c482613df3 Mon Sep 17 00:00:00 2001 From: Jafar Al-Gharaibeh Date: Tue, 12 Jul 2022 20:38:18 -0600 Subject: [PATCH 329/436] Mazda: CX-5 22 FW FP (#24778) Mazda CX-5 2022 FW FP dongle-id: 661621a8442f0688 Signed-off-by: Jafar Al-Gharaibeh --- selfdrive/car/mazda/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index 09b9b7732b6262..12e9eafc4f6874 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -65,6 +65,7 @@ class Buttons: ], (Ecu.engine, 0x7e0, None): [ b'PX2G-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PX2H-188K2-H\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'SH54-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ From 3b4e939b9f88b70727e687613a912aef36178755 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 12 Jul 2022 19:58:39 -0700 Subject: [PATCH 330/436] UI: translations cleanup (#25120) * Make this one translation * Remote html from translations * getBrand as argument * some stuff * Forget Wi-Fi network * Update translations * Remove obsolete * compilation fixes * remove * Fix missing translation --- selfdrive/ui/qt/offroad/networking.cc | 8 +-- selfdrive/ui/qt/offroad/settings.cc | 2 +- selfdrive/ui/qt/widgets/input.cc | 2 +- selfdrive/ui/qt/widgets/prime.cc | 13 ++-- selfdrive/ui/translations/main_ko.qm | Bin 19981 -> 19439 bytes selfdrive/ui/translations/main_ko.ts | 79 +++++++++++------------ selfdrive/ui/translations/main_zh-CHS.qm | Bin 18469 -> 17931 bytes selfdrive/ui/translations/main_zh-CHS.ts | 79 +++++++++++------------ selfdrive/ui/translations/main_zh-CHT.qm | Bin 18509 -> 17969 bytes selfdrive/ui/translations/main_zh-CHT.ts | 79 +++++++++++------------ selfdrive/ui/update_translations.py | 10 ++- 11 files changed, 129 insertions(+), 143 deletions(-) diff --git a/selfdrive/ui/qt/offroad/networking.cc b/selfdrive/ui/qt/offroad/networking.cc index 536ca495cad291..c7341d1987c886 100644 --- a/selfdrive/ui/qt/offroad/networking.cc +++ b/selfdrive/ui/qt/offroad/networking.cc @@ -84,7 +84,7 @@ void Networking::connectToNetwork(const Network &n) { } else if (n.security_type == SecurityType::OPEN) { wifi->connect(n); } else if (n.security_type == SecurityType::WPA) { - QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"") + n.ssid + "\"", true, 8); + QString pass = InputDialog::getText(tr("Enter password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); if (!pass.isEmpty()) { wifi->connect(n, pass); } @@ -94,7 +94,7 @@ void Networking::connectToNetwork(const Network &n) { void Networking::wrongPassword(const QString &ssid) { if (wifi->seenNetworks.contains(ssid)) { const Network &n = wifi->seenNetworks.value(ssid); - QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"") + n.ssid +"\"", true, 8); + QString pass = InputDialog::getText(tr("Wrong password"), this, tr("for \"%1\"").arg(QString::fromUtf8(n.ssid)), true, 8); if (!pass.isEmpty()) { wifi->connect(n, pass); } @@ -174,7 +174,7 @@ AdvancedNetworking::AdvancedNetworking(QWidget* parent, WifiManager* wifi): QWid list->addItem(editApnButton); // Set initial config - wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn"))); + wifi->updateGsmSettings(roamingEnabled, QString::fromStdString(params.get("GsmApn"))); main_layout->addWidget(new ScrollView(list, this)); main_layout->addStretch(1); @@ -296,7 +296,7 @@ void WifiUI::refresh() { QPushButton *forgetBtn = new QPushButton(tr("FORGET")); forgetBtn->setObjectName("forgetBtn"); QObject::connect(forgetBtn, &QPushButton::clicked, [=]() { - if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"") + QString::fromUtf8(network.ssid) + "\"?", this)) { + if (ConfirmationDialog::confirm(tr("Forget Wi-Fi Network \"%1\"?").arg(QString::fromUtf8(network.ssid)), this)) { wifi->forgetConnection(network.ssid); } }); diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 9aeb966ccfe32e..9a6e2039661a7b 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -249,7 +249,7 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { }); - auto uninstallBtn = new ButtonControl(tr("Uninstall ") + getBrand(), tr("UNINSTALL")); + auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); connect(uninstallBtn, &ButtonControl::clicked, [&]() { if (ConfirmationDialog::confirm(tr("Are you sure you want to uninstall?"), this)) { params.putBool("DoUninstall", true); diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index b0facfce83d436..dc54a3621c6a11 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -165,7 +165,7 @@ void InputDialog::handleEnter() { done(QDialog::Accepted); emitText(line->text()); } else { - setMessage(tr("Need at least ") + QString::number(minLength) + tr(" characters!"), false); + setMessage(tr("Need at least %1 characters!").arg(minLength), false); } } diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index d2529821f4e00c..5419475262b338 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -88,13 +88,16 @@ PairingPopup::PairingPopup(QWidget *parent) : QDialogBase(parent) { title->setWordWrap(true); vlayout->addWidget(title); - QLabel *instructions = new QLabel(tr(R"( + QLabel *instructions = new QLabel(QString(R"(
    -
  1. Go to https://connect.comma.ai on your phone
  2. -
  3. Click "add new device" and scan the QR code on the right
  4. -
  5. Bookmark connect.comma.ai to your home screen to use it like an app
  6. +
  7. %1
  8. +
  9. %2
  10. +
  11. %3
- )"), this); + )").arg(tr("Go to https://connect.comma.ai on your phone")) + .arg(tr("Click \"add new device\" and scan the QR code on the right")) + .arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this); + instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;"); instructions->setWordWrap(true); vlayout->addWidget(instructions); diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm index 40b0bb65a38d65ee901ac7a85257e88868b4513c..d59698e07432f37ed105ada86a1c83bc815e0e0f 100644 GIT binary patch delta 2085 zcmYLJ3s98T89lrE|NHp&1qcfgS@r=c1%WkcK?M{P0~C=K4Mjyk2tp9$p~gJAlTkwq zYBdWLX;|7-Q0vsO$ zsjsu}HQ;BzV4uIE&S_uS7$S8GDtRSrT2dnz$;ODP=3(QME zmi(_=FF{uRtE^_ms*??X6oBlMTAph{&Z(CHXW}`mz1YJ8=kdY>0}b2o&ZR3r(kV1o z<#I}a=$Mj0+PN5gV=MQ2WTw~%Af!zea5RKM?v%~l&*{yMk!5y<0il~^Z}oG1fkSpB zO8`u_WH;rU#I!52dr$8o;X1ig^a&7_BM%)Nh7+?N$&;ljsu&?(<7GR2j|31?|PqbSYe z1QuIFeN8zq|GJoD2qp0cV)lAYC`Kt3NgvTVLt;tD2~HqiENMK+5hoG9V|EM&B2u{r&21hG{@#O0UZOj^!O4-$7u~<6H+0cr!)v>##BQ;Df{M!< z#eqQ*u&)zu@1$iyy*-%tia0(&LQ&mHxiyQO#V8G9kyI>Lx$s^x6)RIFE+3}ntjcB0 zZNS0~ll=Wtddf(Q!xbLU_?P~1wL)n7Crn=UfWp;`P*5#u^FE7L|uv|Y2xxsy^Y$=9r2LuseiYYIAf+h)2o z`$2D^MbkI1oLji@Qq&@fe!k9;iaqMths<}fC2d7K5 zlN{;vFsc6hae6dEa^GCZ(bP#tdpTlZuGIK?EtM;moF7JuyuK!>y@-M07O6ur^D((D zb(j|Nwq;3oH&juwZmq2KSA1-4Y3&z2p%<=dlW*+-ri5$P4)Zy%*J{f@6DX;d_SJ2^ zR8X(&{E8!*aa-G+PUTD`I^PQ<5P4W%bzJE;VK?XO*i< zOQaHUa^0#rCJZ*|HZ6ZltXFqv{8=U_)-@mGEi>D7J;$h=BVPCE05R%K-9RS^3ZLth zCqh{8h(0Rl1{=xH$4-#wQ*rvnIo!97>CbH^F4(W{u)RyuItTR~iTs$+s=u_5UeJ2! z`?7f*{kjZ(@15W%HX8z~seo^+!S*nTO6@bm4DmgRzF|n)O9_2nG?XVUW!!zko+B%$ zK%?RJ4JAOZ zfcP85G*V;Be3jPA^{zD4<@WbpYVKWgaW5v&yM+EVOlSLlLfeyY z^>dpmEVfXG&1EtBwwyF)%jC#$jahS~Cr6T8ZhMj|H^d}c$+2U#BNlM+yvf{GJs=U%t5_bJN#=oU$ewUAIrfXZe Y#mB=C2~SQ3x{jrr1aeGpwWdG&KXjTDOaK4? delta 2644 zcmcIk2~bq`9sYLry=6zG(a>Wyk%k_Jfz*{i zM9jOR=-<7EHcHV|An8nG55pE_LUcsc+ z*MacGm~`_4AjXEt`gzQC6&YhUq3_99l1!4WBg^h0Nj`Y#DzkYPVd_us)4m+j)Sq&F z0jAAc&MY~|*;@}tgOQtFL%U;m`Im37%3GLyvK4TK-AD0!KW@ZgX)gnq7GvibGMRcA z4URd$=&5KLl*O_)qi5YB?zgI}v7^c8x2nP01AtM#RYh-L_o5c4vYSVd*uAQEJGh?s zs_NV{0kA$$T~f0X!?mhgkt=~=ZEC6TGaz`QIE$lW-P_0#3WcI{ zGVGo$R9;I0{EiE6%wtJ9gRolo28i1))TSS$0$&?IOX>hx{RYr>Q>d$d$kN<}UETAE znZo`rHc^2p;qXuDfi#ow8=1{9-51V&*~#^M;q2|Za169++cc_Yf!1^{lnl<$CfrIRgC*KY z4ab0_2iln`k{`5OyZhl|z<-SPQI@lVG8?M%8%3&u)w(@HR`6uKx_=isPYhK7Y&F1zL5`JTG3b$e?O=hXK_x1aB09M2B7 z4|@FhZPDwDuTaMRdjC4k`SW-56Tar38!c+R36{IX}jeh1VQatj!ett6t z?YVaS22fsT)VFs|p`5DqSKLc^^Y}{>f_Xb>H%jx*xACO1wDuM+7n=_;k^W<(svBm0 zuaasWv$Z4hq}rpqDbqr!?$UI&s7>15##Rf_lJmft8dA1f`Z!$V1W%Su71B{GlbR%7 z-Xj;JCTju*ZM}5!RR<}9LG|G>+FK0%Cq83ADnr`u*8+o5470ECMhN`eP<~M$lame0 z7kiRHqoMgLw&uB^hSM*SG3(D{&l4;l{DN$2;QGi~ryP=4Ns(-m(^A;uJHbpL;(pOlUyyejh2G}V#yu(3LX3DKQ$XsNZ{COua;*8wc%mRhW zM(v&eCTui@`TULt$uh?FvQVEz#sg8@4+t`Tw1k-GJYj6|`vui|#n_Zg9Cg=tCV^To zSd8tt980f1n7rQK!xQW^4OvYFJhM%H4^zn0aZ^keucAn^DRVs;^xSAFPfn%Z9n;$P zIO)MXrfoOIQyZnGU)3{F%r~ZE52y*>YSX##=PB}2X171_j-26^VfLKBxsJVVwto2- z7^^lO;+4 zX$SeOFQSkxHYV!~79Wq6>is0CV`6hnM^VG+hHluXKMIGh?E1C0&Glf!k6Z=b?!&ch zvrevu^24raN`C%)CC{PQ^Ya!ul*Xgp`?SO4j@*OcEkegEDDeBiR4gd1Q2bmLU(d!J zzPT!=dNCv#{Oy;3QrP+JKqbnMk3<9^g0F&w{cH9oYsJVz1qxAw5`@qr9|hcxqg@1| z>A4i~^h)}N=zVz-XkGLqUSwszYhRMT4>*ryIQT2&dSB3Jgg*`H%Ce20!zf|>asE?= z{~-aATt}4ga}oM~PLgL+Ttk#N=f6qRcgntgJafvYtyDQArTBXAwT%u- z2(uUcOQ(P0qqfET*FJNU6zj8j62eOD136tg6>DRst=u|PdwSUZvS-vTO)n`bS?I{K i+ZETzvEGd - Need at least - 최소 - - - - characters! - 자가 필요합니다! + Need at least %1 characters! + 최소 %1 자가 필요합니다!
@@ -481,8 +476,8 @@ location set - for " - 하기위한 " + for "%1" + 하기위한 "%1" @@ -547,52 +542,50 @@ location set 장치를 콤마 계정과 페어링합니다 - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> - <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> - <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> - </ol> - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>https://connect.comma.ai에 접속하세요</li> - <li style='margin-bottom: 50px;'>"새 장치 추가"를 클릭하고 오른쪽 QR 코드를 검색합니다.</li> - <li style='margin-bottom: 50px;'>connect.comma.ai을 앱처럼 사용하려면 홈 화면에 바로가기를 만드십시오.</li> - </ol> - + + Go to https://connect.comma.ai on your phone + https://connect.comma.ai에 접속하세요 + + + + Click "add new device" and scan the QR code on the right + "새 장치 추가"를 클릭하고 오른쪽 QR 코드를 검색합니다 + + + + Bookmark connect.comma.ai to your home screen to use it like an app + connect.comma.ai을 앱처럼 사용하려면 홈 화면에 바로가기를 만드십시오 PrimeAdWidget - + Upgrade Now 지금 업그레이드 - + Become a comma prime member at connect.comma.ai connect.comma.ai에서 comma prime에 가입합니다 - + PRIME FEATURES: PRIME 기능: - + Remote access 원격 접속 - + 1 year of storage 1년간 저장 - + Developer perks 개발자 혜택 @@ -600,22 +593,22 @@ location set PrimeUserWidget - + ✓ SUBSCRIBED ✓ 구독함 - + comma prime comma prime - + CONNECT.COMMA.AI CONNECT.COMMA.AI - + COMMA POINTS COMMA POINTS @@ -857,17 +850,17 @@ location set SetupWidget - + Finish Setup 설정 완료 - + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. 장치를 (connect.comma.ai)에서 페어링하고 comma prime 오퍼를 청구합니다. - + Pair device 장치 페어링 @@ -1023,13 +1016,13 @@ location set - Uninstall - 제거 + UNINSTALL + 제거 - UNINSTALL - 제거 + Uninstall %1 + 제거 %1 @@ -1274,8 +1267,8 @@ location set - Forget Wi-Fi Network " - wifi 네트워크 저장안함 " + Forget Wi-Fi Network "%1"? + wifi 네트워크 저장안함 "%1"? diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm index 2c7963cf17a496e356fb5b0372b565ac82514661..eed52b27788c46e02689a14fb9f11e17c776755a 100644 GIT binary patch delta 2088 zcmYLJ4OCQR8h+-^z4LQt<_;(zBIwKjA%Y-?3OT?L6a5v5t5eF!7x6L%^C2JkbL@>&65@Jm@YFdC+|kFy|E@Y!#6D zDlh&Vc#6q_{ei3qCK)EK06ae)D^25;|zy3gyak-FvU!iE0Nr_76?f~ z^6jHQWDw@+my&A@Qm6A_{H2yBFzee$JFo!ojzmVWi;umA%xk>W^AP5}{4d6pBU|`@ zB8DM*X*F-NAg84X&}U*{%1&|?@a%z?0H^&wSbV072RczP#6XiB`_EqlV!uGEgOv!9 z(J?xMw5xDqa~bz*RbDZnfGtGjy*B^|XjDzx%IbyPR%LYtbG=)&=VPux4yad~sNv@W&pOe%y4SvDgg6 zO%vL;*nlaD@P5ixz?>*t&f*2*E(uqBVvF2J9M8@lBcr(GxtcQ z+BFokTuM{|$$L;*xP%3Y(n*E#`*cpf^n5@w5bh;Cf4GGet|a~h=q5^)%PaW)G-+*= z!Zz3?)&8DD^o=9v`_%}JH;-WKJgFh1i3Ra^OKR9gYZ&6Bri~k@#AnjkAqp;RllrcZ zfaM42>-BVuZPf_cPf3GABox}C5v49 zq{&lJZ(EC|@%}@e8`KPDd`w4tsY`qF8oRQ(lq4h`v`JS!xeFI41ZfA0qT?#ML*958+oF_!VO<=R`cuwRk9^C3&> zUoG!`uaU$q%MHB?SeklyZ#PS<9+D5gx|7NslutOPNbKHp`D`HrB~k8>$8(mPkvqKX zY_?4EfSQP^-2LIFN=!H{;q%UhYB?b(O`#BA)7Q>p$A|(wqRKMuK_y>m0 zZ&;#$2ZoQHrg9!%8a+;vKuC+x+RAnRR^#N@N_wN)=uArd2k_)^V`}tbV8%sbS^|}b zUSZ6sV?w`3J^_&RAAIsitT{8OUP{PqZ%9?~ZG_R!8)MrwGM&-{< zydZKwIr$?!;ZtSSeb0HQ?KFGLV*5@#ZuYwQkkjQwvv*E5r$c(XdG2*43XeBuIOuZS zI&(?k4NBy0-rl=`9#~=iLqQ3p8aAI={a-4z#QfncD+!!6U%9=P-SL|FYSLRk%!MKv zsjKZtZI|NuWZc-kuD-X(eHUt~VL>1wU`d~pm%H|fVHGV( zPx0@YUQn>ilJBq-7vz^ZEP)X&Z!c?Gz1I{?z$j-{)fd^O%I}ALVEZ}T_Bc$peARC( zw!nzCo4zlo1Z29bmQ4BaZldd~HPN-s=B2W^_D`_=vJvgdvqnGKaETM*e_0NF%^Lb> zwJpLXd0OT97294OA^bR_aY0E@NvR{hxR?~%vS!S43-W!gxArf{MG=xv^oS}${!bI} Vj{|*Nr<1+JzzAEs>yzY+{{iv64I%&l delta 2662 zcmcIk3sjTm8Ge)ee=a}CAJ7n5x%~t%Ah%G#aS4cU#EKmkMPQ<}Fe1hVf)ER-mXuj* zDb~vg4pymJfnvw3J79J5Xs0bw(P8VU-RKybQY~W3Y;|X)BDe>zJ#;(U&UVg{!+FT} zy_e^E-sk)3c8FSci*{=q0+G0c$n6Q>IMKXtB1thY6Icl>2Q~ofi5Tqj3L-XwDCB)$ zJW(hQ%twrA8g+=5-XU^DO_?`vComRx6nKV6g*YX0{a+xOdxdC~D}wb1bn^uw(Crzb z#n?0)AWC})ja-SISONSIQC1|1JOHjIa=!)KhV?^4me8BxUq@IxXS_O%=KjQBj} z-%iZ-eME^DiD|q}B;ttaib7U2F}=S6Rul69L*(-nu`ff4&^Mh4A`eFr$wENRb+pI&4o81t(G0>Jv8;c?+W9`7aR7xhv+1&k+SRi34v8l7k}q#HpMO%=U;^ zyW+%#bK=*oVD#P=@rfNE_E@6$ouwG9dX4y_EEM)(#REOCL3o4s>OprR&pZhe?m?sp zm3V)_BJYeuSGN!f>Lr`9Q1=J(CG9+jhYU$Nj>Br|%aW5F$TQ?iy7bk~Yhq7FJM&aq zCm5V&%u6A#tBSD?CK3r}nJtB2B(*c!nY%;_4l<2PPr-mu)5woaqo#2heeW|(Eu&!O z!5q3?1k7cA@xfjq-9F~n(=CuRiTSk>vr!#qE`HXJ^>3JqxBh||xb-lD3y;BY4whR0 z%@Z!Ls=AHPu#HXO12IGcoA(rEvLJ>n=H7u>2H6b(tGnnPuQlA7R*3%o^9F%SGXjyEiZ0^61Ui+?_VW~ z7-iobhvwp4?0bHf;i_|N{{RU2>)B7Y_P_+K)0lXhy*Cbm5u8-)mkoCvmh!j5p`cl6 z98QITe~>QeI879{N}4Z%_5lOZ=FtfvKZEpM#>Wo0OeNC=L#m)4S?ipwI9Z$QXz8bD zoFluv{1Buw$-ce?MIWAQ^oUZ2ZTg1c}Hj?2>|AgZpHn?}Y!Fh~B%rdP3FEI-hW zIUZhBTR;AX`G)Xpsm!Q&+5xuOqyaZ-V+9md<@8xKstHF%b* z8&c!<0j_=mqYXU4HJ)k)p%JdBZzV=_ncLrsQA@_TBaWBqAz43n(!hdQHTP~Y5?O}p z;(YLooaMT-Mi6?7yZK`q8cn!}j#nc-)5ZVHIe6l>OX{C$iDpS$)(ql7@XvAC_z?r* zg)TMEXpleV()|TSqmOhs|3fII8C7b|fI!%Q(ys&Sfj5e!e8RjW}+Yb!?owC0h z1eu*GX=?x;0Do14_wPX@RuwxAqMi+^BT?A*8&RF81ja6RsJe7-z`ZL}UCBT_r@CN- z7dWHpN*=DIW-agjW-Hb&^K-UCfmz@1y3rIU^=CfjDqf<99)8&lC^$Qc-zjm`#=QW!7%t!I@fFcoL!!eIs>$B{#?!7)ou+` zTSLNk5q!GIR3sGG1dFMl!X`Kct$Sy!)*$s#I7= zdB-b9R57(?dAL?W1yoMOWTsLILxzdgVLuLW zkra*0DvC!|;y+ZM>|{i=`C+~J_Nmm#Cf_eu2pTqgtyrHd8cl|8i#jWPlb%GCh^adN zsl)%!fD)Y(KDz&Ri!`51$G<$q=yi2 zQczxOE)BDo*4g5O$mp`Fczxo-a6vFy%-@f54J)+TY}S%EA=)q%>OAWk8expEnEy@E zKjqBxjrp&0T6|KpGy51Ltd{AXog==U?w-;WRlA;>;c{+eskyYmR$#FR&aa|8I+LQ~ wUFXkQ+gJBX%7vLy$vo-y89vvhTFZ+~HX+v>mTDH#O}0vF`O`4|sl^$82ZSevfB*mh diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index d9377054a593a7..0870ff70281eb0 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -308,13 +308,8 @@ - Need at least - 至少需要 - - - - characters! - 个字符! + Need at least %1 characters! + 至少需要 %1 个字符! @@ -479,8 +474,8 @@ location set - for " - 网络名称:" + for "%1" + 网络名称:"%1" @@ -545,52 +540,50 @@ location set 将您的设备与comma账号配对 - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> - <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> - <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> - </ol> - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>在手机上访问 https://connect.comma.ai</li> - <li style='margin-bottom: 50px;'>点击“添加新设备”,扫描右侧二维码</li> - <li style='margin-bottom: 50px;'>将 connect.comma.ai 收藏到您的主屏幕,以便像应用程序一样使用它</li> - </ol> - + + Go to https://connect.comma.ai on your phone + 在手机上访问 https://connect.comma.ai + + + + Click "add new device" and scan the QR code on the right + 点击“添加新设备”,扫描右侧二维码 + + + + Bookmark connect.comma.ai to your home screen to use it like an app + 将 connect.comma.ai 收藏到您的主屏幕,以便像应用程序一样使用它 PrimeAdWidget - + Upgrade Now 现在升级 - + Become a comma prime member at connect.comma.ai 打开connect.comma.ai以注册comma prime会员 - + PRIME FEATURES: comma prime特权: - + Remote access 远程访问 - + 1 year of storage 1年数据存储 - + Developer perks 开发者福利 @@ -598,22 +591,22 @@ location set PrimeUserWidget - + ✓ SUBSCRIBED ✓ 已订阅 - + comma prime comma prime - + CONNECT.COMMA.AI CONNECT.COMMA.AI - + COMMA POINTS COMMA POINTS点数 @@ -855,17 +848,17 @@ location set SetupWidget - + Finish Setup 完成设置 - + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. 将您的设备与comma connect (connect.comma.ai)配对并领取您的comma prime优惠。 - + Pair device 配对设备 @@ -1021,13 +1014,13 @@ location set - Uninstall - 卸载 + UNINSTALL + 卸载 - UNINSTALL - 卸载 + Uninstall %1 + 卸载 %1 @@ -1272,8 +1265,8 @@ location set - Forget Wi-Fi Network " - 忘记WiFi网络" + Forget Wi-Fi Network "%1"? + 忘记WiFi网络 "%1"? diff --git a/selfdrive/ui/translations/main_zh-CHT.qm b/selfdrive/ui/translations/main_zh-CHT.qm index 0d448f2080322661cb712bd3c06b4049fabbf935..029332de238d74b5cf8c40073a31d27ea6f0bf4f 100644 GIT binary patch delta 2149 zcmZ`(eNa^Q6}@lwz1@9p-|hol5Y`3u0}RLlqJ#o2GC+iYDK3={Ctxe;x}tn2ELE#< zON}LnQFNsmgG3_)O=1%vV&X@l2Bl(=IGWTBLuw5wX0#gZBoxK;;Lg~;%FNDT-tXSu zJ@?%6yM2w!ku6N4&K*u9D<|?5fMY~2oFtMvf!_jG1D^nQ5rylCSe$F_6LB#_5i@`@ zfxib9A?7z;47p3Bcu3?ENu&${z5+}HqK5J`^5+tXJw(Q*L{qL3z3THZii`&i0uksN zM>P9&qNqni*>9jQey_{}UL?wm#U&t+yNt;12yhb!=!nYpBcCU##-%=6iCj4Me_{K* zV7DoZ*t)lgY)6URJw(K4iS0&(@OEPRJ_SA?wx1<3loPkvNHjHwxWny4>H<=9K?3y# zQX1!@+88M#L08>M(yrszp1!RM6f^#M@|o)=tEIL$4lO zPn7IO3(j0X!7^GihCEF;?eD#W?<3UV`ZcudrS6H))(}rOH`n02n(2GIwUY4#I6R!FdiBTV-K4 zZxF>ymt_bp=$a!d_Cd$dfwH|_C}BG%HtjY>OFjdt{$yVFr>#b|p6-1txoB zgBReC$UCy@`}~LkwQ@Fk64AsFdC*-B=XSZdAqAbiDX+;zfw&%d^O;5zx-9=di%ud6 z??8UkN1i@eh`X9y*xre#-?61xP+RI}*WQ>#6g-<CWSzN=PK!o4sMZ@vK7wK z?ckcX!5L~R*ZRjzP@{6N|n&7Ok zE@hj3BRcw7`BBApC~Q~$^gR@icPSswybWj6snR35@x~HV+4AccQGv>F?=h@yQ@vTU z2j{-3cRoP=#6;E6n-&=38(yiw8kyYBTbfmPQ*ZI1&;vJGmcYXcH&YRlO&$1dStv#&uZ1^G ziSCT_%S1_s#O#CxM89Yi=cGZ21iM(!gbNMBVtLLuF{ij|v=A3aV#jv8F#{uB*azjJ z{viH!0B8*t2hM{ayF;UB3q`?unwX%!fk?6@aSTKQ=V=bd;XK5tIkp-Y|D&ec{2oj@ z+pXzN!#|-rG`+Sa?8B*=tMl<3brD*>Lv84&NUN`d0uzpD%_HepbBnbJ*R3c#SvzM7 zB%Cm!U6nQ)d5^U9Z{u-KS*(4(6$Pg6)t>qhp3vJR)dQ?UWtXH&#q*s}Df!=eN;FN7 z0t)hoCgs#iFAw3O=pJdV3ochxNfpkUaO-8M@!BSMV43umqXJU>Abqj&36#o_dTyIQ zAX^$7Zh`b^()El#6D4+*!btZ!laz15=%C}0=W*VaBN z_9Y{QQ7jql>Y~Ej^q(166rRG*kmGPHF_yTDWsZ_+moY5XGva6JT<0IB2oYafiZt`H zMDz0m)zZo;qd6?r?1?q#I{$9SU=);BtR$o7ez3_i)fgi@4^8*HZ%Ut}L>19qQd(77 z;j~w-tXcVtS(2wbBtZclG)VP4oAL1|hFlNu`Oug0H2BkcY)@M{!IUO!zVQExGsWHdlT3uPS N;#p@Yp3%&C{{>l+97q5F delta 2681 zcmcIl3s98T89lrE|GVtJyZ?$V+ZJ{Cc_{dR1p@|QL4|^XElTj!phgJ`Zd@M18emX2 zj8S9oy&B&d6>4l^oq)kKiPkF7V1h9dlGveGix1SA+GwX*t+od+iD{?pv@?a-IqbdP zLS2DjQ9W;atqKD0z)mt3gQUjCgLPO;J)c5AliXpE@C3F ziI~fnXL>Ot1kkcZpEN)>kXS$*MO;oi#{7wZ@e~jm3k+@pp6+uR2=5NOLu8J-tI5y} zBV9usqRm8U9>%p)1CfCkcl9kGR)g{SJhHDw=E#lc{>iLJffqx!V4~Z{bDzSb-;qGT zHaxZHPfW_)x!NfD9Wd{0rOG$ zfcd5f>^^-47!!d8&n(UjXzoXCZEjqCc>(v!Rn}3%y+Bxi>iavxDE2m0+-eRqdXFmm zB%{L)tG0f|_4roRnaKhRJ5=Y?dw~JBRM(z(5eRHoONH+P5qauCmpg%&JaxL{p%Txi zXY}ETqchYy+VsG{5$eM$Ni62H`e?>#z?`K1G@Eq?wx~N=(&&t+AoYb;1AzWN)Cf`E z11x=iEc9=(7e;YJo|5b9qJ(ODq;4% zMz*gNTp3i?d`+n8Oa_!{VPPKWXqO7hh1;x>FKo_ukA95mMY+8fEem@ws6p6Xcb9`Z zDD1hEPwW!j_;@1_GC?>rua0`o5Po6gTueKK53jUyeZTPG^*;b!|J_38=tJ~hwJ43G z;7Om0rrP<`uR=_f2a)zUar$fyB<{LcC>^C!PK(cl)l;W4;&a`O(mRRtxL=@HRZz)e z4-ugXiP8{+mgbcA8JShs8`m3Uh`cfP$-Q#S_7w&{6ebdj|=Hd?8+3(!xRiq!&jU#Cs1&FlxP49Xy%NYSzj(qo`o0 z)^RPJ3SQBUYd8WVq-b+h)IDsScJJMX-0#%hoA?=>6w#^+8A7STZ|Ld+mvG{C-NE9E zY&=PK^Qk?Qu10s~Iu+40>Fy_X(Md=AQinA2t`+-bYDm;v>gW9OJ_$PfRxQ}d{c(P~ zUT2+tIeu?l4&`^FUT5MnViWbD+x>tiTJ&RY6*7LVe#!*zB}yNypSpySjrmBQJA)Dr zI;qb;nLD7UuuOaPkI6Gx((G|3TTdMan^ z__Z-JekL%g%{U>2N+i5soVt+(gKWm4tZrh3#!dI8u|S%!VIA+7?MY+Ht5nY3VEm|^ z7&Xh-ev$-*^(Jk77%#uhWFPnii6oguJs{EkD@+GsxgWg4bhv^zYOvST9P(4TH_g9m7hkcOJtrt?}_QssdC^&G`IIdC}@==+HrayOMq`Q-QueB12V@`ROCuwP$! zeo7kiZp$mS@sdYuk#}^Bp*KYNXLW28+bSRVGd(e2nS5r$qKJ>y?MVhy?TaODq_D$2`$Pm;05OiwiGPDxG=lxux{NT=N!ZF%|pD{L9k6QnAJ6 zj;lL)D=N(w(ti#lUUUIt`!%NM_R9ehlu3r%Y1yfNQ_*)c3_nKScx$lFBmKZ9Sp7q# zO;}M=b9;tAly4&BET=PHnd4F1&N<~CWxX|^(PfR*251)*KtaeiabiJ9nbP>~fLxVV zoe%bU?AL)3xcS|KDwM*BB!nY|9|h$-YwkyDMVNy!6vBmKL^8vP0`4a;E(USTtVANS zlK-K4cP9sAB`#9_+?%tlhWX0O}`=EMDe{D|X8vSD~m!=fEipxE7+-{}u&Dbk_ z5(A4x{=ZT%jkbzJdbhb2HLSYHm2WL?O)n`cbb6F&uE=zklI8SNl$6a=LVV}ba{dnW C?SW|k diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index efd893ec4c9f1a..0620382a631fbb 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -308,13 +308,8 @@ - Need at least - 需要至少 - - - - characters! - 個字元! + Need at least %1 characters! + 需要至少 %1 個字元! @@ -481,8 +476,8 @@ location set - for " - 給 " + for "%1" + 給 "%1" @@ -547,52 +542,50 @@ location set 將設備與您的 comma 帳號配對 - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> - <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> - <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> - </ol> - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>用手機連至 https://connect.comma.ai</li> - <li style='margin-bottom: 50px;'>點選 "add new device" 後掃描右邊的二維碼</li> - <li style='margin-bottom: 50px;'>將 connect.comma.ai 加入您的主屏幕,以便像手機 App 一樣使用它</li> - </ol> - + + Go to https://connect.comma.ai on your phone + 用手機連至 https://connect.comma.ai + + + + Click "add new device" and scan the QR code on the right + 點選 "add new device" 後掃描右邊的二維碼 + + + + Bookmark connect.comma.ai to your home screen to use it like an app + 將 connect.comma.ai 加入您的主屏幕,以便像手機 App 一樣使用它 PrimeAdWidget - + Upgrade Now 馬上升級 - + Become a comma prime member at connect.comma.ai 成為 connect.comma.ai 的高級會員 - + PRIME FEATURES: 高級會員特點: - + Remote access 遠程訪問 - + 1 year of storage 一年的雲端行車記錄 - + Developer perks 開發者福利 @@ -600,22 +593,22 @@ location set PrimeUserWidget - + ✓ SUBSCRIBED ✓ 已訂閱 - + comma prime comma 高級會員 - + CONNECT.COMMA.AI CONNECT.COMMA.AI - + COMMA POINTS COMMA 積分 @@ -860,17 +853,17 @@ location set SetupWidget - + Finish Setup 完成設置 - + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. 將您的設備與 comma connect (connect.comma.ai) 配對並領取您的 comma 高級會員優惠。 - + Pair device 配對設備 @@ -1026,13 +1019,13 @@ location set - Uninstall - 卸載 + UNINSTALL + 卸載 - UNINSTALL - 卸載 + Uninstall %1 + 卸載 %1 @@ -1277,8 +1270,8 @@ location set - Forget Wi-Fi Network " - 清除 Wi-Fi 網路 " + Forget Wi-Fi Network "%1"? + 清除 Wi-Fi 網路 "%1"? diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index d872be0d86c522..f06d54b2d50a31 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -10,7 +10,7 @@ LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") -def update_translations(release=False, translations_dir=TRANSLATIONS_DIR): +def update_translations(release=False, vanish=False, translations_dir=TRANSLATIONS_DIR): with open(LANGUAGES_FILE, "r") as f: translation_files = json.load(f) @@ -20,7 +20,10 @@ def update_translations(release=False, translations_dir=TRANSLATIONS_DIR): continue tr_file = os.path.join(translations_dir, f"{file}.ts") - ret = os.system(f"lupdate -recursive {UI_DIR} -ts {tr_file}") + args = f"lupdate -recursive {UI_DIR} -ts {tr_file}" + if vanish: + args += " -no-obsolete" + ret = os.system(args) assert ret == 0 if release: @@ -32,6 +35,7 @@ def update_translations(release=False, translations_dir=TRANSLATIONS_DIR): parser = argparse.ArgumentParser(description="Update translation files for UI", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--release", action="store_true", help="Create compiled QM translation files used by UI") + parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found") args = parser.parse_args() - update_translations(args.release) + update_translations(args.release, args.vanish) From 97d7ee369b9b8193fe1580fbaa00e65a26235432 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 12 Jul 2022 23:11:55 -0700 Subject: [PATCH 331/436] Chrysler: send LKAS control bit while above minSteerSpeed (#25150) * Chrysler: send LKAS control bit while above minSteerSpeed * update refs * rework that a bit * little more * update refs --- selfdrive/car/chrysler/carcontroller.py | 11 ++++++----- selfdrive/car/chrysler/chryslercan.py | 4 ++-- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 8156e7841eb308..00893b6bc45872 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -14,7 +14,7 @@ def __init__(self, dbc_name, CP, VM): self.hud_count = 0 self.last_lkas_falling_edge = 0 - self.lkas_active_prev = False + self.lkas_control_bit_prev = False self.last_button_frame = 0 self.packer = CANPacker(dbc_name) @@ -24,7 +24,8 @@ def update(self, CC, CS, low_speed_alert): can_sends = [] # EPS faults if LKAS re-enables too quickly - lkas_active = CC.latActive and not low_speed_alert and (self.frame - self.last_lkas_falling_edge > 200) + lkas_control_bit = not low_speed_alert and (self.frame - self.last_lkas_falling_edge > 200) + lkas_active = CC.latActive and self.lkas_control_bit_prev # *** control msgs *** @@ -59,12 +60,12 @@ def update(self, CC, CS, low_speed_alert): self.apply_steer_last = apply_steer idx = self.frame // 2 - can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_active, idx)) + can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_control_bit, idx)) self.frame += 1 - if not lkas_active and self.lkas_active_prev: + if not lkas_control_bit and self.lkas_control_bit_prev: self.last_lkas_falling_edge = self.frame - self.lkas_active_prev = lkas_active + self.lkas_control_bit_prev = lkas_control_bit new_actuators = CC.actuators.copy() new_actuators.steer = self.apply_steer_last / self.params.STEER_MAX diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index 632c0d2bcf6c7d..1e26a6d275634c 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -52,12 +52,12 @@ def create_lkas_hud(packer, CP, lkas_active, hud_alert, hud_count, car_model, au return packer.make_can_msg("DAS_6", 0, values) -def create_lkas_command(packer, CP, apply_steer, lat_active, frame): +def create_lkas_command(packer, CP, apply_steer, lkas_control_bit, frame): # LKAS_COMMAND Lane-keeping signal to turn the wheel enabled_val = 2 if CP.carFingerprint in RAM_CARS else 1 values = { "STEERING_TORQUE": apply_steer, - "LKAS_CONTROL_BIT": enabled_val if lat_active else 0, + "LKAS_CONTROL_BIT": enabled_val if lkas_control_bit else 0, } return packer.make_can_msg("LKAS_COMMAND", 0, values, frame % 0x10) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index e77a38de5f180f..c99a1653f4c2c3 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -5efbbdf69e16db3d989bfaf62d10e958e80b9ca2 \ No newline at end of file +7fbe776f271ed2d45abe989736133a5cfa0ec826 \ No newline at end of file From e710ba549a3e0b1aba50ba8d4ed47e695a02c538 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 00:20:35 -0700 Subject: [PATCH 332/436] Car docs diff bot: skip PRs from forks (#25151) * check permissions explicitly * fix syntax * Fix * Diff * fix * revert --- .github/workflows/selfdrive_tests.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index 298ea5fb499266..fc151cc2e2d000 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -10,7 +10,6 @@ env: CL_BASE_IMAGE: openpilot-base-cl DOCKER_REGISTRY: ghcr.io/commaai AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} - HAS_AZURE_TOKEN: $AZURE_TOKEN != '' DOCKER_LOGIN: docker login ghcr.io -u adeebshihadeh -p ${{ secrets.CONTAINER_TOKEN }} BUILD: | @@ -366,7 +365,7 @@ jobs: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - name: Upload reference logs - if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.HAS_AZURE_TOKEN }} + if: ${{ failure() && github.event_name == 'pull_request' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} run: | ${{ env.RUN }} "scons -j$(nproc) && \ CI=1 AZURE_TOKEN='$AZURE_TOKEN' python selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" @@ -551,22 +550,22 @@ jobs: output="${output//$'\n'/'%0A'}" echo "::set-output name=diff::$output" - name: Find comment - if: env.HAS_AZURE_TOKEN - uses: peter-evans/find-comment@v1 + if: ${{ env.AZURE_TOKEN != '' }} + uses: peter-evans/find-comment@1769778a0c5bd330272d749d12c036d65e70d39d id: fc with: issue-number: ${{ github.event.pull_request.number }} body-includes: This PR makes changes to - name: Update comment - if: steps.save_diff.outputs.diff != '' && env.HAS_AZURE_TOKEN - uses: peter-evans/create-or-update-comment@v1 + if: ${{ steps.save_diff.outputs.diff != '' && env.AZURE_TOKEN != '' }} + uses: peter-evans/create-or-update-comment@b95e16d2859ad843a14218d1028da5b2c4cbc4b4 with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} body: "${{ steps.save_diff.outputs.diff }}" edit-mode: replace - name: Delete comment - if: steps.fc.outputs.comment-id != '' && steps.save_diff.outputs.diff == '' && env.HAS_AZURE_TOKEN + if: ${{ steps.fc.outputs.comment-id != '' && steps.save_diff.outputs.diff == '' && env.AZURE_TOKEN != '' }} uses: actions/github-script@v6 with: script: | From a7b778c324bf4eaba7f5db32ec60f88212e2c6fe Mon Sep 17 00:00:00 2001 From: Jafar Al-Gharaibeh Date: Wed, 13 Jul 2022 01:31:52 -0600 Subject: [PATCH 333/436] Mazda: Support CX-9 2022 (#25147) * Mazda: Support CX-9 2022 dongle-id: 8c6e0e30decb68f7 Signed-off-by: Jafar Al-Gharaibeh * update years Co-authored-by: Shane Smiskol --- docs/CARS.md | 2 +- selfdrive/car/mazda/values.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index e24672e9bc881f..754052085d57e0 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -198,7 +198,7 @@ How We Rate The Cars |Lexus|RX 2016-18|All|[3](#footnotes)||||| |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| |Mazda|CX-5 2022|All|||||| -|Mazda|CX-9 2021|All|||||| +|Mazda|CX-9 2021-22|All|||||| |Ram|1500 2019-22|Adaptive Cruise|||||| |Subaru|Crosstrek 2018-19|EyeSight|||||| |Subaru|Impreza 2017-19|EyeSight|||||| diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index 12e9eafc4f6874..e1d6907991372f 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -40,7 +40,7 @@ class MazdaCarInfo(CarInfo): CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-17"), CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017"), CAR.MAZDA6: MazdaCarInfo("Mazda 6 2017"), - CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021"), + CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021-22"), CAR.CX5_2022: MazdaCarInfo("Mazda CX-5 2022"), } @@ -267,6 +267,7 @@ class Buttons: (Ecu.engine, 0x7e0, None): [ b'PXM4-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'PXM4-188K2-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PXM6-188K2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x764, None): [ b'K131-67XK2-E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', @@ -279,9 +280,11 @@ class Buttons: b'GSH7-67XK2-M\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-N\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', b'GSH7-67XK2-P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'GSH7-67XK2-S\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.transmission, 0x7e1, None): [ b'PXM4-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + b'PXM6-21PS1-B\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], } } From 3a8f17111c3439cb22eb627aec805898d9d9a41a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 01:10:56 -0700 Subject: [PATCH 334/436] Log VIN response address (#25148) * log vin rx addr * clean up --- selfdrive/car/car_helpers.py | 8 ++++---- selfdrive/car/fw_versions.py | 6 +++--- selfdrive/car/isotp_parallel_query.py | 2 +- selfdrive/car/vin.py | 10 +++++----- selfdrive/debug/disable_ecu.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index b6bdece676a94f..1a9a5f50f3bced 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -93,17 +93,17 @@ def fingerprint(logcan, sendcan): if cached_params is not None and len(cached_params.carFw) > 0 and cached_params.carVin is not VIN_UNKNOWN: cloudlog.warning("Using cached CarParams") - vin = cached_params.carVin + vin, vin_rx_addr = cached_params.carVin, 0 car_fw = list(cached_params.carFw) else: cloudlog.warning("Getting VIN & FW versions") - _, vin = get_vin(logcan, sendcan, bus) + _, vin_rx_addr, vin = get_vin(logcan, sendcan, bus) ecu_rx_addrs = get_present_ecus(logcan, sendcan) car_fw = get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs) exact_fw_match, fw_candidates = match_fw_to_car(car_fw) else: - vin = VIN_UNKNOWN + vin, vin_rx_addr = VIN_UNKNOWN, 0 exact_fw_match, fw_candidates, car_fw = True, set(), [] if len(vin) != 17: @@ -166,7 +166,7 @@ def fingerprint(logcan, sendcan): source = car.CarParams.FingerprintSource.fixed cloudlog.event("fingerprinted", car_fingerprint=car_fingerprint, source=source, fuzzy=not exact_match, - fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), error=True) + fw_count=len(car_fw), ecu_responses=list(ecu_rx_addrs), vin_rx_addr=vin_rx_addr, error=True) return car_fingerprint, finger, vin, car_fw, source, exact_match diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index a8f5357f0e408e..5a33cdf6b7e965 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -444,7 +444,7 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, if addrs: query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) - fw_versions.update({(r.brand, addr): (version, r) for addr, version in query.get_data(timeout).items()}) + fw_versions.update({(r.brand, addr): (version, r) for (addr, _), version in query.get_data(timeout).items()}) except Exception: cloudlog.warning(f"FW query exception: {traceback.format_exc()}") @@ -497,8 +497,8 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, t = time.time() print("Getting vin...") - addr, vin = get_vin(logcan, sendcan, 1, retry=10, debug=args.debug) - print(f"VIN: {vin}") + addr, vin_rx_addr, vin = get_vin(logcan, sendcan, 1, retry=10, debug=args.debug) + print(f'TX: {hex(addr)}, RX: {hex(vin_rx_addr)}, VIN: {vin}') print(f"Getting VIN took {time.time() - t:.3f} s") print() diff --git a/selfdrive/car/isotp_parallel_query.py b/selfdrive/car/isotp_parallel_query.py index 0e807512cfba99..bb96572c33e9a2 100644 --- a/selfdrive/car/isotp_parallel_query.py +++ b/selfdrive/car/isotp_parallel_query.py @@ -126,7 +126,7 @@ def get_data(self, timeout, total_timeout=60.): msg.send(self.request[counter + 1]) request_counter[tx_addr] += 1 else: - results[tx_addr] = dat[len(expected_response):] + results[(tx_addr, msg._can_client.rx_addr)] = dat[len(expected_response):] request_done[tx_addr] = True else: error_code = dat[2] if len(dat) > 2 else -1 diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index fd1ca61e665b52..007c10e7728b01 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -22,18 +22,18 @@ def get_vin(logcan, sendcan, bus, timeout=0.1, retry=5, debug=False): for request, response in ((UDS_VIN_REQUEST, UDS_VIN_RESPONSE), (OBD_VIN_REQUEST, OBD_VIN_RESPONSE)): try: query = IsoTpParallelQuery(sendcan, logcan, bus, FUNCTIONAL_ADDRS, [request, ], [response, ], functional_addr=True, debug=debug) - for addr, vin in query.get_data(timeout).items(): + for (addr, rx_addr), vin in query.get_data(timeout).items(): # Honda Bosch response starts with a length, trim to correct length if vin.startswith(b'\x11'): vin = vin[1:18] - return addr[0], vin.decode() + return addr[0], rx_addr, vin.decode() print(f"vin query retry ({i+1}) ...") except Exception: cloudlog.warning(f"VIN query exception: {traceback.format_exc()}") - return 0, VIN_UNKNOWN + return 0, 0, VIN_UNKNOWN if __name__ == "__main__": @@ -41,5 +41,5 @@ def get_vin(logcan, sendcan, bus, timeout=0.1, retry=5, debug=False): sendcan = messaging.pub_sock('sendcan') logcan = messaging.sub_sock('can') time.sleep(1) - addr, vin = get_vin(logcan, sendcan, 1, debug=False) - print(hex(addr), vin) + addr, vin_rx_addr, vin = get_vin(logcan, sendcan, 1, debug=False) + print(f'TX: {hex(addr)}, RX: {hex(vin_rx_addr)}, VIN: {vin}') diff --git a/selfdrive/debug/disable_ecu.py b/selfdrive/debug/disable_ecu.py index af007207eb2771..c01c22fdd0b4f7 100644 --- a/selfdrive/debug/disable_ecu.py +++ b/selfdrive/debug/disable_ecu.py @@ -16,7 +16,7 @@ def disable_ecu(ecu_addr, logcan, sendcan, bus, timeout=0.5, retry=5, debug=Fals try: # enter extended diagnostic session query = IsoTpParallelQuery(sendcan, logcan, bus, [ecu_addr], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], debug=debug) - for addr, dat in query.get_data(timeout).items(): # pylint: disable=unused-variable + for _, _ in query.get_data(timeout).items(): # pylint: disable=unused-variable print("ecu communication control disable tx/rx ...") # communication control disable tx and rx query = IsoTpParallelQuery(sendcan, logcan, bus, [ecu_addr], [COM_CONT_REQUEST], [COM_CONT_RESPONSE], debug=debug) From 0edd8201cb8d8f21f361d0be581b02bc43c1b80e Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 13 Jul 2022 15:12:49 +0200 Subject: [PATCH 335/436] README.md: update directory structure --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 34b17625f90166..3dce9d4475bb87 100755 --- a/README.md +++ b/README.md @@ -105,23 +105,25 @@ Directory Structure ├── third_party # External libraries ├── pyextra # Extra python packages └── system # Generic services + ├── camerad # Driver to capture images from the camera sensors + ├── clocksd # Broadcasts current time + ├── hardware # Hardware abstraction classes ├── logcatd # systemd journal as a service └── proclogd # Logs information from /proc └── selfdrive # Code needed to drive the car ├── assets # Fonts, images, and sounds for UI ├── athena # Allows communication with the app ├── boardd # Daemon to talk to the board - ├── camerad # Driver to capture images from the camera sensors ├── car # Car specific code to read states and control actuators - ├── common # Shared C/C++ code for the daemons ├── controls # Planning and controls ├── debug # Tools to help you debug and do car ports ├── locationd # Precise localization and vehicle parameter estimation ├── loggerd # Logger and uploader of car data + ├── manager # Deamon that starts/stops all other daemons as needed ├── modeld # Driving and monitoring model runners - ├── proclogd # Logs information from proc - ├── sensord # IMU interface code + ├── monitoring # Daemon to determine driver attention ├── navd # Turn-by-turn navigation + ├── sensord # IMU interface code ├── test # Unit tests, system tests, and a car simulator └── ui # The UI From a006cd168ea5a2fec2f03b29460a3a685538b9cf Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 13 Jul 2022 17:55:05 +0200 Subject: [PATCH 336/436] run pre-commit in release CI (#25158) * run pre-commit in release * add pylintrc and init files * build first * add mypy ini * limit amount of debug scripts shipped in release * add python version? * add more missing __init__.py * excluded rednose for cppcheck * remove files before dirty check --- .github/workflows/selfdrive_tests.yaml | 7 +++++++ .pre-commit-config.yaml | 2 +- release/files_common | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index fc151cc2e2d000..fbc0d94194f5e0 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -58,6 +58,9 @@ jobs: run: | TARGET_DIR=$STRIPPED_DIR release/build_devel.sh cp Dockerfile.openpilot_base $STRIPPED_DIR + cp .pre-commit-config.yaml $STRIPPED_DIR + cp .pylintrc $STRIPPED_DIR + cp mypy.ini $STRIPPED_DIR - name: Build Docker image run: | eval "$BUILD" @@ -66,6 +69,10 @@ jobs: run: | cd $STRIPPED_DIR ${{ env.RUN }} "CI=1 python selfdrive/manager/build.py && \ + pre-commit run --all && \ + rm .pre-commit-config.yaml && \ + rm .pylintrc && \ + rm mypy.ini && \ release/check-dirty.sh && \ python -m unittest discover selfdrive/car" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b901e07721ae2c..8b8bc1f1b9ce67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: entry: cppcheck language: system types: [c++] - exclude: '^(third_party/)|(pyextra/)|(cereal/)|(opendbc/)|(panda/)|(tools/)|(selfdrive/modeld/thneed/debug/)|(selfdrive/modeld/test/)|(selfdrive/camerad/test/)/|(installer/)' + exclude: '^(third_party/)|(pyextra/)|(cereal/)|(rednose/)|(rednose_repo/)|(opendbc/)|(panda/)|(tools/)|(selfdrive/modeld/thneed/debug/)|(selfdrive/modeld/test/)|(selfdrive/camerad/test/)/|(installer/)' args: - --error-exitcode=1 - --language=c++ diff --git a/release/files_common b/release/files_common index fb911705610df3..954726d967c2db 100644 --- a/release/files_common +++ b/release/files_common @@ -57,6 +57,7 @@ common/api/__init__.py release/* +tools/__init__.py tools/lib/* tools/joystick/* tools/replay/*.cc @@ -128,7 +129,15 @@ system/clocksd/.gitignore system/clocksd/SConscript system/clocksd/clocksd.cc -selfdrive/debug/*.py +selfdrive/debug/can_printer.py +selfdrive/debug/check_freq.py +selfdrive/debug/dump.py +selfdrive/debug/filter_log_message.py +selfdrive/debug/get_fingerprint.py +selfdrive/debug/uiview.py + +selfdrive/debug/hyundai_enable_radar_points.py +selfdrive/debug/vw_mqb_config.py common/SConscript common/version.h @@ -187,6 +196,9 @@ selfdrive/controls/lib/lateral_mpc_lib/* selfdrive/controls/lib/longitudinal_mpc_lib/* selfdrive/hardware + +system/__init__.py + system/hardware/__init__.py system/hardware/base.h system/hardware/base.py @@ -220,6 +232,7 @@ selfdrive/locationd/laikad_helpers.py selfdrive/locationd/locationd.h selfdrive/locationd/locationd.cc selfdrive/locationd/paramsd.py +selfdrive/locationd/models/__init__.py selfdrive/locationd/models/.gitignore selfdrive/locationd/models/car_kf.py selfdrive/locationd/models/gnss_kf.py @@ -330,6 +343,7 @@ selfdrive/manager/process.py selfdrive/manager/test/__init__.py selfdrive/manager/test/test_manager.py +selfdrive/modeld/__init__.py selfdrive/modeld/SConscript selfdrive/modeld/modeld.cc selfdrive/modeld/dmonitoringmodeld.cc From 49dd56fc241d5ea3e0ab37e10175ad545ef4d528 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 13 Jul 2022 17:58:45 +0200 Subject: [PATCH 337/436] nav: draw inactive lanes with 50% opacity (#25157) * nav: draw inactive lanes with 50% opacity * update ts --- .../navigation/direction_turn_left_inactive.png | Bin 0 -> 7221 bytes .../navigation/direction_turn_right_inactive.png | Bin 0 -> 7239 bytes .../direction_turn_straight_inactive.png | Bin 0 -> 5452 bytes selfdrive/ui/qt/maps/map.cc | 4 ++++ selfdrive/ui/translations/main_ko.ts | 10 +++++----- selfdrive/ui/translations/main_zh-CHS.ts | 10 +++++----- selfdrive/ui/translations/main_zh-CHT.ts | 10 +++++----- 7 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 selfdrive/assets/navigation/direction_turn_left_inactive.png create mode 100644 selfdrive/assets/navigation/direction_turn_right_inactive.png create mode 100644 selfdrive/assets/navigation/direction_turn_straight_inactive.png diff --git a/selfdrive/assets/navigation/direction_turn_left_inactive.png b/selfdrive/assets/navigation/direction_turn_left_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..2946984acd3252f257898cec9f6cda866811adc3 GIT binary patch literal 7221 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 Zo~{oAPRia=wfrNXs-&q{q3|yFKLGp@*MI;3 literal 0 HcmV?d00001 diff --git a/selfdrive/assets/navigation/direction_turn_right_inactive.png b/selfdrive/assets/navigation/direction_turn_right_inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..7d327766af13be26a23159e354cce5df6453f920 GIT binary patch literal 7239 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

52)T=qZliR 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 literal 0 HcmV?d00001 diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc index a486110a736d11..3967416f73bea7 100644 --- a/selfdrive/ui/qt/maps/map.cc +++ b/selfdrive/ui/qt/maps/map.cc @@ -526,6 +526,10 @@ void MapInstructions::updateInstructions(cereal::NavInstruction::Reader instruct fn += "turn_straight"; } + if (!active) { + fn += "_inactive"; + } + auto icon = new QLabel; int wh = active ? 125 : 75; icon->setPixmap(loadPixmap(fn + ICON_SUFFIX, {wh, wh}, Qt::IgnoreAspectRatio)); diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 2fefa69a8076af..07d68774636958 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -338,27 +338,27 @@ MapETA - + eta 도착 - + min - + hr 시간 - + km km - + mi mi diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 0870ff70281eb0..424488fc56a422 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -338,27 +338,27 @@ MapETA - + eta 埃塔 - + min 分钟 - + hr 小时 - + km km - + mi mi diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 0620382a631fbb..1eab75a6b4ca2f 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -338,27 +338,27 @@ MapETA - + eta 埃塔 - + min 分鐘 - + hr 小時 - + km km - + mi mi From 18f2a50501d67345e2fa2e44b6046d412c3452de Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 12:43:36 -0700 Subject: [PATCH 338/436] disable_ecu.py: remove duplicate file (#25154) * remove duplicate function * make executable --- selfdrive/car/disable_ecu.py | 14 ++++++++++++ selfdrive/debug/disable_ecu.py | 40 ---------------------------------- 2 files changed, 14 insertions(+), 40 deletions(-) mode change 100644 => 100755 selfdrive/car/disable_ecu.py delete mode 100644 selfdrive/debug/disable_ecu.py diff --git a/selfdrive/car/disable_ecu.py b/selfdrive/car/disable_ecu.py old mode 100644 new mode 100755 index ac5c6c9f8fd19d..cd3e93fa80c1bd --- a/selfdrive/car/disable_ecu.py +++ b/selfdrive/car/disable_ecu.py @@ -6,6 +6,7 @@ COM_CONT_RESPONSE = b'' + def disable_ecu(logcan, sendcan, bus=0, addr=0x7d0, com_cont_req=b'\x28\x83\x01', timeout=0.1, retry=10, debug=False): """Silence an ECU by disabling sending and receiving messages using UDS 0x28. The ECU will stay silent as long as openpilot keeps sending Tester Present. @@ -26,9 +27,22 @@ def disable_ecu(logcan, sendcan, bus=0, addr=0x7d0, com_cont_req=b'\x28\x83\x01' cloudlog.warning("ecu disabled") return True + except Exception: cloudlog.exception("ecu disable exception") print(f"ecu disable retry ({i+1}) ...") cloudlog.warning("ecu disable failed") return False + + +if __name__ == "__main__": + import time + import cereal.messaging as messaging + sendcan = messaging.pub_sock('sendcan') + logcan = messaging.sub_sock('can') + time.sleep(1) + + # honda bosch radar disable + disabled = disable_ecu(logcan, sendcan, bus=1, addr=0x18DAB0F1, com_cont_req=b'\x28\x83\x03', timeout=0.5, debug=False) + print(f"disabled: {disabled}") diff --git a/selfdrive/debug/disable_ecu.py b/selfdrive/debug/disable_ecu.py deleted file mode 100644 index c01c22fdd0b4f7..00000000000000 --- a/selfdrive/debug/disable_ecu.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -import traceback - -import cereal.messaging as messaging -from selfdrive.car.isotp_parallel_query import IsoTpParallelQuery -from system.swaglog import cloudlog - -EXT_DIAG_REQUEST = b'\x10\x03' -EXT_DIAG_RESPONSE = b'\x50\x03' -COM_CONT_REQUEST = b'\x28\x83\x03' -COM_CONT_RESPONSE = b'' - -def disable_ecu(ecu_addr, logcan, sendcan, bus, timeout=0.5, retry=5, debug=False): - print(f"ecu disable {hex(ecu_addr)} ...") - for i in range(retry): - try: - # enter extended diagnostic session - query = IsoTpParallelQuery(sendcan, logcan, bus, [ecu_addr], [EXT_DIAG_REQUEST], [EXT_DIAG_RESPONSE], debug=debug) - for _, _ in query.get_data(timeout).items(): # pylint: disable=unused-variable - print("ecu communication control disable tx/rx ...") - # communication control disable tx and rx - query = IsoTpParallelQuery(sendcan, logcan, bus, [ecu_addr], [COM_CONT_REQUEST], [COM_CONT_RESPONSE], debug=debug) - query.get_data(0) - return True - print(f"ecu disable retry ({i+1}) ...") - except Exception: - cloudlog.warning(f"ecu disable exception: {traceback.format_exc()}") - - return False - - -if __name__ == "__main__": - import time - sendcan = messaging.pub_sock('sendcan') - logcan = messaging.sub_sock('can') - time.sleep(1) - - # honda bosch radar disable - disabled = disable_ecu(0x18DAB0F1, logcan, sendcan, 1, debug=False) - print(f"disabled: {disabled}") From d5719913a97f7cf3c3fc171d0aa562c7d9cafb15 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 13 Jul 2022 13:22:30 -0700 Subject: [PATCH 339/436] Chrysler: never drop control bit on older models (#25159) * Chrysler: never drop control bit on older models * update refs --- selfdrive/car/chrysler/carcontroller.py | 17 ++++++++++++++--- selfdrive/car/chrysler/interface.py | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 00893b6bc45872..6d158e7cd0c949 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -2,7 +2,7 @@ from common.realtime import DT_CTRL from selfdrive.car import apply_toyota_steer_torque_limits from selfdrive.car.chrysler.chryslercan import create_lkas_hud, create_lkas_command, create_cruise_buttons -from selfdrive.car.chrysler.values import RAM_CARS, CarControllerParams +from selfdrive.car.chrysler.values import CAR, RAM_CARS, CarControllerParams class CarController: @@ -20,11 +20,22 @@ def __init__(self, dbc_name, CP, VM): self.packer = CANPacker(dbc_name) self.params = CarControllerParams(CP) - def update(self, CC, CS, low_speed_alert): + def update(self, CC, CS): can_sends = [] + # TODO: can we make this more sane? why is it different for all the cars? + lkas_control_bit = self.lkas_control_bit_prev + if CS.out.vEgo > self.CP.minSteerSpeed: + lkas_control_bit = True + elif self.CP.carFingerprint in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020, CAR.JEEP_CHEROKEE_2019): + if CS.out.vEgo < (self.CP.minSteerSpeed - 3.0): + lkas_control_bit = False + elif self.CP.carFingerprint in RAM_CARS: + if CS.out.vEgo < (self.CP.minSteerSpeed - 0.5): + lkas_control_bit = False + # EPS faults if LKAS re-enables too quickly - lkas_control_bit = not low_speed_alert and (self.frame - self.last_lkas_falling_edge > 200) + lkas_control_bit = lkas_control_bit and (self.frame - self.last_lkas_falling_edge > 200) lkas_active = CC.latActive and self.lkas_control_bit_prev # *** control msgs *** diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 8826a925236148..acc08954a844dd 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -93,4 +93,4 @@ def _update(self, c): return ret def apply(self, c): - return self.CC.update(c, self.CS, self.low_speed_alert) + return self.CC.update(c, self.CS) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index c99a1653f4c2c3..dff232911e53b0 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -7fbe776f271ed2d45abe989736133a5cfa0ec826 \ No newline at end of file +fa52fa6c6703269e23610b1c6aba8a56b911fbbb \ No newline at end of file From 18c1700f72e9c03750365a302dcb15b5e3d46366 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 14:28:55 -0700 Subject: [PATCH 340/436] RAV4 2022: Add missing engine fw (#25136) Add engine fw from 909a5635aa370818 --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index f40a58b5a74c60..63b7240e2f867b 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1302,6 +1302,7 @@ class ToyotaCarInfo(CarInfo): b'\x01896634AA0000\x00\x00\x00\x00', b'\x01896634AA1000\x00\x00\x00\x00', b'\x01896634A88000\x00\x00\x00\x00', + b'\x01896634A89000\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F0R01100\x00\x00\x00\x00', From 5a7c2f90361e72e9c35e88abd2e11acdc4aba354 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 13 Jul 2022 14:44:34 -0700 Subject: [PATCH 341/436] bump panda --- panda | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panda b/panda index 2abeab913f6432..e51aa5ebce031c 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 2abeab913f6432e4327b07e247b8a46994ac77a1 +Subproject commit e51aa5ebce031c96e802b07d13120a039fa7b82f From c8051154d2a03f94d54f085d491469800c4e915a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 20:03:55 -0700 Subject: [PATCH 342/436] Civic 2022: Add missing fw versions (#25164) Add fingerprints from a918d2895b3210a1 --- selfdrive/car/honda/values.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index b8417ee19bb080..c665b1cd03e7be 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -1406,6 +1406,7 @@ class HondaCarInfo(CarInfo): b'38897-T20-A020\x00\x00', b'38897-T20-A510\x00\x00', b'38897-T21-A010\x00\x00', + b'38897-T20-A210\x00\x00', ], (Ecu.srs, 0x18DA53F1, None): [ b'77959-T20-A970\x00\x00', @@ -1415,6 +1416,7 @@ class HondaCarInfo(CarInfo): b'78108-T21-A220\x00\x00', b'78108-T21-A620\x00\x00', b'78108-T23-A110\x00\x00', + b'78108-T21-A230\x00\x00', ], (Ecu.vsa, 0x18DA28F1, None): [ b'57114-T20-AB40\x00\x00', @@ -1429,6 +1431,7 @@ class HondaCarInfo(CarInfo): b'37805-64L-A540\x00\x00', b'37805-64S-A540\x00\x00', b'37805-64S-A720\x00\x00', + b'37805-64A-A540\x00\x00', ], }, } From 93a1ca8351d93adfc18d5a9c99ce2bb908cb43b6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 13 Jul 2022 23:51:32 -0700 Subject: [PATCH 343/436] Multilanguage Contributing README (#25170) * Create README.md * Update README.md * Update README.md * Update README.md * Update README.md * typo --- selfdrive/ui/translations/README.md | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 selfdrive/ui/translations/README.md diff --git a/selfdrive/ui/translations/README.md b/selfdrive/ui/translations/README.md new file mode 100644 index 00000000000000..0ddb03a24c6b77 --- /dev/null +++ b/selfdrive/ui/translations/README.md @@ -0,0 +1,39 @@ +# Multilanguage + +![multilanguage_onroad](https://user-images.githubusercontent.com/25857203/178912800-2c798af8-78e3-498e-9e19-35906e0bafff.png) + +## Contributing + +Before getting started, make sure you have set up the openpilot Ubuntu development environment by reading the [tools README.md](/tools/README.md). + +### Adding a New Language + +openpilot provides a few tools to help contributors manage their translations and to ensure quality. To get started: + +1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Simplified Chinese is `中文(繁體)`). +2. Generate the translation file (`*.ts`): + ```shell + selfdrive/ui/update_translations.py + ``` +3. Edit the translation file, marking each translation as completed: + ```shell + linguist selfdrive/ui/translations/your_language_file.ts + ``` +4. Save your file and generate the compiled QM file used by the Qt UI: + ```shell + selfdrive/ui/update_translations.py --release + ``` + +### Improving an Existing Language + +Follow the steps above, omitting steps 1. and 2. Any time you edit translations you'll want to make sure to compile them. + +### Testing + +openpilot has a unit test to make sure all translations are up to date with the text in openpilot and that all translations are completed. + +Run and fix any issues: + +```python +selfdrive/ui/tests/test_translations.py +``` From c7771d1783ef7d7e1efb29e81aae44ad26747c5c Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Thu, 14 Jul 2022 11:49:27 +0200 Subject: [PATCH 344/436] Bump laika and catch Downloadfiles exception. --- laika_repo | 2 +- selfdrive/locationd/laikad.py | 3 ++- selfdrive/locationd/test/test_laikad.py | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/laika_repo b/laika_repo index 828612e1b8848c..3ce2628dfc8ddb 160000 --- a/laika_repo +++ b/laika_repo @@ -1 +1 @@ -Subproject commit 828612e1b8848ccf70072d5513c0b7977f1707da +Subproject commit 3ce2628dfc8ddba1769c518f90275e3caca9e8c6 diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 4868e8ae5235e3..13829b22a96e03 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -15,6 +15,7 @@ from common.params import Params, put_nonblocking from laika import AstroDog from laika.constants import SECS_IN_HR, SECS_IN_MIN +from laika.downloader import DownloadFailed from laika.ephemeris import Ephemeris, EphemerisType, convert_ublox_ephem from laika.gps_time import GPSTime from laika.helpers import ConstellationId @@ -212,7 +213,7 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types, cach astro_dog.get_orbit_data(t, only_predictions=True) cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") return astro_dog.orbits, astro_dog.orbit_fetched_times, t - except (RuntimeError, ValueError, IOError) as e: + except (DownloadFailed, RuntimeError, ValueError, IOError) as e: cloudlog.warning(f"No orbit data found or parsing failure: {e}") return None, None, t diff --git a/selfdrive/locationd/test/test_laikad.py b/selfdrive/locationd/test/test_laikad.py index c10a470d1a5b61..bc7a0d7fa49949 100755 --- a/selfdrive/locationd/test/test_laikad.py +++ b/selfdrive/locationd/test/test_laikad.py @@ -8,6 +8,7 @@ from common.params import Params from laika.constants import SECS_IN_DAY +from laika.downloader import DownloadFailed from laika.ephemeris import EphemerisType, GPSEphemeris from laika.gps_time import GPSTime from laika.helpers import ConstellationId, TimeRangeHolder @@ -51,6 +52,9 @@ def get_measurement_mock(gpstime, sat_ephemeris): return meas +GPS_TIME_PREDICTION_ORBITS_RUSSIAN_SRC = GPSTime.from_datetime(datetime(2022, month=1, day=29, hour=12)) + + class TestLaikad(unittest.TestCase): @classmethod @@ -109,7 +113,7 @@ def test_ephemeris_source_in_msg(self): data_mock = defaultdict(str) data_mock['sv_id'] = 1 - gpstime = GPSTime.from_datetime(datetime(2022, month=3, day=1)) + gpstime = GPS_TIME_PREDICTION_ORBITS_RUSSIAN_SRC laikad = Laikad() laikad.fetch_orbits(gpstime, block=True) meas = get_measurement_mock(gpstime, laikad.astro_dog.orbits['R01'][0]) @@ -165,7 +169,13 @@ def test_laika_online_nav_only(self): @mock.patch('laika.downloader.download_and_cache_file') def test_laika_offline(self, downloader_mock): - downloader_mock.side_effect = IOError + downloader_mock.side_effect = DownloadFailed("Mock download failed") + laikad = Laikad(auto_update=False) + laikad.fetch_orbits(GPS_TIME_PREDICTION_ORBITS_RUSSIAN_SRC, block=True) + + @mock.patch('laika.downloader.download_and_cache_file') + def test_download_failed_russian_source(self, downloader_mock): + downloader_mock.side_effect = DownloadFailed laikad = Laikad(auto_update=False) correct_msgs = verify_messages(self.logs, laikad) self.assertEqual(16, len(correct_msgs)) From 3160f86aa085c300204a5d3e1c37d72ea5fd3051 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 14 Jul 2022 10:18:16 -0700 Subject: [PATCH 345/436] bump version to 0.8.16 --- RELEASES.md | 3 +++ common/version.h | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index b87bd2ee7d1d15..b47a18a3dee7db 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,6 @@ +Version 0.8.16 (2022-XX-XX) +======================== + Version 0.8.15 (2022-07-20) ======================== * New driving model diff --git a/common/version.h b/common/version.h index de550d6be5e7c5..bf1c58df1e2920 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.8.15" +#define COMMA_VERSION "0.8.16" From f38204ad260f2af6a01b71191850178d0af03e8e Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 14 Jul 2022 19:21:40 +0200 Subject: [PATCH 346/436] camerad: cleanup unused RGB code (#25172) * camerad: cleanup unused RGB code * hdr is unused * more cleanup * remove envs * remove from sconsfile * fix docs --- cereal | 2 +- common/modeldata.h | 5 +- docs/c_docs.rst | 2 - release/files_common | 5 - system/camerad/SConscript | 2 - system/camerad/cameras/camera_common.cc | 54 +----- system/camerad/cameras/camera_common.h | 16 +- system/camerad/cameras/camera_qcom2.cc | 26 +-- system/camerad/cameras/camera_qcom2.h | 2 +- system/camerad/transforms/rgb_to_yuv.cc | 36 ---- system/camerad/transforms/rgb_to_yuv.h | 14 -- system/camerad/transforms/rgb_to_yuv_test.cc | 191 ------------------- 12 files changed, 20 insertions(+), 335 deletions(-) delete mode 100644 system/camerad/transforms/rgb_to_yuv.cc delete mode 100644 system/camerad/transforms/rgb_to_yuv.h delete mode 100644 system/camerad/transforms/rgb_to_yuv_test.cc diff --git a/cereal b/cereal index cda60ec9652c05..3ed1b8c51afb61 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit cda60ec9652c05de4ccfcad1fae7936e708434a3 +Subproject commit 3ed1b8c51afb616880565e5868806cef82bbc835 diff --git a/common/modeldata.h b/common/modeldata.h index b1938414164150..e13840d53eb07d 100644 --- a/common/modeldata.h +++ b/common/modeldata.h @@ -34,12 +34,13 @@ const mat3 ecam_intrinsic_matrix = (mat3){{567.0, 0.0, 1928.0 / 2, 0.0, 567.0, 1208.0 / 2, 0.0, 0.0, 1.0}}; -static inline mat3 get_model_yuv_transform(bool bayer = true) { +static inline mat3 get_model_yuv_transform() { float db_s = 1.0; const mat3 transform = (mat3){{ 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 }}; - return bayer ? transform_scale_buffer(transform, db_s) : transform; + // Can this be removed since scale is 1? + return transform_scale_buffer(transform, db_s); } diff --git a/docs/c_docs.rst b/docs/c_docs.rst index 5638b40bf04428..94d0adb560f719 100644 --- a/docs/c_docs.rst +++ b/docs/c_docs.rst @@ -29,8 +29,6 @@ camerad ^^^^^^^ .. autodoxygenindex:: :project: system_camerad_cameras -.. autodoxygenindex:: - :project: system_camerad_transforms .. autodoxygenindex:: :project: system_camerad_imgproc diff --git a/release/files_common b/release/files_common index 954726d967c2db..38f86d247a5ca9 100644 --- a/release/files_common +++ b/release/files_common @@ -324,11 +324,6 @@ system/camerad/cameras/camera_common.h system/camerad/cameras/camera_common.cc system/camerad/cameras/sensor2_i2c.h -system/camerad/transforms/rgb_to_yuv.cc -system/camerad/transforms/rgb_to_yuv.h -system/camerad/transforms/rgb_to_yuv.cl -system/camerad/transforms/rgb_to_yuv_test.cc - system/camerad/imgproc/conv.cl system/camerad/imgproc/pool.cl system/camerad/imgproc/utils.cc diff --git a/system/camerad/SConscript b/system/camerad/SConscript index b181fbab75cbb9..25e366210f3f1f 100644 --- a/system/camerad/SConscript +++ b/system/camerad/SConscript @@ -10,7 +10,6 @@ if arch == "larch64": env.Program('camerad', [ 'main.cc', 'cameras/camera_common.cc', - 'transforms/rgb_to_yuv.cc', 'imgproc/utils.cc', cameras, ], LIBS=libs) @@ -19,5 +18,4 @@ if GetOption("test"): env.Program('test/ae_gray_test', [ 'test/ae_gray_test.cc', 'cameras/camera_common.cc', - 'transforms/rgb_to_yuv.cc', ], LIBS=libs) diff --git a/system/camerad/cameras/camera_common.cc b/system/camerad/cameras/camera_common.cc index 04f3136485f603..1d4ecd526a041a 100644 --- a/system/camerad/cameras/camera_common.cc +++ b/system/camerad/cameras/camera_common.cc @@ -32,15 +32,14 @@ class Debayer { Debayer(cl_device_id device_id, cl_context context, const CameraBuf *b, const CameraState *s, int buf_width, int uv_offset) { char args[4096]; const CameraInfo *ci = &s->ci; - hdr_ = ci->hdr; snprintf(args, sizeof(args), "-cl-fast-relaxed-math -cl-denorms-are-zero " "-DFRAME_WIDTH=%d -DFRAME_HEIGHT=%d -DFRAME_STRIDE=%d -DFRAME_OFFSET=%d " "-DRGB_WIDTH=%d -DRGB_HEIGHT=%d -DRGB_STRIDE=%d -DYUV_STRIDE=%d -DUV_OFFSET=%d " - "-DBAYER_FLIP=%d -DHDR=%d -DCAM_NUM=%d%s", + "-DCAM_NUM=%d%s", ci->frame_width, ci->frame_height, ci->frame_stride, ci->frame_offset, b->rgb_width, b->rgb_height, b->rgb_stride, buf_width, uv_offset, - ci->bayer_flip, ci->hdr, s->camera_num, s->camera_num==1 ? " -DVIGNETTING" : ""); + s->camera_num, s->camera_num==1 ? " -DVIGNETTING" : ""); const char *cl_file = "cameras/real_debayer.cl"; cl_program prg_debayer = cl_program_from_file(context, device_id, cl_file, args); krnl_ = CL_CHECK_ERR(clCreateKernel(prg_debayer, "debayer10", &err)); @@ -63,12 +62,10 @@ class Debayer { private: cl_kernel krnl_; - bool hdr_; }; -void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType init_rgb_type, VisionStreamType init_yuv_type) { +void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType init_yuv_type) { vipc_server = v; - this->rgb_type = init_rgb_type; this->yuv_type = init_yuv_type; const CameraInfo *ci = &s->ci; @@ -89,11 +86,7 @@ void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, rgb_width = ci->frame_width; rgb_height = ci->frame_height; - yuv_transform = get_model_yuv_transform(ci->bayer); - - vipc_server->create_buffers(rgb_type, UI_BUF_COUNT, true, rgb_width, rgb_height); - rgb_stride = vipc_server->get_buffer(rgb_type)->stride; - LOGD("created %d UI vipc buffers with size %dx%d", UI_BUF_COUNT, rgb_width, rgb_height); + yuv_transform = get_model_yuv_transform(); int nv12_width = VENUS_Y_STRIDE(COLOR_FMT_NV12, rgb_width); int nv12_height = VENUS_Y_SCANLINES(COLOR_FMT_NV12, rgb_height); @@ -104,10 +97,7 @@ void CameraBuf::init(cl_device_id device_id, cl_context context, CameraState *s, vipc_server->create_buffers_with_sizes(yuv_type, YUV_BUFFER_COUNT, false, rgb_width, rgb_height, nv12_size, nv12_width, nv12_uv_offset); LOGD("created %d YUV vipc buffers with size %dx%d", YUV_BUFFER_COUNT, nv12_width, nv12_height); - if (ci->bayer) { - debayer = new Debayer(device_id, context, this, s, nv12_width, nv12_uv_offset); - } - rgb2yuv = std::make_unique(context, device_id, rgb_width, rgb_height, rgb_stride); + debayer = new Debayer(device_id, context, this, s, nv12_width, nv12_uv_offset); #ifdef __APPLE__ q = CL_CHECK_ERR(clCreateCommandQueue(context, device_id, 0, &err)); @@ -135,7 +125,6 @@ bool CameraBuf::acquire() { } cur_frame_data = camera_bufs_metadata[cur_buf_idx]; - cur_rgb_buf = vipc_server->get_buffer(rgb_type); cur_yuv_buf = vipc_server->get_buffer(yuv_type); cl_mem camrabuf_cl = camera_bufs[cur_buf_idx].buf_cl; cl_event event; @@ -144,12 +133,7 @@ bool CameraBuf::acquire() { cur_camera_buf = &camera_bufs[cur_buf_idx]; - if (debayer) { - debayer->queue(q, camrabuf_cl, cur_yuv_buf->buf_cl, rgb_width, rgb_height, &event); - } else { - assert(rgb_stride == camera_state->ci.frame_stride); - rgb2yuv->queue(q, camrabuf_cl, cur_rgb_buf->buf_cl); - } + debayer->queue(q, camrabuf_cl, cur_yuv_buf->buf_cl, rgb_width, rgb_height, &event); clWaitForEvents(1, &event); CL_CHECK(clReleaseEvent(event)); @@ -193,32 +177,6 @@ void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &fr framed.setProcessingTime(frame_data.processing_time); } -kj::Array get_frame_image(const CameraBuf *b) { - static const int x_min = util::getenv("XMIN", 0); - static const int y_min = util::getenv("YMIN", 0); - static const int env_xmax = util::getenv("XMAX", -1); - static const int env_ymax = util::getenv("YMAX", -1); - static const int scale = util::getenv("SCALE", 1); - - assert(b->cur_rgb_buf); - - const int x_max = env_xmax != -1 ? env_xmax : b->rgb_width - 1; - const int y_max = env_ymax != -1 ? env_ymax : b->rgb_height - 1; - const int new_width = (x_max - x_min + 1) / scale; - const int new_height = (y_max - y_min + 1) / scale; - const uint8_t *dat = (const uint8_t *)b->cur_rgb_buf->addr; - - kj::Array frame_image = kj::heapArray(new_width*new_height*3); - uint8_t *resized_dat = frame_image.begin(); - int goff = x_min*3 + y_min*b->rgb_stride; - for (int r=0;rrgb_stride*scale+c*3*scale], 3*sizeof(uint8_t)); - } - } - return kj::mv(frame_image); -} - kj::Array get_raw_frame_image(const CameraBuf *b) { const uint8_t *dat = (const uint8_t *)b->cur_camera_buf->addr; diff --git a/system/camerad/cameras/camera_common.h b/system/camerad/cameras/camera_common.h index 4695d4e2c92066..2a56052559be95 100644 --- a/system/camerad/cameras/camera_common.h +++ b/system/camerad/cameras/camera_common.h @@ -9,7 +9,6 @@ #include "cereal/visionipc/visionbuf.h" #include "cereal/visionipc/visionipc.h" #include "cereal/visionipc/visionipc_server.h" -#include "system/camerad/transforms/rgb_to_yuv.h" #include "common/mat.h" #include "common/queue.h" #include "common/swaglog.h" @@ -27,7 +26,6 @@ #define CAMERA_ID_IMX390 9 #define CAMERA_ID_MAX 10 -const int UI_BUF_COUNT = 4; const int YUV_BUFFER_COUNT = 40; enum CameraType { @@ -36,11 +34,6 @@ enum CameraType { WideRoadCam }; -// TODO: remove these once all the internal tools are moved to vipc -const bool env_send_driver = getenv("SEND_DRIVER") != NULL; -const bool env_send_road = getenv("SEND_ROAD") != NULL; -const bool env_send_wide_road = getenv("SEND_WIDE_ROAD") != NULL; - // for debugging const bool env_disable_road = getenv("DISABLE_ROAD") != NULL; const bool env_disable_wide_road = getenv("DISABLE_WIDE_ROAD") != NULL; @@ -51,9 +44,6 @@ const bool env_log_raw_frames = getenv("LOG_RAW_FRAMES") != NULL; typedef struct CameraInfo { uint32_t frame_width, frame_height; uint32_t frame_stride; - bool bayer; - int bayer_flip; - bool hdr; uint32_t frame_offset = 0; uint32_t extra_height = 0; int registers_offset = -1; @@ -92,9 +82,8 @@ class CameraBuf { VisionIpcServer *vipc_server; CameraState *camera_state; Debayer *debayer = nullptr; - std::unique_ptr rgb2yuv; - VisionStreamType rgb_type, yuv_type; + VisionStreamType yuv_type; int cur_buf_idx; @@ -116,7 +105,7 @@ class CameraBuf { CameraBuf() = default; ~CameraBuf(); - void init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType rgb_type, VisionStreamType yuv_type); + void init(cl_device_id device_id, cl_context context, CameraState *s, VisionIpcServer * v, int frame_cnt, VisionStreamType yuv_type); bool acquire(); void release(); void queue(size_t buf_idx); @@ -125,7 +114,6 @@ class CameraBuf { typedef void (*process_thread_cb)(MultiCameraState *s, CameraState *c, int cnt); void fill_frame_data(cereal::FrameData::Builder &framed, const FrameMetadata &frame_data); -kj::Array get_frame_image(const CameraBuf *b); kj::Array get_raw_frame_image(const CameraBuf *b); float set_exposure_target(const CameraBuf *b, int x_start, int x_end, int x_skip, int y_start, int y_end, int y_skip); std::thread start_process_thread(MultiCameraState *cameras, CameraState *cs, process_thread_cb callback); diff --git a/system/camerad/cameras/camera_qcom2.cc b/system/camerad/cameras/camera_qcom2.cc index f001009b9a4665..31af284d52a776 100644 --- a/system/camerad/cameras/camera_qcom2.cc +++ b/system/camerad/cameras/camera_qcom2.cc @@ -46,19 +46,11 @@ CameraInfo cameras_supported[CAMERA_ID_MAX] = { .registers_offset = 0, .frame_offset = AR0231_REGISTERS_HEIGHT, .stats_offset = AR0231_REGISTERS_HEIGHT + FRAME_HEIGHT, - - .bayer = true, - .bayer_flip = 1, - .hdr = false, }, [CAMERA_ID_IMX390] = { .frame_width = FRAME_WIDTH, .frame_height = FRAME_HEIGHT, .frame_stride = FRAME_STRIDE, - - .bayer = true, - .bayer_flip = 1, - .hdr = false, }, }; @@ -614,7 +606,7 @@ void CameraState::enqueue_req_multi(int start, int n, bool dp) { // ******************* camera ******************* -void CameraState::camera_init(MultiCameraState *multi_cam_state_, VisionIpcServer * v, int camera_id_, int camera_num_, unsigned int fps, cl_device_id device_id, cl_context ctx, VisionStreamType rgb_type, VisionStreamType yuv_type, bool enabled_) { +void CameraState::camera_init(MultiCameraState *multi_cam_state_, VisionIpcServer * v, int camera_id_, int camera_num_, unsigned int fps, cl_device_id device_id, cl_context ctx, VisionStreamType yuv_type, bool enabled_) { multi_cam_state = multi_cam_state_; camera_id = camera_id_; camera_num = camera_num_; @@ -638,7 +630,7 @@ void CameraState::camera_init(MultiCameraState *multi_cam_state_, VisionIpcServe exposure_time = 5; cur_ev[0] = cur_ev[1] = cur_ev[2] = (dc_gain_enabled ? DC_GAIN : 1) * sensor_analog_gains[gain_idx] * exposure_time; - buf.init(device_id, ctx, this, v, FRAME_BUF_COUNT, rgb_type, yuv_type); + buf.init(device_id, ctx, this, v, FRAME_BUF_COUNT, yuv_type); } void CameraState::camera_open() { @@ -833,9 +825,9 @@ void CameraState::camera_open() { } void cameras_init(VisionIpcServer *v, MultiCameraState *s, cl_device_id device_id, cl_context ctx) { - s->driver_cam.camera_init(s, v, CAMERA_ID_AR0231, 2, 20, device_id, ctx, VISION_STREAM_RGB_DRIVER, VISION_STREAM_DRIVER, !env_disable_driver); - s->road_cam.camera_init(s, v, CAMERA_ID_AR0231, 1, 20, device_id, ctx, VISION_STREAM_RGB_ROAD, VISION_STREAM_ROAD, !env_disable_road); - s->wide_road_cam.camera_init(s, v, CAMERA_ID_AR0231, 0, 20, device_id, ctx, VISION_STREAM_RGB_WIDE_ROAD, VISION_STREAM_WIDE_ROAD, !env_disable_wide_road); + s->driver_cam.camera_init(s, v, CAMERA_ID_AR0231, 2, 20, device_id, ctx, VISION_STREAM_DRIVER, !env_disable_driver); + s->road_cam.camera_init(s, v, CAMERA_ID_AR0231, 1, 20, device_id, ctx, VISION_STREAM_ROAD, !env_disable_road); + s->wide_road_cam.camera_init(s, v, CAMERA_ID_AR0231, 0, 20, device_id, ctx, VISION_STREAM_WIDE_ROAD, !env_disable_wide_road); s->pm = new PubMaster({"roadCameraState", "driverCameraState", "wideRoadCameraState", "thumbnail"}); } @@ -1233,9 +1225,7 @@ static void process_driver_camera(MultiCameraState *s, CameraState *c, int cnt) auto framed = msg.initEvent().initDriverCameraState(); framed.setFrameType(cereal::FrameData::FrameType::FRONT); fill_frame_data(framed, c->buf.cur_frame_data); - if (env_send_driver) { - framed.setImage(get_frame_image(&c->buf)); - } + if (c->camera_id == CAMERA_ID_AR0231) { ar0231_process_registers(s, c, framed); } @@ -1248,9 +1238,7 @@ void process_road_camera(MultiCameraState *s, CameraState *c, int cnt) { MessageBuilder msg; auto framed = c == &s->road_cam ? msg.initEvent().initRoadCameraState() : msg.initEvent().initWideRoadCameraState(); fill_frame_data(framed, b->cur_frame_data); - if ((c == &s->road_cam && env_send_road) || (c == &s->wide_road_cam && env_send_wide_road)) { - framed.setImage(get_frame_image(b)); - } else if (env_log_raw_frames && c == &s->road_cam && cnt % 100 == 5) { // no overlap with qlog decimation + if (env_log_raw_frames && c == &s->road_cam && cnt % 100 == 5) { // no overlap with qlog decimation framed.setImage(get_raw_frame_image(b)); } LOGT(c->buf.cur_frame_data.frame_id, "%s: Image set", c == &s->road_cam ? "RoadCamera" : "WideRoadCamera"); diff --git a/system/camerad/cameras/camera_qcom2.h b/system/camerad/cameras/camera_qcom2.h index 57fef8d49a4362..03d3d1a8232508 100644 --- a/system/camerad/cameras/camera_qcom2.h +++ b/system/camerad/cameras/camera_qcom2.h @@ -55,7 +55,7 @@ class CameraState { void sensors_start(); void camera_open(); - void camera_init(MultiCameraState *multi_cam_state, VisionIpcServer * v, int camera_id, int camera_num, unsigned int fps, cl_device_id device_id, cl_context ctx, VisionStreamType rgb_type, VisionStreamType yuv_type, bool enabled); + void camera_init(MultiCameraState *multi_cam_state, VisionIpcServer * v, int camera_id, int camera_num, unsigned int fps, cl_device_id device_id, cl_context ctx, VisionStreamType yuv_type, bool enabled); void camera_close(); std::map ar0231_parse_registers(uint8_t *data, std::initializer_list addrs); diff --git a/system/camerad/transforms/rgb_to_yuv.cc b/system/camerad/transforms/rgb_to_yuv.cc deleted file mode 100644 index 5e51579cf9d89f..00000000000000 --- a/system/camerad/transforms/rgb_to_yuv.cc +++ /dev/null @@ -1,36 +0,0 @@ -#include "system/camerad/transforms/rgb_to_yuv.h" - -#include -#include - -Rgb2Yuv::Rgb2Yuv(cl_context ctx, cl_device_id device_id, int width, int height, int rgb_stride) { - assert(width % 2 == 0 && height % 2 == 0); - char args[1024]; - snprintf(args, sizeof(args), - "-cl-fast-relaxed-math -cl-denorms-are-zero " -#ifdef CL_DEBUG - "-DCL_DEBUG " -#endif - "-DWIDTH=%d -DHEIGHT=%d -DUV_WIDTH=%d -DUV_HEIGHT=%d -DRGB_STRIDE=%d -DRGB_SIZE=%d", - width, height, width / 2, height / 2, rgb_stride, width * height); - - cl_program prg = cl_program_from_file(ctx, device_id, "transforms/rgb_to_yuv.cl", args); - krnl = CL_CHECK_ERR(clCreateKernel(prg, "rgb_to_yuv", &err)); - CL_CHECK(clReleaseProgram(prg)); - - work_size[0] = (width + (width % 4 == 0 ? 0 : (4 - width % 4))) / 4; - work_size[1] = (height + (height % 4 == 0 ? 0 : (4 - height % 4))) / 4; -} - -Rgb2Yuv::~Rgb2Yuv() { - CL_CHECK(clReleaseKernel(krnl)); -} - -void Rgb2Yuv::queue(cl_command_queue q, cl_mem rgb_cl, cl_mem yuv_cl) { - CL_CHECK(clSetKernelArg(krnl, 0, sizeof(cl_mem), &rgb_cl)); - CL_CHECK(clSetKernelArg(krnl, 1, sizeof(cl_mem), &yuv_cl)); - cl_event event; - CL_CHECK(clEnqueueNDRangeKernel(q, krnl, 2, NULL, &work_size[0], NULL, 0, 0, &event)); - CL_CHECK(clWaitForEvents(1, &event)); - CL_CHECK(clReleaseEvent(event)); -} diff --git a/system/camerad/transforms/rgb_to_yuv.h b/system/camerad/transforms/rgb_to_yuv.h deleted file mode 100644 index e1de180d408526..00000000000000 --- a/system/camerad/transforms/rgb_to_yuv.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "common/clutil.h" - -class Rgb2Yuv { -public: - Rgb2Yuv(cl_context ctx, cl_device_id device_id, int width, int height, int rgb_stride); - ~Rgb2Yuv(); - void queue(cl_command_queue q, cl_mem rgb_cl, cl_mem yuv_cl); -private: - size_t work_size[2]; - cl_kernel krnl; -}; - diff --git a/system/camerad/transforms/rgb_to_yuv_test.cc b/system/camerad/transforms/rgb_to_yuv_test.cc deleted file mode 100644 index 2f909e3b73c145..00000000000000 --- a/system/camerad/transforms/rgb_to_yuv_test.cc +++ /dev/null @@ -1,191 +0,0 @@ -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef ANDROID - -#define MAXE 0 -#include - -#else -// The libyuv implementation on ARM is slightly different than on x86 -// Our implementation matches the ARM version, so accept errors of 1 -#define MAXE 1 - -#endif - -#include - -#include "libyuv.h" -#include "system/camerad/transforms/rgb_to_yuv.h" -#include "common/clutil.h" - -static inline double millis_since_boot() { - struct timespec t; - clock_gettime(CLOCK_BOOTTIME, &t); - return t.tv_sec * 1000.0 + t.tv_nsec * 1e-6; -} - -void cl_init(cl_device_id &device_id, cl_context &context) { - device_id = cl_get_device_id(CL_DEVICE_TYPE_DEFAULT); - context = CL_CHECK_ERR(clCreateContext(NULL, 1, &device_id, NULL, NULL, &err)); -} - - -bool compare_results(uint8_t *a, uint8_t *b, int len, int stride, int width, int height, uint8_t *rgb) { - int min_diff = 0., max_diff = 0., max_e = 0.; - int e1 = 0, e0 = 0; - int e0y = 0, e0u = 0, e0v = 0, e1y = 0, e1u = 0, e1v = 0; - int max_e_i = 0; - for (int i = 0;i < len;i++) { - int e = ((int)a[i]) - ((int)b[i]); - if(e < min_diff) { - min_diff = e; - } - if(e > max_diff) { - max_diff = e; - } - int e_abs = std::abs(e); - if(e_abs > max_e) { - max_e = e_abs; - max_e_i = i; - } - if(e_abs < 1) { - e0++; - if(i < stride * height) - e0y++; - else if(i < stride * height + stride * height / 4) - e0u++; - else - e0v++; - } else { - e1++; - if(i < stride * height) - e1y++; - else if(i < stride * height + stride * height / 4) - e1u++; - else - e1v++; - } - } - //printf("max diff : %d, min diff : %d, e < 1: %d, e >= 1: %d\n", max_diff, min_diff, e0, e1); - //printf("Y: e < 1: %d, e >= 1: %d, U: e < 1: %d, e >= 1: %d, V: e < 1: %d, e >= 1: %d\n", e0y, e1y, e0u, e1u, e0v, e1v); - if(max_e <= MAXE) { - return true; - } - int row = max_e_i / stride; - if(row < height) { - printf("max error is Y: %d = (libyuv: %u - cl: %u), row: %d, col: %d\n", max_e, a[max_e_i], b[max_e_i], row, max_e_i % stride); - } else if(row >= height && row < (height + height / 4)) { - printf("max error is U: %d = %u - %u, row: %d, col: %d\n", max_e, a[max_e_i], b[max_e_i], (row - height) / 2, max_e_i % stride / 2); - } else { - printf("max error is V: %d = %u - %u, row: %d, col: %d\n", max_e, a[max_e_i], b[max_e_i], (row - height - height / 4) / 2, max_e_i % stride / 2); - } - return false; -} - -int main(int argc, char** argv) { - srand(1337); - - cl_device_id device_id; - cl_context context; - cl_init(device_id, context) ; - - int err; - const cl_queue_properties props[] = {0}; //CL_QUEUE_PRIORITY_KHR, CL_QUEUE_PRIORITY_HIGH_KHR, 0}; - cl_command_queue q = clCreateCommandQueueWithProperties(context, device_id, props, &err); - if(err != 0) { - std::cout << "clCreateCommandQueueWithProperties error: " << err << std::endl; - } - - int width = 1164; - int height = 874; - - int opt = 0; - while ((opt = getopt(argc, argv, "f")) != -1) - { - switch (opt) - { - case 'f': - std::cout << "Using front camera dimensions" << std::endl; - int width = 1152; - int height = 846; - } - } - - std::cout << "Width: " << width << " Height: " << height << std::endl; - uint8_t *rgb_frame = new uint8_t[width * height * 3]; - - - RGBToYUVState rgb_to_yuv_state; - rgb_to_yuv_init(&rgb_to_yuv_state, context, device_id, width, height, width * 3); - - int frame_yuv_buf_size = width * height * 3 / 2; - cl_mem yuv_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, frame_yuv_buf_size, (void*)NULL, &err)); - uint8_t *frame_yuv_buf = new uint8_t[frame_yuv_buf_size]; - uint8_t *frame_yuv_ptr_y = frame_yuv_buf; - uint8_t *frame_yuv_ptr_u = frame_yuv_buf + (width * height); - uint8_t *frame_yuv_ptr_v = frame_yuv_ptr_u + ((width/2) * (height/2)); - - cl_mem rgb_cl = CL_CHECK_ERR(clCreateBuffer(context, CL_MEM_READ_WRITE, width * height * 3, (void*)NULL, &err)); - int mismatched = 0; - int counter = 0; - srand (time(NULL)); - - for (int i = 0; i < 100; i++) { - for (int i = 0; i < width * height * 3; i++) { - rgb_frame[i] = (uint8_t)rand(); - } - - double t1 = millis_since_boot(); - libyuv::RGB24ToI420((uint8_t*)rgb_frame, width * 3, - frame_yuv_ptr_y, width, - frame_yuv_ptr_u, width/2, - frame_yuv_ptr_v, width/2, - width, height); - double t2 = millis_since_boot(); - //printf("Libyuv: rgb to yuv: %.2fms\n", t2-t1); - - clEnqueueWriteBuffer(q, rgb_cl, CL_TRUE, 0, width * height * 3, (void *)rgb_frame, 0, NULL, NULL); - t1 = millis_since_boot(); - rgb_to_yuv_queue(&rgb_to_yuv_state, q, rgb_cl, yuv_cl); - t2 = millis_since_boot(); - - //printf("OpenCL: rgb to yuv: %.2fms\n", t2-t1); - uint8_t *yyy = (uint8_t *)clEnqueueMapBuffer(q, yuv_cl, CL_TRUE, - CL_MAP_READ, 0, frame_yuv_buf_size, - 0, NULL, NULL, &err); - if(!compare_results(frame_yuv_ptr_y, yyy, frame_yuv_buf_size, width, width, height, (uint8_t*)rgb_frame)) - mismatched++; - clEnqueueUnmapMemObject(q, yuv_cl, yyy, 0, NULL, NULL); - - // std::this_thread::sleep_for(std::chrono::milliseconds(20)); - if(counter++ % 100 == 0) - printf("Matched: %d, Mismatched: %d\n", counter - mismatched, mismatched); - - } - printf("Matched: %d, Mismatched: %d\n", counter - mismatched, mismatched); - - delete[] frame_yuv_buf; - rgb_to_yuv_destroy(&rgb_to_yuv_state); - clReleaseContext(context); - delete[] rgb_frame; - - if (mismatched == 0) - return 0; - else - return -1; -} From 811bb1b762cb6a58c9bd28387c44de21a02a86f9 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 14 Jul 2022 19:24:30 +0200 Subject: [PATCH 347/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 3ed1b8c51afb61..a4c1afa3bfcbba 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 3ed1b8c51afb616880565e5868806cef82bbc835 +Subproject commit a4c1afa3bfcbba989c128ec9b5092f6c91f4da22 From 98676fb3b05630bf6e62b0a1fbfc8804948a578c Mon Sep 17 00:00:00 2001 From: cydia2020 <12470297+cydia2020@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:34:27 +1000 Subject: [PATCH 348/436] Toyota: don't send UI on Prius V (#25036) * Toyota: don't send UI on Prius V * Update selfdrive/car/toyota/carcontroller.py Co-authored-by: Adeeb Shihadeh --- selfdrive/car/toyota/carcontroller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index 33e3fe118e35b0..61a41b9c51e550 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -126,7 +126,7 @@ def update(self, CC, CS): # forcing the pcm to disengage causes a bad fault sound so play a good sound instead send_ui = True - if self.frame % 100 == 0 or send_ui: + if (self.frame % 100 == 0 or send_ui) and (self.CP.carFingerprint != CAR.PRIUS_V): can_sends.append(create_ui_command(self.packer, steer_alert, pcm_cancel_cmd, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart, CC.enabled)) From e203794b2d2341d5b3c0e39b6ae2b08da30ea203 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Thu, 14 Jul 2022 16:41:20 -0400 Subject: [PATCH 349/436] VW MQB: Update supported models and model-years (#25123) * updates for Atlas * updates for Arteon * regen CARS.md * consolidate wagons, update Golfs * update Jetta and Passat * harness footnotes for Jetta and Passat * update Polo * regen CARS.md --- docs/CARS.md | 45 ++++++++++++++++----------- selfdrive/car/volkswagen/values.py | 49 +++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 754052085d57e0..3e35782b2a4e75 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -70,7 +70,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 70 cars +# Silver - 77 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -133,19 +133,26 @@ How We Rate The Cars |Toyota|RAV4 2022|All|||||| |Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 Hybrid 2022|All|||||| -|Volkswagen|Atlas 2018-19, 2022[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|e-Golf 2014, 2018-20|Driver Assistance|||||| -|Volkswagen|Golf 2015-20|Driver Assistance|||||| -|Volkswagen|Golf Alltrack 2017-18|Driver Assistance|||||| -|Volkswagen|Golf GTE 2016|Driver Assistance|||||| -|Volkswagen|Golf GTI 2018-21|Driver Assistance|||||| -|Volkswagen|Golf R 2016-19|Driver Assistance|||||| -|Volkswagen|Golf SportsVan 2016|Driver Assistance|||||| -|Volkswagen|Golf SportWagen 2015|Driver Assistance|||||| -|Volkswagen|Passat 2015-19[6](#footnotes)|Driver Assistance|||||| -|Volkswagen|Polo 2020|Driver Assistance|||||| - -# Bronze - 80 cars +|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|e-Golf 2014-20|Driver Assistance|||||| +|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance|||||| +|Volkswagen|Golf Alltrack 2015-19|Driver Assistance|||||| +|Volkswagen|Golf GTD 2015-20|Driver Assistance|||||| +|Volkswagen|Golf GTE 2015-20|Driver Assistance|||||| +|Volkswagen|Golf GTI 2015-21|Driver Assistance|||||| +|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance|||||| +|Volkswagen|Golf SportsVan 2015-20|Driver Assistance|||||| +|Volkswagen|Passat 2015-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Passat GTE 2015-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance|||||| + +# Bronze - 83 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -219,11 +226,14 @@ How We Rate The Cars |Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| |Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| |Toyota|Sienna 2018-20|All|[3](#footnotes)||||| -|Volkswagen|Arteon 2018, 2021[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Arteon 2018-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Arteon eHybrid 2020-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Arteon R 2020-22[7](#footnotes)|Driver Assistance|||||| |Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Jetta 2018-21|Driver Assistance|||||| -|Volkswagen|Jetta GLI 2021|Driver Assistance|||||| +|Volkswagen|CC 2018-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance|||||| +|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance|||||| |Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| @@ -239,6 +249,7 @@ How We Rate The Cars 5Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
6Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.
7Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.
+8Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). \ No newline at end of file diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 6e64f705b0e25b..8f38a00d4d84e2 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -109,6 +109,9 @@ class Footnote(Enum): "(older design) or light brown (newer design). For the newer design, in the interim, choose \"VW J533 Development\" " + "from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.", Column.MODEL) + VW_VARIANT = CarFootnote( + "Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)", + Column.MODEL) @dataclass @@ -118,24 +121,42 @@ class VWCarInfo(CarInfo): CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = { - CAR.ARTEON_MK1: VWCarInfo("Volkswagen Arteon 2018, 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), - CAR.ATLAS_MK1: VWCarInfo("Volkswagen Atlas 2018-19, 2022", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + CAR.ARTEON_MK1: [ + VWCarInfo("Volkswagen Arteon 2018-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + VWCarInfo("Volkswagen Arteon R 2020-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + VWCarInfo("Volkswagen Arteon eHybrid 2020-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + VWCarInfo("Volkswagen CC 2018-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + ], + CAR.ATLAS_MK1: [ + VWCarInfo("Volkswagen Atlas 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Atlas Cross Sport 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Teramont 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Teramont Cross Sport 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Teramont X 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + ], CAR.GOLF_MK7: [ - VWCarInfo("Volkswagen e-Golf 2014, 2018-20"), - VWCarInfo("Volkswagen Golf 2015-20"), - VWCarInfo("Volkswagen Golf Alltrack 2017-18"), - VWCarInfo("Volkswagen Golf GTE 2016"), - VWCarInfo("Volkswagen Golf GTI 2018-21"), - VWCarInfo("Volkswagen Golf R 2016-19"), - VWCarInfo("Volkswagen Golf SportsVan 2016"), - VWCarInfo("Volkswagen Golf SportWagen 2015"), + VWCarInfo("Volkswagen e-Golf 2014-20"), + VWCarInfo("Volkswagen Golf 2015-20", footnotes=[Footnote.VW_VARIANT]), + VWCarInfo("Volkswagen Golf Alltrack 2015-19"), + VWCarInfo("Volkswagen Golf GTD 2015-20"), + VWCarInfo("Volkswagen Golf GTE 2015-20"), + VWCarInfo("Volkswagen Golf GTI 2015-21"), + VWCarInfo("Volkswagen Golf R 2015-19", footnotes=[Footnote.VW_VARIANT]), + VWCarInfo("Volkswagen Golf SportsVan 2015-20"), ], CAR.JETTA_MK7: [ - VWCarInfo("Volkswagen Jetta 2018-21"), - VWCarInfo("Volkswagen Jetta GLI 2021"), + VWCarInfo("Volkswagen Jetta 2018-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Jetta GLI 2021-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + ], + CAR.PASSAT_MK8: [ + VWCarInfo("Volkswagen Passat 2015-22", footnotes=[Footnote.VW_HARNESS, Footnote.PASSAT, Footnote.VW_VARIANT], harness=Harness.j533), + VWCarInfo("Volkswagen Passat Alltrack 2015-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Passat GTE 2015-22", footnotes=[Footnote.VW_HARNESS, Footnote.VW_VARIANT], harness=Harness.j533), + ], + CAR.POLO_MK6: [ + VWCarInfo("Volkswagen Polo 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), + VWCarInfo("Volkswagen Polo GTI 2020-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), ], - CAR.PASSAT_MK8: VWCarInfo("Volkswagen Passat 2015-19", footnotes=[Footnote.PASSAT]), - CAR.POLO_MK6: VWCarInfo("Volkswagen Polo 2020"), CAR.TAOS_MK1: VWCarInfo("Volkswagen Taos 2022", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), CAR.TCROSS_MK1: VWCarInfo("Volkswagen T-Cross 2021", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), CAR.TIGUAN_MK2: VWCarInfo("Volkswagen Tiguan 2019-22", footnotes=[Footnote.VW_HARNESS], harness=Harness.j533), From 70678bb772ecec78c174b3a71632045f2f6a8806 Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:55:08 -0500 Subject: [PATCH 350/436] Update Corolla Cross Hybrid f/w & doc (#25178) --- docs/CARS.md | 3 ++- selfdrive/car/toyota/values.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 3e35782b2a4e75..cf5853ff4c427a 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -70,7 +70,7 @@ How We Rate The Cars |Toyota|RAV4 2019-21|All|||||| |Toyota|RAV4 Hybrid 2019-21|All|||||| -# Silver - 77 cars +# Silver - 78 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -126,6 +126,7 @@ How We Rate The Cars |Toyota|Camry 2018-20|All||[4](#footnotes)|||| |Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| |Toyota|Corolla Cross 2020-21 (Non-US only)|All|||||| +|Toyota|Corolla Cross Hybrid 2020-22 (Non-US only)|All|||||| |Toyota|Highlander 2017-19|All|[3](#footnotes)||||| |Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| |Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 63b7240e2f867b..49aa3c9a949283 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -124,6 +124,7 @@ class ToyotaCarInfo(CarInfo): ], CAR.COROLLAH_TSS2: [ ToyotaCarInfo("Toyota Corolla Hybrid 2020-22"), + ToyotaCarInfo("Toyota Corolla Cross Hybrid 2020-22 (Non-US only)", min_enable_speed=7.5), ToyotaCarInfo("Lexus UX Hybrid 2019-22"), ], CAR.HIGHLANDER: ToyotaCarInfo("Toyota Highlander 2017-19", video_link="https://www.youtube.com/watch?v=0wS0wXSLzoo", footnotes=[Footnote.DSU]), @@ -788,6 +789,7 @@ class ToyotaCarInfo(CarInfo): (Ecu.eps, 0x7a1, None): [ b'8965B12361\x00\x00\x00\x00\x00\x00', b'8965B12451\x00\x00\x00\x00\x00\x00', + b'8965B16011\x00\x00\x00\x00\x00\x00', b'8965B76012\x00\x00\x00\x00\x00\x00', b'8965B76050\x00\x00\x00\x00\x00\x00', b'\x018965B12350\x00\x00\x00\x00\x00\x00', @@ -808,15 +810,16 @@ class ToyotaCarInfo(CarInfo): b'F152612800\x00\x00\x00\x00\x00\x00', b'F152612820\x00\x00\x00\x00\x00\x00', b'F152612840\x00\x00\x00\x00\x00\x00', + b'F152612842\x00\x00\x00\x00\x00\x00', b'F152612890\x00\x00\x00\x00\x00\x00', b'F152612A00\x00\x00\x00\x00\x00\x00', b'F152612A10\x00\x00\x00\x00\x00\x00', + b'F152612D00\x00\x00\x00\x00\x00\x00', + b'F152616011\x00\x00\x00\x00\x00\x00', b'F152642540\x00\x00\x00\x00\x00\x00', b'F152676293\x00\x00\x00\x00\x00\x00', b'F152676303\x00\x00\x00\x00\x00\x00', b'F152676304\x00\x00\x00\x00\x00\x00', - b'F152612D00\x00\x00\x00\x00\x00\x00', - b'F152612842\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F3301100\x00\x00\x00\x00', @@ -832,6 +835,7 @@ class ToyotaCarInfo(CarInfo): b'\x028646F1202000\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', b'\x028646F1202100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F1202200\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', + b'\x028646F1601100\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b"\x028646F1601300\x00\x00\x00\x008646G2601400\x00\x00\x00\x00", b'\x028646F4203400\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', b'\x028646F76020C0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00', From 505bbce29d45c246be177d81e618f9289e78aa16 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 14 Jul 2022 19:55:18 -0700 Subject: [PATCH 351/436] controlsd: handle radar missing + cleanup system error handling (#25179) * controlsd: handle radar missing + cleanup system erorr handling * fix carla * update refs --- selfdrive/controls/controlsd.py | 25 +++++++++++-------- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/process_replay/regen.py | 2 +- .../test/process_replay/test_processes.py | 12 ++++----- tools/sim/start_carla.sh | 1 + 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index b344705f9da324..a20a3a9f37ba51 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -96,7 +96,11 @@ def __init__(self, sm=None, pm=None, can_sock=None, CI=None): self.sm = sm if self.sm is None: - ignore = ['driverCameraState', 'managerState'] if SIMULATION else None + ignore = [] + if SIMULATION: + ignore += ['driverCameraState', 'managerState'] + if params.get_bool('WideCameraOnly'): + ignore += ['roadCameraState'] self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'driverMonitoringState', 'longitudinalPlan', 'lateralPlan', 'liveLocationKalman', 'managerState', 'liveParameters', 'radarState'] + self.camera_packets + joystick_packet, @@ -224,12 +228,8 @@ def update_events(self, CS): if not self.CP.notCar: self.events.add_from_msg(self.sm['driverMonitoringState'].events) - # Handle car events. Ignore when CAN is invalid - if CS.canTimeout: - self.events.add(EventName.canBusMissing) - elif not CS.canValid: - self.events.add(EventName.canError) - else: + # Add car events, ignore if CAN isn't valid + if CS.canValid: self.events.add_from_msg(CS.events) # Create events for temperature, disk space, and memory @@ -309,14 +309,19 @@ def update_events(self, CS): self.events.add(EventName.cameraFrameRate) if self.rk.lagging: self.events.add(EventName.controlsdLagging) - if len(self.sm['radarState'].radarErrors): + if len(self.sm['radarState'].radarErrors) or not self.sm.all_checks(['radarState']): self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) + if CS.canTimeout: + self.events.add(EventName.canBusMissing) + elif not CS.canValid: + self.events.add(EventName.canError) # generic catch-all. ideally, a more specific event should be added above instead - no_system_errors = len(self.events) != num_events - if (not self.sm.all_checks() or self.can_rcv_error) and no_system_errors and CS.canValid and not CS.canTimeout: + has_disable_events = self.events.any(ET.NO_ENTRY) and (self.events.any(ET.SOFT_DISABLE) or self.events.any(ET.IMMEDIATE_DISABLE)) + no_system_errors = (not has_disable_events) or (len(self.events) == num_events) + if (not self.sm.all_checks() or self.can_rcv_error) and no_system_errors: if not self.sm.all_alive(): self.events.add(EventName.commIssue) elif not self.sm.all_freq_ok(): diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index dff232911e53b0..00bf28ed8317ed 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -fa52fa6c6703269e23610b1c6aba8a56b911fbbb \ No newline at end of file +7c1168af0311d2fef67b82812cd863a0e97c030e \ No newline at end of file diff --git a/selfdrive/test/process_replay/regen.py b/selfdrive/test/process_replay/regen.py index 1a2d436f1abce7..39f75ce428aac6 100755 --- a/selfdrive/test/process_replay/regen.py +++ b/selfdrive/test/process_replay/regen.py @@ -263,7 +263,7 @@ def regen_segment(lr, frs=None, outdir=FAKEDATA, disable_tqdm=False): seg_path = os.path.join(outdir, segment) # check to make sure openpilot is engaged in the route if not check_enabled(LogReader(os.path.join(seg_path, "rlog"))): - raise Exception(f"Route never enabled: {segment}") + raise Exception(f"Route did not engage for long enough: {segment}") return seg_path diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 4ebb0701dd44aa..08933d143ffa18 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -38,18 +38,18 @@ segments = [ ("BODY", "regen660D86654BA|2022-07-06--14-27-15--0"), - ("HYUNDAI", "regen657E25856BB|2022-07-06--14-26-51--0"), + ("HYUNDAI", "regen114E5FF24D8|2022-07-14--17-08-47--0"), ("HYUNDAI", "d824e27e8c60172c|2022-07-08--21-21-15--0"), ("TOYOTA", "regenBA97410FBEC|2022-07-06--14-26-49--0"), ("TOYOTA2", "regenDEDB1D9C991|2022-07-06--14-54-08--0"), ("TOYOTA3", "regenDDC1FE60734|2022-07-06--14-32-06--0"), - ("HONDA", "regen17B09D158B8|2022-07-06--14-31-46--0"), - ("HONDA2", "regen041739C3E9A|2022-07-06--15-08-02--0"), - ("CHRYSLER", "regenBB2F9C1425C|2022-07-06--14-31-41--0"), + ("HONDA", "regenE62960EEC38|2022-07-14--19-33-24--0"), + ("HONDA2", "regenC3EBD92F029|2022-07-14--19-29-47--0"), + ("CHRYSLER", "regen38346FB33D0|2022-07-14--18-05-26--0"), ("RAM", "2f4452b03ccb98f0|2022-07-07--08-01-56--3"), - ("SUBARU", "regen732B69F33B1|2022-07-06--14-36-18--0"), + ("SUBARU", "regen54A1E2BE5AA|2022-07-14--18-07-50--0"), ("GM", "regen01D09D915B5|2022-07-06--14-36-20--0"), - ("NISSAN", "regenEA6FB2773F5|2022-07-06--14-58-23--0"), + ("NISSAN", "regenCA0B0DC946E|2022-07-14--18-10-17--0"), ("VOLKSWAGEN", "regen007098CA0EF|2022-07-06--15-01-26--0"), ("MAZDA", "regen61BA413D53B|2022-07-06--14-39-42--0"), ] diff --git a/tools/sim/start_carla.sh b/tools/sim/start_carla.sh index 67ced7eb21dc3f..7ead6699f00fbd 100755 --- a/tools/sim/start_carla.sh +++ b/tools/sim/start_carla.sh @@ -22,6 +22,7 @@ if [[ "$DETACH" ]]; then EXTRA_ARGS="-d" fi +docker kill carla_sim || true docker run \ --name carla_sim \ --rm \ From 5ccc2b89213f37649440984b8b660504a2c7fc29 Mon Sep 17 00:00:00 2001 From: Gijs Koning Date: Fri, 15 Jul 2022 13:18:52 +0200 Subject: [PATCH 352/436] Laikad: improve logging + posfix every 2 sec (#25184) * Improve logging for pos fix and caching * Set to debug --- selfdrive/locationd/laikad.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/selfdrive/locationd/laikad.py b/selfdrive/locationd/laikad.py index 13829b22a96e03..b67c4834979988 100755 --- a/selfdrive/locationd/laikad.py +++ b/selfdrive/locationd/laikad.py @@ -79,8 +79,8 @@ def load_cache(self): cloudlog.exception("Error parsing cache") timestamp = self.last_fetch_orbits_t.as_datetime() if self.last_fetch_orbits_t is not None else 'Nan' cloudlog.debug( - f"Loaded nav and orbits cache with timestamp: {timestamp}. Unique orbit and nav sats: {list(cache['orbits'].keys())} {list(cache['nav'].keys())} " + - f"Total: {sum([len(v) for v in cache['orbits']])} and {sum([len(v) for v in cache['nav']])}") + f"Loaded nav ({sum([len(v) for v in cache['nav']])}) and orbits ({sum([len(v) for v in cache['orbits']])}) cache with timestamp: {timestamp}. Unique orbit and nav sats: {list(cache['orbits'].keys())} {list(cache['nav'].keys())} " + + f"With time range: {[f'{start.as_datetime()}, {end.as_datetime()}' for (start,end) in self.astro_dog.orbit_fetched_times._ranges]}") def cache_ephemeris(self, t: GPSTime): if self.save_ephemeris and (self.last_cached_t is None or t - self.last_cached_t > SECS_IN_MIN): @@ -94,10 +94,15 @@ def get_est_pos(self, t, processed_measurements): if self.last_pos_fix_t is None or abs(self.last_pos_fix_t - t) >= 2: min_measurements = 6 if any(p.constellation_id == ConstellationId.GLONASS for p in processed_measurements) else 5 pos_fix, pos_fix_residual = calc_pos_fix_gauss_newton(processed_measurements, self.posfix_functions, min_measurements=min_measurements) - if len(pos_fix) > 0 and np.median(np.abs(pos_fix_residual)) < POS_FIX_RESIDUAL_THRESHOLD: - self.last_pos_fix = pos_fix[:3] - self.last_pos_residual = pos_fix_residual + if len(pos_fix) > 0: self.last_pos_fix_t = t + residual_median = np.median(np.abs(pos_fix_residual)) + if np.median(np.abs(pos_fix_residual)) < POS_FIX_RESIDUAL_THRESHOLD: + cloudlog.debug(f"Pos fix is within threshold with median: {residual_median.round()}") + self.last_pos_fix = pos_fix[:3] + self.last_pos_residual = pos_fix_residual + else: + cloudlog.debug(f"Pos fix failed with median: {residual_median.round()}. All residuals: {np.round(pos_fix_residual)}") return self.last_pos_fix def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): @@ -115,10 +120,11 @@ def process_ublox_msg(self, ublox_msg, ublox_mono_time: int, block=False): new_meas = [m for m in new_meas if 1e7 < m.observables['C1C'] < 3e7] processed_measurements = process_measurements(new_meas, self.astro_dog) - est_pos = self.get_est_pos(t, processed_measurements) corrected_measurements = correct_measurements(processed_measurements, est_pos, self.astro_dog) if len(est_pos) > 0 else [] + if ublox_mono_time % 10 == 0: + cloudlog.debug(f"Measurements Incoming/Processed/Corrected: {len(new_meas), len(processed_measurements), len(corrected_measurements)}") self.update_localizer(est_pos, t, corrected_measurements) kf_valid = all(self.kf_valid(t)) @@ -212,6 +218,8 @@ def get_orbit_data(t: GPSTime, valid_const, auto_update, valid_ephem_types, cach try: astro_dog.get_orbit_data(t, only_predictions=True) cloudlog.info(f"Done parsing orbits. Took {time.monotonic() - start_time:.1f}s") + cloudlog.debug(f"Downloaded orbits ({sum([len(v) for v in astro_dog.orbits])}): {list(astro_dog.orbits.keys())}" + + f"With time range: {[f'{start.as_datetime()}, {end.as_datetime()}' for (start,end) in astro_dog.orbit_fetched_times._ranges]}") return astro_dog.orbits, astro_dog.orbit_fetched_times, t except (DownloadFailed, RuntimeError, ValueError, IOError) as e: cloudlog.warning(f"No orbit data found or parsing failure: {e}") From 91eb096ac98b46605c283c637ed1e6b15598dc52 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Sat, 16 Jul 2022 01:53:10 +0800 Subject: [PATCH 353/436] replay: add support for reading from plain logs (#25053) * read from plain log * reduce memory copies * cleanup * fix test_replay --- tools/replay/logreader.cc | 22 +++++++++++----------- tools/replay/logreader.h | 2 +- tools/replay/route.cc | 4 ++-- tools/replay/tests/test_replay.cc | 1 + 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/tools/replay/logreader.cc b/tools/replay/logreader.cc index f27224ac53a09b..9b7a07a83fc018 100644 --- a/tools/replay/logreader.cc +++ b/tools/replay/logreader.cc @@ -47,22 +47,22 @@ LogReader::~LogReader() { } bool LogReader::load(const std::string &url, std::atomic *abort, bool local_cache, int chunk_size, int retries) { - FileReader f(local_cache, chunk_size, retries); - std::string data = f.read(url, abort); - if (data.empty()) return false; + raw_ = FileReader(local_cache, chunk_size, retries).read(url, abort); + if (raw_.empty()) return false; - return load((std::byte*)data.data(), data.size(), abort); + if (url.find(".bz2") != std::string::npos) { + raw_ = decompressBZ2(raw_, abort); + if (raw_.empty()) return false; + } + return parse(abort); } bool LogReader::load(const std::byte *data, size_t size, std::atomic *abort) { - raw_ = decompressBZ2(data, size, abort); - if (raw_.empty()) { - if (!(abort && *abort)) { - rWarning("failed to decompress log"); - } - return false; - } + raw_.assign((const char *)data, size); + return parse(abort); +} +bool LogReader::parse(std::atomic *abort) { try { kj::ArrayPtr words((const capnp::word *)raw_.data(), raw_.size() / sizeof(capnp::word)); while (words.size() > 0 && !(abort && *abort)) { diff --git a/tools/replay/logreader.h b/tools/replay/logreader.h index fb63bf39136e3b..bd666d0a74b20d 100644 --- a/tools/replay/logreader.h +++ b/tools/replay/logreader.h @@ -52,10 +52,10 @@ class LogReader { ~LogReader(); bool load(const std::string &url, std::atomic *abort = nullptr, bool local_cache = false, int chunk_size = -1, int retries = 0); bool load(const std::byte *data, size_t size, std::atomic *abort = nullptr); - std::vector events; private: + bool parse(std::atomic *abort); std::string raw_; #ifdef HAS_MEMORY_RESOURCE std::pmr::monotonic_buffer_resource *mbr_ = nullptr; diff --git a/tools/replay/route.cc b/tools/replay/route.cc index 5b47090229749d..c91b27ae8100bb 100644 --- a/tools/replay/route.cc +++ b/tools/replay/route.cc @@ -82,9 +82,9 @@ void Route::addFileToSegment(int n, const QString &file) { const int pos = name.lastIndexOf("--"); name = pos != -1 ? name.mid(pos + 2) : name; - if (name == "rlog.bz2") { + if (name == "rlog.bz2" || name == "rlog") { segments_[n].rlog = file; - } else if (name == "qlog.bz2") { + } else if (name == "qlog.bz2" || name == "qlog") { segments_[n].qlog = file; } else if (name == "fcamera.hevc") { segments_[n].road_cam = file; diff --git a/tools/replay/tests/test_replay.cc b/tools/replay/tests/test_replay.cc index bd5dee013c8af6..d6482c3ca2def9 100644 --- a/tools/replay/tests/test_replay.cc +++ b/tools/replay/tests/test_replay.cc @@ -71,6 +71,7 @@ TEST_CASE("LogReader") { FileReader reader(true); std::string corrupt_content = reader.read(TEST_RLOG_URL); corrupt_content.resize(corrupt_content.length() / 2); + corrupt_content = decompressBZ2(corrupt_content); LogReader log; REQUIRE(log.load((std::byte *)corrupt_content.data(), corrupt_content.size())); REQUIRE(log.events.size() > 0); From 98a0cd9455ad4ae02145d0653c0dabdfab0b3f0f Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Fri, 15 Jul 2022 19:42:13 +0100 Subject: [PATCH 354/436] Ford: FPv2 firmware request (#24211) --- selfdrive/car/fw_versions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 5a33cdf6b7e965..c7256e74380c4e 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -99,6 +99,11 @@ def p16(val): CHRYSLER_RX_OFFSET = -0x280 +FORD_VERSION_REQUEST = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + \ + p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER) +FORD_VERSION_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + \ + p16(uds.DATA_IDENTIFIER_TYPE.VEHICLE_MANUFACTURER_ECU_SOFTWARE_NUMBER) + @dataclass class Request: @@ -207,6 +212,13 @@ class Request: [CHRYSLER_VERSION_REQUEST], [CHRYSLER_VERSION_RESPONSE], ), + # Ford + Request( + "ford", + [TESTER_PRESENT_REQUEST, FORD_VERSION_REQUEST], + [TESTER_PRESENT_RESPONSE, FORD_VERSION_RESPONSE], + bus=0, + ), ] From 93cd0285ac014427cdd137fa2c8852c58bdef4db Mon Sep 17 00:00:00 2001 From: Greg Hogan Date: Mon, 18 Jul 2022 04:57:21 -0700 Subject: [PATCH 355/436] nav: toggle to show on left side of onroad ui (#25169) * nav: show on left side of onroad ui * add toggle * capitalization * update translations * single params object * skip unfinished translations test Co-authored-by: Willem Melching --- common/params.cc | 1 + selfdrive/ui/qt/offroad/settings.cc | 6 ++ selfdrive/ui/qt/onroad.cc | 8 +- selfdrive/ui/tests/test_translations.py | 1 + selfdrive/ui/translations/main_ko.ts | 132 ++++++++++++----------- selfdrive/ui/translations/main_zh-CHS.ts | 132 ++++++++++++----------- selfdrive/ui/translations/main_zh-CHT.ts | 132 ++++++++++++----------- selfdrive/ui/ui.cc | 4 +- selfdrive/ui/ui.h | 2 +- 9 files changed, 232 insertions(+), 186 deletions(-) diff --git a/common/params.cc b/common/params.cc index c4f65a9e02512a..77be6f36b18c76 100644 --- a/common/params.cc +++ b/common/params.cc @@ -140,6 +140,7 @@ std::unordered_map keys = { {"LiveParameters", PERSISTENT}, {"NavDestination", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"NavSettingTime24h", PERSISTENT}, + {"NavSettingLeftSide", PERSISTENT}, {"NavdRender", PERSISTENT}, {"OpenpilotEnabledToggle", PERSISTENT}, {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 9a6e2039661a7b..7b6be919e033d7 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -71,6 +71,12 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { tr("Use 24h format instead of am/pm"), "../assets/offroad/icon_metric.png", }, + { + "NavSettingLeftSide", + tr("Show Map on Left Side of UI"), + tr("Show map on left side when in split screen view."), + "../assets/offroad/icon_road.png", + }, #endif }; diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index ca39a89ae45740..fe39cd0cfc579c 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -52,6 +52,12 @@ void OnroadWindow::updateState(const UIState &s) { alerts->updateAlert(alert, bgColor); } + if (s.scene.map_on_left) { + split->setDirection(QBoxLayout::LeftToRight); + } else { + split->setDirection(QBoxLayout::RightToLeft); + } + nvg->updateState(s); if (bg != bgColor) { @@ -80,7 +86,7 @@ void OnroadWindow::offroadTransition(bool offroad) { QObject::connect(uiState(), &UIState::offroadTransition, m, &MapWindow::offroadTransition); m->setFixedWidth(topWidget(this)->width() / 2); - split->addWidget(m, 0, Qt::AlignRight); + split->insertWidget(0, m); // Make map visible after adding to split m->offroadTransition(offroad); diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 3230a995437f1b..d5409dd4162908 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -61,6 +61,7 @@ def test_translations_updated(self): self.assertEqual(cur_translations, new_translations, f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py --release to update the translation files") + @unittest.skip("Only test unfinished translations before going to release") def test_unfinished_translations(self): for name, file in self.translation_files.items(): with self.subTest(name=name, file=file): diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 07d68774636958..d737a9ce3367fa 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A N/A - + Serial Serial - + Driver Camera 운전자 카메라 - + PREVIEW 미리보기 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 운전자 모니터링이 좋은 가시성을 갖도록 운전자를 향한 카메라를 미리 봅니다. (차량연결은 해제되어있어야 합니다) - + Reset Calibration 캘리브레이션 재설정 - + RESET 재설정 - + Are you sure you want to reset calibration? 캘리브레이션을 재설정하시겠습니까? - + Review Training Guide 트레이닝 가이드 다시보기 - + REVIEW 다시보기 - + Review the rules, features, and limitations of openpilot openpilot의 규칙, 기능 및 제한 다시보기 - + Are you sure you want to review the training guide? 트레이닝 가이드를 다시보시겠습니까? - + Regulatory 규제 - + VIEW 보기 - + Change Language 언어 변경 - + CHANGE 변경 - + Select a language 언어를 선택하세요 - + Reboot 재부팅 - + Power Off 전원 종료 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot은 장치를 좌측 또는 우측은 4° 이내, 위쪽 5° 또는 아래쪽은 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. - + Your device is pointed %1° %2 and %3° %4. 사용자의 장치가 %1° %2 및 %3° %4를 가리키고 있습니다. - + down 아래로 - + up 위로 - + left 좌측으로 - + right 우측으로 - + Are you sure you want to reboot? 재부팅 하시겠습니까? - + Disengage to Reboot 재부팅 하려면 해제하세요 - + Are you sure you want to power off? 전원을 종료하시겠습니까? - + Disengage to Power Off 전원을 종료하려면 해제하세요 @@ -488,30 +488,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX MAX - - + + SPEED SPEED - - + + LIMIT LIMIT @@ -710,33 +710,33 @@ location set SettingsWindow - + × × - + Device 장치 - - + + Network 네트워크 - + Toggles 토글 - + Software 소프트웨어 - + Navigation 네비게이션 @@ -975,68 +975,68 @@ location set SoftwarePanel - + Git Branch Git 브렌치 - + Git Commit Git 커밋 - + OS Version OS 버전 - + Version 버전 - + Last Update Check 최신 업데이트 검사 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 최근에 openpilot이 업데이트를 성공적으로 확인했습니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. - + Check for Update 업데이트 확인 - + CHECKING 확인중 - + UNINSTALL 제거 - + Uninstall %1 제거 %1 - + Are you sure you want to uninstall? 제거하시겠습니까? - + failed to fetch update 업데이트를 가져올수없습니다 - - + + CHECK 확인 @@ -1194,12 +1194,22 @@ location set 오전/오후 대신 24시간 형식 사용 - + + Show Map on Left Side of UI + + + + + Show map on left side when in split screen view. + + + + openpilot Longitudinal Control openpilot Longitudinal Control - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot은 차량'의 레이더를 무력화시키고 가속페달과 브레이크의 제어를 인계받을 것이다. 경고: AEB를 비활성화합니다! diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 424488fc56a422..682d1a05cf5c93 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID 设备ID(Dongle ID) - + N/A N/A - + Serial 序列号 - + Driver Camera 驾驶员摄像头 - + PREVIEW 预览 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。仅熄火时可用。 - + Reset Calibration 重置设备校准 - + RESET 重置 - + Are you sure you want to reset calibration? 您确定要重置设备校准吗? - + Review Training Guide 新手指南 - + REVIEW 查看 - + Review the rules, features, and limitations of openpilot 查看openpilot的使用规则,以及其功能和限制。 - + Are you sure you want to review the training guide? 您确定要查看新手指南吗? - + Regulatory 监管信息 - + VIEW 查看 - + Change Language 切换语言 - + CHANGE 切换 - + Select a language 选择语言 - + Reboot 重启 - + Power Off 关机 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下8°之间。一般来说,openpilot会持续更新校准,很少需要重置。 - + Your device is pointed %1° %2 and %3° %4. 您的设备校准为%1° %2、%3° %4。 - + down 朝下 - + up 朝上 - + left 朝左 - + right 朝右 - + Are you sure you want to reboot? 您确定要重新启动吗? - + Disengage to Reboot 取消openpilot以重新启动 - + Are you sure you want to power off? 您确定要关机吗? - + Disengage to Power Off 取消openpilot以关机 @@ -486,30 +486,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX 最高定速 - - + + SPEED SPEED - - + + LIMIT LIMIT @@ -708,33 +708,33 @@ location set SettingsWindow - + × × - + Device 设备 - - + + Network 网络 - + Toggles 设定 - + Software 软件 - + Navigation 导航 @@ -973,68 +973,68 @@ location set SoftwarePanel - + Git Branch Git Branch - + Git Commit Git Commit - + OS Version 系统版本 - + Version 软件版本 - + Last Update Check 上次检查更新 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上一次成功检查更新的时间。更新程序仅在汽车熄火时运行。 - + Check for Update 检查更新 - + CHECKING 正在检查更新 - + UNINSTALL 卸载 - + Uninstall %1 卸载 %1 - + Are you sure you want to uninstall? 您确定要卸载吗? - + failed to fetch update 获取更新失败 - - + + CHECK 查看 @@ -1192,12 +1192,22 @@ location set 使用24小时制代替am/pm - + + Show Map on Left Side of UI + + + + + Show map on left side when in split screen view. + + + + openpilot Longitudinal Control openpilot纵向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot将禁用车辆的雷达并接管油门和刹车的控制。警告:AEB将被禁用! diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 1eab75a6b4ca2f..c04a3053552108 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A 無法使用 - + Serial 序號 - + Driver Camera 駕駛員攝像頭 - + PREVIEW 預覽 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。僅在熄火時可用。 - + Reset Calibration 重置校準 - + RESET 重置 - + Are you sure you want to reset calibration? 您確定要重置校準嗎? - + Review Training Guide 觀看使用教學 - + REVIEW 觀看 - + Review the rules, features, and limitations of openpilot 觀看 openpilot 的使用規則、功能和限制 - + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? - + Regulatory 法規/監管 - + VIEW 觀看 - + Change Language 更改語言 - + CHANGE 更改 - + Select a language 選擇語言 - + Reboot 重新啟動 - + Power Off 關機 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以内或朝下偏差 8° 以内。鏡頭在後台會持續自動校準,很少有需要重置的情况。 - + Your device is pointed %1° %2 and %3° %4. 你的設備目前朝%2 %1° 以及朝%4 %3° 。 - + down - + up - + left - + right - + Are you sure you want to reboot? 您確定要重新啟動嗎? - + Disengage to Reboot 請先取消控車才能重新啟動 - + Are you sure you want to power off? 您確定您要關機嗎? - + Disengage to Power Off 請先取消控車才能關機 @@ -488,30 +488,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX 最高 - - + + SPEED 速度 - - + + LIMIT 速限 @@ -713,33 +713,33 @@ location set SettingsWindow - + × × - + Device 設備 - - + + Network 網路 - + Toggles 設定 - + Software 軟體 - + Navigation 導航 @@ -978,68 +978,68 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 系統版本 - + Version 版本 - + Last Update Check 上次檢查時間 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 - + Check for Update 檢查更新 - + CHECKING 檢查中 - + UNINSTALL 卸載 - + Uninstall %1 卸載 %1 - + Are you sure you want to uninstall? 您確定您要卸載嗎? - + failed to fetch update 下載更新失敗 - - + + CHECK 檢查 @@ -1197,12 +1197,22 @@ location set 使用 24 小時制。(預設值為 12 小時制) - + + Show Map on Left Side of UI + + + + + Show map on left side when in split screen view. + + + + openpilot Longitudinal Control openpilot 縱向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot 將會關閉雷達訊號並接管油門和剎車的控制。注意:這也會關閉自動緊急煞車 (AEB) 系統! diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 7922714c17d3c6..317ef497a52f10 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -192,7 +192,9 @@ static void update_state(UIState *s) { } void ui_update_params(UIState *s) { - s->scene.is_metric = Params().getBool("IsMetric"); + auto params = Params(); + s->scene.is_metric = params.getBool("IsMetric"); + s->scene.map_on_left = params.getBool("NavSettingLeftSide"); } void UIState::updateStatus() { diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 1aee3df9a13fc1..16f78cdef416de 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -101,7 +101,7 @@ typedef struct UIScene { QPointF lead_vertices[2]; float light_sensor, accel_sensor, gyro_sensor; - bool started, ignition, is_metric, longitudinal_control; + bool started, ignition, is_metric, map_on_left, longitudinal_control; uint64_t started_frame; } UIScene; From 2a5da48a7cd39d5289fbc933d4b89fd75fd453fa Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 18 Jul 2022 15:07:56 -0700 Subject: [PATCH 356/436] 2021 pacifica is supported --- docs/CARS.md | 3 ++- selfdrive/car/chrysler/values.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index cf5853ff4c427a..78044d9fb5c04e 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -153,7 +153,7 @@ How We Rate The Cars |Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance|||||| |Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance|||||| -# Bronze - 83 cars +# Bronze - 84 cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| |---|---|---|:---:|:---:|:---:|:---:|:---:| @@ -165,6 +165,7 @@ How We Rate The Cars |Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| |Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica 2019-20|Adaptive Cruise|||||| +|Chrysler|Pacifica 2021|All|||||| |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| |Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| |Genesis|G90 2017-18|All|||||| diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 80baba9bd68a5d..5a979776ec569c 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -14,7 +14,7 @@ class CAR: PACIFICA_2017_HYBRID = "CHRYSLER PACIFICA HYBRID 2017" PACIFICA_2018_HYBRID = "CHRYSLER PACIFICA HYBRID 2018" PACIFICA_2019_HYBRID = "CHRYSLER PACIFICA HYBRID 2019" - PACIFICA_2018 = "CHRYSLER PACIFICA 2018" # includes 2017 Pacifica + PACIFICA_2018 = "CHRYSLER PACIFICA 2018" PACIFICA_2020 = "CHRYSLER PACIFICA 2020" # Jeep @@ -51,7 +51,10 @@ class ChryslerCarInfo(CarInfo): CAR.PACIFICA_2018_HYBRID: None, # same platforms CAR.PACIFICA_2019_HYBRID: ChryslerCarInfo("Chrysler Pacifica Hybrid 2019-22"), CAR.PACIFICA_2018: ChryslerCarInfo("Chrysler Pacifica 2017-18"), - CAR.PACIFICA_2020: ChryslerCarInfo("Chrysler Pacifica 2019-20"), + CAR.PACIFICA_2020: [ + ChryslerCarInfo("Chrysler Pacifica 2019-20"), + ChryslerCarInfo("Chrysler Pacifica 2021", package="All"), + ], CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-22"), From ee081f278b090b5bd4a06f12749fffeb780c6162 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 17:05:16 -0700 Subject: [PATCH 357/436] FPv2: fingerprint on all FW combinations (#25204) * Try to fingerprint on all possible combinations * build_fw_dict creates set dict, tries each fw version for addr, subaddr * clean up * static analysis * comment * fix comment * revert changes to test_fw_query_on_routes * remove comment --- selfdrive/car/fw_versions.py | 52 +++++++++++----------- selfdrive/debug/test_fw_query_on_routes.py | 21 ++------- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index c7256e74380c4e..9db03390b27362 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -228,13 +228,13 @@ def chunks(l, n=128): def build_fw_dict(fw_versions, filter_brand=None): - fw_versions_dict = {} + fw_versions_dict = defaultdict(set) for fw in fw_versions: if filter_brand is None or fw.brand == filter_brand: addr = fw.address sub_addr = fw.subAddress if fw.subAddress != 0 else None - fw_versions_dict[(addr, sub_addr)] = fw.fwVersion - return fw_versions_dict + fw_versions_dict[(addr, sub_addr)].add(fw.fwVersion) + return dict(fw_versions_dict) def get_brand_addrs(): @@ -271,17 +271,18 @@ def match_fw_to_car_fuzzy(fw_versions_dict, log=True, exclude=None): match_count = 0 candidate = None - for addr, version in fw_versions_dict.items(): - # All cars that have this FW response on the specified address - candidates = all_fw_versions[(addr[0], addr[1], version)] - - if len(candidates) == 1: - match_count += 1 - if candidate is None: - candidate = candidates[0] - # We uniquely matched two different cars. No fuzzy match possible - elif candidate != candidates[0]: - return set() + for addr, versions in fw_versions_dict.items(): + for version in versions: + # All cars that have this FW response on the specified address + candidates = all_fw_versions[(addr[0], addr[1], version)] + + if len(candidates) == 1: + match_count += 1 + if candidate is None: + candidate = candidates[0] + # We uniquely matched two different cars. No fuzzy match possible + elif candidate != candidates[0]: + return set() if match_count >= 2: if log: @@ -303,23 +304,23 @@ def match_fw_to_car_exact(fw_versions_dict): for ecu, expected_versions in fws.items(): ecu_type = ecu[0] addr = ecu[1:] - found_version = fw_versions_dict.get(addr, None) - if ecu_type == Ecu.esp and candidate in (TOYOTA.RAV4, TOYOTA.COROLLA, TOYOTA.HIGHLANDER, TOYOTA.SIENNA, TOYOTA.LEXUS_IS) and found_version is None: + found_versions = fw_versions_dict.get(addr, set()) + if ecu_type == Ecu.esp and candidate in (TOYOTA.RAV4, TOYOTA.COROLLA, TOYOTA.HIGHLANDER, TOYOTA.SIENNA, TOYOTA.LEXUS_IS) and not len(found_versions): continue # On some Toyota models, the engine can show on two different addresses - if ecu_type == Ecu.engine and candidate in (TOYOTA.CAMRY, TOYOTA.COROLLA_TSS2, TOYOTA.CHR, TOYOTA.LEXUS_IS) and found_version is None: + if ecu_type == Ecu.engine and candidate in (TOYOTA.CAMRY, TOYOTA.COROLLA_TSS2, TOYOTA.CHR, TOYOTA.LEXUS_IS) and not len(found_versions): continue # Ignore non essential ecus - if ecu_type not in ESSENTIAL_ECUS and found_version is None: + if ecu_type not in ESSENTIAL_ECUS and not len(found_versions): continue # Virtual debug ecu doesn't need to match the database if ecu_type == Ecu.debug: continue - if found_version not in expected_versions: + if not any([found_version in expected_versions for found_version in found_versions]): invalid.append(candidate) break @@ -327,19 +328,16 @@ def match_fw_to_car_exact(fw_versions_dict): def match_fw_to_car(fw_versions, allow_fuzzy=True): - versions = get_interface_attr('FW_VERSIONS', ignore_none=True) - # Try exact matching first exact_matches = [(True, match_fw_to_car_exact)] if allow_fuzzy: exact_matches.append((False, match_fw_to_car_fuzzy)) for exact_match, match_func in exact_matches: - # For each brand, attempt to fingerprint using FW returned from its queries + # TODO: For each brand, attempt to fingerprint using only FW returned from its queries matches = set() - for brand in versions.keys(): - fw_versions_dict = build_fw_dict(fw_versions, filter_brand=brand) - matches |= match_func(fw_versions_dict) + fw_versions_dict = build_fw_dict(fw_versions, filter_brand=None) + matches |= match_func(fw_versions_dict) if len(matches): return exact_match, matches @@ -408,7 +406,9 @@ def get_fw_versions_ordered(logcan, sendcan, ecu_rx_addrs, timeout=0.1, debug=Fa for brand in sorted(brand_matches, key=lambda b: len(brand_matches[b]), reverse=True): car_fw = get_fw_versions(logcan, sendcan, query_brand=brand, timeout=timeout, debug=debug, progress=progress) all_car_fw.extend(car_fw) - matches = match_fw_to_car_exact(build_fw_dict(car_fw)) + + # TODO: Until erroneous FW versions are removed, try to fingerprint on all possible combinations so far + _, matches = match_fw_to_car(all_car_fw, allow_fuzzy=False) if len(matches) == 1: break diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 9ce0ebb3f59ca4..191411f45a77ae 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -89,24 +89,9 @@ print("not in supported cars") break - # Older routes only have carFw from their brand - old_route = not any([len(fw.brand) for fw in car_fw]) - brands = SUPPORTED_BRANDS if not old_route else [None] - - # Exact match - exact_matches, fuzzy_matches = [], [] - for brand in brands: - fw_versions_dict = build_fw_dict(car_fw, filter_brand=brand) - exact_matches = match_fw_to_car_exact(fw_versions_dict) - if len(exact_matches) == 1: - break - - # Fuzzy match - for brand in brands: - fw_versions_dict = build_fw_dict(car_fw, filter_brand=brand) - fuzzy_matches = match_fw_to_car_fuzzy(fw_versions_dict) - if len(fuzzy_matches) == 1: - break + fw_versions_dict = build_fw_dict(car_fw) + exact_matches = match_fw_to_car_exact(fw_versions_dict) + fuzzy_matches = match_fw_to_car_fuzzy(fw_versions_dict) if (len(exact_matches) == 1) and (list(exact_matches)[0] == live_fingerprint): good_exact += 1 From 54e168fce2b98d573ad5924a31e7014dec1656cd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 17:19:33 -0700 Subject: [PATCH 358/436] Honda Civic 2022: add missing FW versions (#25206) Add missing 2022 Civic FW versions --- selfdrive/car/honda/values.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index c665b1cd03e7be..3ce467e2989956 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -1407,6 +1407,7 @@ class HondaCarInfo(CarInfo): b'38897-T20-A510\x00\x00', b'38897-T21-A010\x00\x00', b'38897-T20-A210\x00\x00', + b'38897-T20-A310\x00\x00', ], (Ecu.srs, 0x18DA53F1, None): [ b'77959-T20-A970\x00\x00', @@ -1417,6 +1418,7 @@ class HondaCarInfo(CarInfo): b'78108-T21-A620\x00\x00', b'78108-T23-A110\x00\x00', b'78108-T21-A230\x00\x00', + b'78108-T22-A020\x00\x00', ], (Ecu.vsa, 0x18DA28F1, None): [ b'57114-T20-AB40\x00\x00', @@ -1426,12 +1428,14 @@ class HondaCarInfo(CarInfo): b'28101-65D-A020\x00\x00', b'28101-65D-A120\x00\x00', b'28101-65H-A020\x00\x00', + b'28101-65H-A120\x00\x00', ], (Ecu.programmedFuelInjection, 0x18da10f1, None): [ b'37805-64L-A540\x00\x00', b'37805-64S-A540\x00\x00', b'37805-64S-A720\x00\x00', b'37805-64A-A540\x00\x00', + b'37805-64A-A620\x00\x00', ], }, } From a80324528942e2df1a6f27df65583976c0b1bdbc Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Tue, 19 Jul 2022 01:29:16 +0100 Subject: [PATCH 359/436] Ford FPv2: send engine FW request to OBD port (#25202) * ford fw request on OBD port too * Ford: whitelist fw query for ecu type --- selfdrive/car/fw_versions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 9db03390b27362..ee2c0f31d45114 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -213,11 +213,18 @@ class Request: [CHRYSLER_VERSION_RESPONSE], ), # Ford + Request( + "ford", + [TESTER_PRESENT_REQUEST, FORD_VERSION_REQUEST], + [TESTER_PRESENT_RESPONSE, FORD_VERSION_RESPONSE], + whitelist_ecus=[Ecu.engine], + ), Request( "ford", [TESTER_PRESENT_REQUEST, FORD_VERSION_REQUEST], [TESTER_PRESENT_RESPONSE, FORD_VERSION_RESPONSE], bus=0, + whitelist_ecus=[Ecu.eps, Ecu.esp, Ecu.fwdRadar, Ecu.fwdCamera], ), ] From 500d16622c2c2a60d11539f20f7dda5205bb0915 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 18 Jul 2022 17:53:00 -0700 Subject: [PATCH 360/436] simplify compatibility docs (#25190) * simplify tiers * little more * cleanup * fix test --- docs/CARS.md | 422 ++++++++++++++---------------- selfdrive/car/CARS_template.md | 16 +- selfdrive/car/docs.py | 31 +-- selfdrive/car/docs_definitions.py | 48 ++-- selfdrive/car/tests/test_docs.py | 8 +- 5 files changed, 239 insertions(+), 286 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 78044d9fb5c04e..0fee4ddc840e2d 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -2,20 +2,9 @@ A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. -Cars are organized into three tiers: - -- Gold - The best openpilot experience. Great highway driving and beyond. -- Silver - A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future. -- Bronze - A good highway experience, but may have limited performance in traffic and on sharp turns. - How We Rate The Cars --- -### openpilot Adaptive Cruise Control (ACC) -- - openpilot is able to control the gas and brakes. -- - openpilot is able to control the gas and brakes with some restrictions. -- - The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system. - ### Stop and Go - - Adaptive Cruise Control (ACC) operates down to 0 mph. - - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed. @@ -25,223 +14,206 @@ How We Rate The Cars - - No steering control below certain speeds. ### Steering Torque -- - Car has enough steering torque to take tighter turns. -- - Car has enough steering torque for comfortable highway driving. +- - Car has enough steering torque to take tight turns. - - Limited ability to make turns. -### Actively Maintained -- - Mainline software support, harness hardware sold by comma, lots of users, primary development target. -- - Low user count, community maintained, harness hardware not sold by comma. - -**All supported cars can move between the tiers as support changes.** - -# Gold - 30 cars - -|Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| -|---|---|---|:---:|:---:|:---:|:---:|:---:| -|comma|body|All|||||| -|Genesis|G70 2020|All|||||| -|Hyundai|Palisade 2020-21|All|||||| -|Hyundai|Santa Fe 2019-20|All|||||| -|Hyundai|Sonata 2020-22|All|||||| -|Hyundai|Sonata Hybrid 2020-22|All|||||| -|Kia|Niro Electric 2019-20|All|||||| -|Kia|Niro Electric 2021|All|||||| -|Kia|Niro Electric 2022|All|||||| -|Kia|Telluride 2020|SCC + LKAS|||||| -|Lexus|ES 2019-22|All|||||| -|Lexus|ES Hybrid 2019-22|All|||||| -|Lexus|NX 2020-21|All|||||| -|Lexus|NX Hybrid 2020-21|All|||||| -|Lexus|RX 2020-22|All|||||| -|Lexus|UX Hybrid 2019-22|All|||||| -|Toyota|Avalon 2022|All|||||| -|Toyota|Avalon Hybrid 2022|All|||||| -|Toyota|Camry 2021-22|All||[4](#footnotes)|||| -|Toyota|Camry Hybrid 2021-22|All|||||| -|Toyota|Corolla 2020-22|All|||||| -|Toyota|Corolla Hatchback 2019-22|All|||||| -|Toyota|Corolla Hybrid 2020-22|All|||||| -|Toyota|Highlander 2020-22|All|||||| -|Toyota|Highlander Hybrid 2020-22|All|||||| -|Toyota|Mirai 2021|All|||||| -|Toyota|Prius 2021-22|All|||||| -|Toyota|Prius Prime 2021-22|All|||||| -|Toyota|RAV4 2019-21|All|||||| -|Toyota|RAV4 Hybrid 2019-21|All|||||| - -# Silver - 78 cars - -|Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| -|---|---|---|:---:|:---:|:---:|:---:|:---:| -|Audi|A3 2014-19|ACC + Lane Assist|||||| -|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|||||| -|Audi|RS3 2018|ACC + Lane Assist|||||| -|Audi|S3 2015-17|ACC + Lane Assist|||||| -|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|||||| -|Genesis|G70 2018-19|All|||||| -|Genesis|G80 2017-19|All|||||| -|Hyundai|Elantra 2021-22|SCC + LKAS|||||| -|Hyundai|Elantra Hybrid 2021-22|SCC + LKAS|||||| -|Hyundai|Ioniq Electric 2020|SCC + LKAS|||||| -|Hyundai|Ioniq Hybrid 2020-22|SCC + LFA|||||| -|Hyundai|Ioniq Plug-in Hybrid 2020-21|SCC + LKAS|||||| -|Hyundai|Kona 2020|SCC + LKAS|||||| -|Hyundai|Kona Electric 2018-21|SCC + LKAS|||||| -|Hyundai|Kona Hybrid 2020|SCC + LKAS|||||| -|Hyundai|Santa Fe 2021-22|All|||||| -|Hyundai|Santa Fe Hybrid 2022|All|||||| -|Hyundai|Santa Fe Plug-in Hybrid 2022|All|||||| -|Hyundai|Tucson Diesel 2019|SCC + LKAS|||||| -|Kia|Ceed 2019|SCC + LKAS|||||| -|Kia|EV6 2022|All|||||| -|Kia|Forte 2018|SCC + LKAS|||||| -|Kia|Forte 2019-21|SCC + LKAS|||||| -|Kia|K5 2021-22|SCC|||||| -|Kia|Niro Hybrid 2021|SCC + LKAS|||||| -|Kia|Niro Hybrid 2022|SCC + LKAS|||||| -|Kia|Optima 2019|SCC + LKAS|||||| -|Kia|Seltos 2021|SCC + LKAS|||||| -|Kia|Sorento 2018|SCC + LKAS|||||| -|Kia|Sorento 2019|SCC + LKAS|||||| -|Kia|Stinger 2018-20|SCC + LKAS|||||| -|Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)||||| -|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)||||| -|Lexus|NX 2018-19|All|[3](#footnotes)||||| -|Lexus|NX Hybrid 2018-19|All|[3](#footnotes)||||| -|Lexus|RX Hybrid 2020-21|All|||||| -|Nissan|Altima 2019-20|ProPILOT|||||| -|Nissan|Leaf 2018-22|ProPILOT|||||| -|Nissan|Rogue 2018-20|ProPILOT|||||| -|Nissan|X-Trail 2017|ProPILOT|||||| -|SEAT|Ateca 2018|Driver Assistance|||||| -|SEAT|Leon 2014-20|Driver Assistance|||||| -|Subaru|Ascent 2019-21|All|||||| -|Subaru|Crosstrek 2020-21|EyeSight|||||| -|Subaru|Forester 2019-22|All|||||| -|Subaru|Impreza 2020-22|EyeSight|||||| -|Subaru|XV 2020-21|EyeSight|||||| -|Toyota|Alphard 2019-20|All|||||| -|Toyota|Alphard Hybrid 2021|All|||||| -|Toyota|Camry 2018-20|All||[4](#footnotes)|||| -|Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)|||| -|Toyota|Corolla Cross 2020-21 (Non-US only)|All|||||| -|Toyota|Corolla Cross Hybrid 2020-22 (Non-US only)|All|||||| -|Toyota|Highlander 2017-19|All|[3](#footnotes)||||| -|Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)||||| -|Toyota|Prius 2016-20|TSS-P|[3](#footnotes)||||| -|Toyota|Prius Prime 2017-20|All|[3](#footnotes)||||| -|Toyota|RAV4 2022|All|||||| -|Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|RAV4 Hybrid 2022|All|||||| -|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|e-Golf 2014-20|Driver Assistance|||||| -|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|Golf Alltrack 2015-19|Driver Assistance|||||| -|Volkswagen|Golf GTD 2015-20|Driver Assistance|||||| -|Volkswagen|Golf GTE 2015-20|Driver Assistance|||||| -|Volkswagen|Golf GTI 2015-21|Driver Assistance|||||| -|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance|||||| -|Volkswagen|Golf SportsVan 2015-20|Driver Assistance|||||| -|Volkswagen|Passat 2015-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Passat GTE 2015-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance|||||| - -# Bronze - 84 cars - -|Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque|Actively Maintained| -|---|---|---|:---:|:---:|:---:|:---:|:---:| -|Acura|ILX 2016-19|AcuraWatch Plus|||||| -|Acura|RDX 2016-18|AcuraWatch Plus|||||| -|Acura|RDX 2019-22|All|||||| -|Audi|Q2 2018|ACC + Lane Assist|||||| -|Audi|Q3 2020-21|ACC + Lane Assist|||||| -|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|||||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica 2019-20|Adaptive Cruise|||||| -|Chrysler|Pacifica 2021|All|||||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|||||| -|Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|||||| -|Genesis|G90 2017-18|All|||||| -|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|||||| -|Honda|Accord 2018-22|All|||||| -|Honda|Accord Hybrid 2018-22|All|||||| -|Honda|Civic 2016-18|Honda Sensing|||||| -|Honda|Civic 2019-21|All|||[2](#footnotes)||| -|Honda|Civic 2022|All|||||| -|Honda|Civic Hatchback 2017-21|Honda Sensing|||||| -|Honda|Civic Hatchback 2022|All|||||| -|Honda|CR-V 2015-16|Touring|||||| -|Honda|CR-V 2017-22|Honda Sensing|||||| -|Honda|CR-V Hybrid 2017-19|Honda Sensing|||||| -|Honda|e 2020|All|||||| -|Honda|Fit 2018-20|Honda Sensing|||||| -|Honda|Freed 2020|Honda Sensing|||||| -|Honda|HR-V 2019-22|Honda Sensing|||||| -|Honda|Insight 2019-22|All|||||| -|Honda|Inspire 2018|All|||||| -|Honda|Odyssey 2018-22|Honda Sensing|||||| -|Honda|Passport 2019-21|All|||||| -|Honda|Pilot 2016-22|Honda Sensing|||||| -|Honda|Ridgeline 2017-22|Honda Sensing|||||| -|Hyundai|Elantra 2017-19|SCC + LKAS|||||| -|Hyundai|Genesis 2015-16|SCC + LKAS|||||| -|Hyundai|Ioniq Electric 2019|SCC + LKAS|||||| -|Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|||||| -|Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|||||| -|Hyundai|Sonata 2018-19|SCC + LKAS|||||| -|Hyundai|Tucson 2021|SCC + LKAS|||||| -|Hyundai|Veloster 2019-20|SCC + LKAS|||||| -|Jeep|Grand Cherokee 2016-18|Adaptive Cruise|||||| -|Jeep|Grand Cherokee 2019-21|Adaptive Cruise|||||| -|Kia|Niro Plug-in Hybrid 2019|SCC + LKAS|||||| -|Kia|Optima 2017|SCC + LKAS|||||| -|Lexus|IS 2017-19|All|||||| -|Lexus|RC 2017-2020|All|||||| -|Lexus|RX 2016-18|All|[3](#footnotes)||||| -|Lexus|RX Hybrid 2016-19|All|[3](#footnotes)||||| -|Mazda|CX-5 2022|All|||||| -|Mazda|CX-9 2021-22|All|||||| -|Ram|1500 2019-22|Adaptive Cruise|||||| -|Subaru|Crosstrek 2018-19|EyeSight|||||| -|Subaru|Impreza 2017-19|EyeSight|||||| -|Subaru|XV 2018-19|EyeSight|||||| -|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|||||| -|Škoda|Karoq 2019|Driver Assistance|||||| -|Škoda|Kodiaq 2018-19|Driver Assistance|||||| -|Škoda|Octavia 2015, 2018-19|Driver Assistance|||||| -|Škoda|Octavia RS 2016|Driver Assistance|||||| -|Škoda|Scala 2020|Driver Assistance|||||| -|Škoda|Superb 2015-18|Driver Assistance|||||| -|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)||||| -|Toyota|C-HR 2017-21|All|||||| -|Toyota|C-HR Hybrid 2017-19|All|||||| -|Toyota|Corolla 2017-19|All|[3](#footnotes)||||| -|Toyota|Prius v 2017|TSS-P|[3](#footnotes)||||| -|Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)||||| -|Toyota|Sienna 2018-20|All|[3](#footnotes)||||| -|Volkswagen|Arteon 2018-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Arteon eHybrid 2020-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Arteon R 2020-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|CC 2018-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|||||| -|Volkswagen|Touran 2017|Driver Assistance|||||| +# 192 Supported Cars + +|Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque| +|---|---|---|:---:|:---:|:---:|:---:| +|Acura|ILX 2016-19|AcuraWatch Plus||||| +|Acura|RDX 2016-18|AcuraWatch Plus||||| +|Acura|RDX 2019-22|All||||| +|Audi|A3 2014-19|ACC + Lane Assist||||| +|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist||||| +|Audi|Q2 2018|ACC + Lane Assist||||| +|Audi|Q3 2020-21|ACC + Lane Assist||||| +|Audi|RS3 2018|ACC + Lane Assist||||| +|Audi|S3 2015-17|ACC + Lane Assist||||| +|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS||||| +|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise||||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise||||| +|Chrysler|Pacifica 2019-20|Adaptive Cruise||||| +|Chrysler|Pacifica 2021|All||||| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise||||| +|Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise||||| +|comma|body|All||||| +|Genesis|G70 2018-19|All||||| +|Genesis|G70 2020|All||||| +|Genesis|G80 2017-19|All||||| +|Genesis|G90 2017-18|All||||| +|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise||||| +|Honda|Accord 2018-22|All||||| +|Honda|Accord Hybrid 2018-22|All||||| +|Honda|Civic 2016-18|Honda Sensing||||| +|Honda|Civic 2019-21|All|||[2](#footnotes)|| +|Honda|Civic 2022|All||||| +|Honda|Civic Hatchback 2017-21|Honda Sensing||||| +|Honda|Civic Hatchback 2022|All||||| +|Honda|CR-V 2015-16|Touring||||| +|Honda|CR-V 2017-22|Honda Sensing||||| +|Honda|CR-V Hybrid 2017-19|Honda Sensing||||| +|Honda|e 2020|All||||| +|Honda|Fit 2018-20|Honda Sensing||||| +|Honda|Freed 2020|Honda Sensing||||| +|Honda|HR-V 2019-22|Honda Sensing||||| +|Honda|Insight 2019-22|All||||| +|Honda|Inspire 2018|All||||| +|Honda|Odyssey 2018-22|Honda Sensing||||| +|Honda|Passport 2019-21|All||||| +|Honda|Pilot 2016-22|Honda Sensing||||| +|Honda|Ridgeline 2017-22|Honda Sensing||||| +|Hyundai|Elantra 2017-19|SCC + LKAS||||| +|Hyundai|Elantra 2021-22|SCC + LKAS||||| +|Hyundai|Elantra Hybrid 2021-22|SCC + LKAS||||| +|Hyundai|Genesis 2015-16|SCC + LKAS||||| +|Hyundai|Ioniq Electric 2019|SCC + LKAS||||| +|Hyundai|Ioniq Electric 2020|SCC + LKAS||||| +|Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS||||| +|Hyundai|Ioniq Hybrid 2020-22|SCC + LFA||||| +|Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS||||| +|Hyundai|Ioniq Plug-in Hybrid 2020-21|SCC + LKAS||||| +|Hyundai|Kona 2020|SCC + LKAS||||| +|Hyundai|Kona Electric 2018-21|SCC + LKAS||||| +|Hyundai|Kona Hybrid 2020|SCC + LKAS||||| +|Hyundai|Palisade 2020-21|All||||| +|Hyundai|Santa Fe 2019-20|All||||| +|Hyundai|Santa Fe 2021-22|All||||| +|Hyundai|Santa Fe Hybrid 2022|All||||| +|Hyundai|Santa Fe Plug-in Hybrid 2022|All||||| +|Hyundai|Sonata 2018-19|SCC + LKAS||||| +|Hyundai|Sonata 2020-22|All||||| +|Hyundai|Sonata Hybrid 2020-22|All||||| +|Hyundai|Tucson 2021|SCC + LKAS||||| +|Hyundai|Tucson Diesel 2019|SCC + LKAS||||| +|Hyundai|Veloster 2019-20|SCC + LKAS||||| +|Jeep|Grand Cherokee 2016-18|Adaptive Cruise||||| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise||||| +|Kia|Ceed 2019|SCC + LKAS||||| +|Kia|EV6 2022|All||||| +|Kia|Forte 2018|SCC + LKAS||||| +|Kia|Forte 2019-21|SCC + LKAS||||| +|Kia|K5 2021-22|SCC||||| +|Kia|Niro Electric 2019-20|All||||| +|Kia|Niro Electric 2021|All||||| +|Kia|Niro Electric 2022|All||||| +|Kia|Niro Hybrid 2021|SCC + LKAS||||| +|Kia|Niro Hybrid 2022|SCC + LKAS||||| +|Kia|Niro Plug-in Hybrid 2019|SCC + LKAS||||| +|Kia|Optima 2017|SCC + LKAS||||| +|Kia|Optima 2019|SCC + LKAS||||| +|Kia|Seltos 2021|SCC + LKAS||||| +|Kia|Sorento 2018|SCC + LKAS||||| +|Kia|Sorento 2019|SCC + LKAS||||| +|Kia|Stinger 2018-20|SCC + LKAS||||| +|Kia|Telluride 2020|SCC + LKAS||||| +|Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)|||| +|Lexus|ES 2019-22|All||||| +|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)|||| +|Lexus|ES Hybrid 2019-22|All||||| +|Lexus|IS 2017-19|All||||| +|Lexus|NX 2018-19|All|[3](#footnotes)|||| +|Lexus|NX 2020-21|All||||| +|Lexus|NX Hybrid 2018-19|All|[3](#footnotes)|||| +|Lexus|NX Hybrid 2020-21|All||||| +|Lexus|RC 2017-2020|All||||| +|Lexus|RX 2016-18|All|[3](#footnotes)|||| +|Lexus|RX 2020-22|All||||| +|Lexus|RX Hybrid 2016-19|All|[3](#footnotes)|||| +|Lexus|RX Hybrid 2020-21|All||||| +|Lexus|UX Hybrid 2019-22|All||||| +|Mazda|CX-5 2022|All||||| +|Mazda|CX-9 2021-22|All||||| +|Nissan|Altima 2019-20|ProPILOT||||| +|Nissan|Leaf 2018-22|ProPILOT||||| +|Nissan|Rogue 2018-20|ProPILOT||||| +|Nissan|X-Trail 2017|ProPILOT||||| +|Ram|1500 2019-22|Adaptive Cruise||||| +|SEAT|Ateca 2018|Driver Assistance||||| +|SEAT|Leon 2014-20|Driver Assistance||||| +|Subaru|Ascent 2019-21|All||||| +|Subaru|Crosstrek 2018-19|EyeSight||||| +|Subaru|Crosstrek 2020-21|EyeSight||||| +|Subaru|Forester 2019-22|All||||| +|Subaru|Impreza 2017-19|EyeSight||||| +|Subaru|Impreza 2020-22|EyeSight||||| +|Subaru|XV 2018-19|EyeSight||||| +|Subaru|XV 2020-21|EyeSight||||| +|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance||||| +|Škoda|Karoq 2019|Driver Assistance||||| +|Škoda|Kodiaq 2018-19|Driver Assistance||||| +|Škoda|Octavia 2015, 2018-19|Driver Assistance||||| +|Škoda|Octavia RS 2016|Driver Assistance||||| +|Škoda|Scala 2020|Driver Assistance||||| +|Škoda|Superb 2015-18|Driver Assistance||||| +|Toyota|Alphard 2019-20|All||||| +|Toyota|Alphard Hybrid 2021|All||||| +|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon 2022|All||||| +|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon Hybrid 2022|All||||| +|Toyota|C-HR 2017-21|All||||| +|Toyota|C-HR Hybrid 2017-19|All||||| +|Toyota|Camry 2018-20|All||[4](#footnotes)||| +|Toyota|Camry 2021-22|All||[4](#footnotes)||| +|Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)||| +|Toyota|Camry Hybrid 2021-22|All||||| +|Toyota|Corolla 2017-19|All|[3](#footnotes)|||| +|Toyota|Corolla 2020-22|All||||| +|Toyota|Corolla Cross 2020-21 (Non-US only)|All||||| +|Toyota|Corolla Cross Hybrid 2020-22 (Non-US only)|All||||| +|Toyota|Corolla Hatchback 2019-22|All||||| +|Toyota|Corolla Hybrid 2020-22|All||||| +|Toyota|Highlander 2017-19|All|[3](#footnotes)|||| +|Toyota|Highlander 2020-22|All||||| +|Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)|||| +|Toyota|Highlander Hybrid 2020-22|All||||| +|Toyota|Mirai 2021|All||||| +|Toyota|Prius 2016-20|TSS-P|[3](#footnotes)|||| +|Toyota|Prius 2021-22|All||||| +|Toyota|Prius Prime 2017-20|All|[3](#footnotes)|||| +|Toyota|Prius Prime 2021-22|All||||| +|Toyota|Prius v 2017|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 2019-21|All||||| +|Toyota|RAV4 2022|All||||| +|Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 Hybrid 2019-21|All||||| +|Toyota|RAV4 Hybrid 2022|All||||| +|Toyota|Sienna 2018-20|All|[3](#footnotes)|||| +|Volkswagen|Arteon 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Arteon eHybrid 2020-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Arteon R 2020-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|California 2021[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance||||| +|Volkswagen|CC 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|e-Golf 2014-20|Driver Assistance||||| +|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance||||| +|Volkswagen|Golf Alltrack 2015-19|Driver Assistance||||| +|Volkswagen|Golf GTD 2015-20|Driver Assistance||||| +|Volkswagen|Golf GTE 2015-20|Driver Assistance||||| +|Volkswagen|Golf GTI 2015-21|Driver Assistance||||| +|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance||||| +|Volkswagen|Golf SportsVan 2015-20|Driver Assistance||||| +|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Passat 2015-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Passat GTE 2015-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance||||| +|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Touran 2017|Driver Assistance||||| 1Requires an OBD-II car harness and community built ASCM harness. NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).
diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 891445a558705d..8603f31434f840 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -5,12 +5,6 @@ A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. -Cars are organized into three tiers: - -{% for tier in tiers %} -- {{tier.name.title()}} - {{tier.value}} -{% endfor %} - How We Rate The Cars --- @@ -23,20 +17,16 @@ How We Rate The Cars {% endfor %} {% endfor %} -**All supported cars can move between the tiers as support changes.** -{% for tier, cars in tiers.items() %} -# {{tier.name.title()}} - {{cars | length}} cars +# {{all_car_info | length}} Supported Cars |{{Column | map(attribute='value') | join('|')}}| -|---|---|---|:---:|:---:|:---:|:---:|:---:| -{% for car_info in cars %} +|---|---|---|:---:|:---:|:---:|:---:| +{% for car_info in all_car_info %} |{% for column in Column %}{{car_info.get_column(column, star_icon, footnote_tag)}}|{% endfor %} {% endfor %} -{% endfor %} - {% for footnote in footnotes %} {{loop.index}}{{footnote}}
diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 860503dbdde74d..5831916039d517 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -7,7 +7,7 @@ from typing import Dict, List from common.basedir import BASEDIR -from selfdrive.car.docs_definitions import STAR_DESCRIPTIONS, CarInfo, Column, Star, Tier +from selfdrive.car.docs_definitions import STAR_DESCRIPTIONS, StarColumns, TierColumns, CarInfo, Column, Star from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR from selfdrive.car.tests.routes import non_tested_cars @@ -47,25 +47,21 @@ def get_all_car_info() -> List[CarInfo]: return sorted_cars -def sort_by_tier(all_car_info: List[CarInfo]) -> Dict[Tier, List[CarInfo]]: - tier_car_info: Dict[Tier, List[CarInfo]] = {tier: [] for tier in Tier} - for car_info in all_car_info: - tier_car_info[car_info.tier].append(car_info) - - # Sort cars by make and model + year - for tier, cars in tier_car_info.items(): - tier_car_info[tier] = natsorted(cars, key=lambda car: (car.make + car.model).lower()) - - return tier_car_info - - -def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: +def generate_cars_md(all_car_info: List[CarInfo], template_fn: str, only_tier_cols: bool) -> str: with open(template_fn, "r") as f: template = jinja2.Template(f.read(), trim_blocks=True, lstrip_blocks=True) + cols = list(Column) + if only_tier_cols: + hide_cols = set(StarColumns) - set(TierColumns) + cols = [c for c in cols if c not in hide_cols] + for car in all_car_info: + for c in hide_cols: + del car.row[c] + footnotes = [fn.value.text for fn in ALL_FOOTNOTES] - cars_md: str = template.render(tiers=sort_by_tier(all_car_info), all_car_info=all_car_info, - footnotes=footnotes, Star=Star, Column=Column, star_descriptions=STAR_DESCRIPTIONS) + cars_md: str = template.render(all_car_info=all_car_info, + footnotes=footnotes, Star=Star, Column=cols, star_descriptions=STAR_DESCRIPTIONS) return cars_md @@ -73,10 +69,11 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str) -> str: parser = argparse.ArgumentParser(description="Auto generates supported cars documentation", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("--tier-columns", action="store_true", help="Include only columns that count in the tier") parser.add_argument("--template", default=CARS_MD_TEMPLATE, help="Override default template filename") parser.add_argument("--out", default=CARS_MD_OUT, help="Override default generated filename") args = parser.parse_args() with open(args.out, 'w') as f: - f.write(generate_cars_md(get_all_car_info(), args.template)) + f.write(generate_cars_md(get_all_car_info(), args.template, args.tier_columns)) print(f"Generated and written to {args.out}") diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 1efa23037fd08b..99873a9c23f64b 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -1,5 +1,3 @@ -import math - from cereal import car from collections import namedtuple from dataclasses import dataclass @@ -12,9 +10,9 @@ class Tier(Enum): - GOLD = "The best openpilot experience. Great highway driving and beyond." - SILVER = "A solid highway driving experience, but is limited by stock longitudinal. May be upgraded in the future." - BRONZE = "A good highway experience, but may have limited performance in traffic and on sharp turns." + GOLD = 0 + SILVER = 1 + BRONZE = 2 class Column(Enum): @@ -25,7 +23,6 @@ class Column(Enum): FSR_LONGITUDINAL = "Stop and Go" FSR_STEERING = "Steer to 0" STEERING_TORQUE = "Steering Torque" - MAINTAINED = "Actively Maintained" class Star(Enum): @@ -35,6 +32,7 @@ class Star(Enum): StarColumns = list(Column)[3:] +TierColumns = (Column.FSR_LONGITUDINAL, Column.FSR_STEERING, Column.STEERING_TORQUE) CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) @@ -72,6 +70,7 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic min_enable_speed = self.min_enable_speed self.car_name = CP.carName + self.car_fingerprint = CP.carFingerprint self.make, self.model = self.name.split(' ', 1) self.row = { Column.MAKE: self.make, @@ -82,17 +81,14 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic Column.FSR_LONGITUDINAL: Star.FULL if min_enable_speed <= 0. else Star.EMPTY, Column.FSR_STEERING: Star.FULL if min_steer_speed <= 0. else Star.EMPTY, # Column.STEERING_TORQUE set below - Column.MAINTAINED: Star.FULL if CP.carFingerprint not in non_tested_cars and self.harness is not Harness.none else Star.EMPTY, } # Set steering torque star from max lateral acceleration - if not math.isnan(CP.maxLateralAccel): - if CP.maxLateralAccel >= GREAT_TORQUE_THRESHOLD: - self.row[Column.STEERING_TORQUE] = Star.FULL - elif CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: - self.row[Column.STEERING_TORQUE] = Star.HALF - else: - self.row[Column.STEERING_TORQUE] = Star.EMPTY + assert CP.maxLateralAccel > 0.1 + if CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: + self.row[Column.STEERING_TORQUE] = Star.FULL + else: + self.row[Column.STEERING_TORQUE] = Star.EMPTY if CP.notCar: for col in StarColumns: @@ -105,7 +101,15 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic if footnote is not None and footnote.value.star is not None: self.row[column] = footnote.value.star - self.tier = {5: Tier.GOLD, 4: Tier.SILVER}.get(list(self.row.values()).count(Star.FULL), Tier.BRONZE) + # openpilot ACC star doesn't count for tiers + full_stars = [s for col, s in self.row.items() if col in TierColumns].count(Star.FULL) + if full_stars == len(TierColumns): + self.tier = Tier.GOLD + elif full_stars == (len(TierColumns)-1): + self.tier = Tier.SILVER + else: + self.tier = Tier.BRONZE + return self @no_type_check @@ -156,11 +160,6 @@ class Harness(Enum): STAR_DESCRIPTIONS = { "Gas & Brakes": { # icon and row name - "openpilot Adaptive Cruise Control (ACC)": [ # star column - [Star.FULL.value, "openpilot is able to control the gas and brakes."], - [Star.HALF.value, "openpilot is able to control the gas and brakes with some restrictions."], - [Star.EMPTY.value, "The gas and brakes are controlled by the car's stock Adaptive Cruise Control (ACC) system."], - ], Column.FSR_LONGITUDINAL.value: [ [Star.FULL.value, "Adaptive Cruise Control (ACC) operates down to 0 mph."], [Star.EMPTY.value, "Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed."], @@ -172,15 +171,8 @@ class Harness(Enum): [Star.EMPTY.value, "No steering control below certain speeds."], ], Column.STEERING_TORQUE.value: [ - [Star.FULL.value, "Car has enough steering torque to take tighter turns."], - [Star.HALF.value, "Car has enough steering torque for comfortable highway driving."], + [Star.FULL.value, "Car has enough steering torque to take tight turns."], [Star.EMPTY.value, "Limited ability to make turns."], ], }, - "Support": { - Column.MAINTAINED.value: [ - [Star.FULL.value, "Mainline software support, harness hardware sold by comma, lots of users, primary development target."], - [Star.EMPTY.value, "Low user count, community maintained, harness hardware not sold by comma."], - ], - }, } diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index 98c909a9be9d97..dbdd9769decbae 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -4,6 +4,7 @@ from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.docs import CARS_MD_OUT, CARS_MD_TEMPLATE, generate_cars_md, get_all_car_info from selfdrive.car.docs_definitions import Column, Star +from selfdrive.car.honda.values import CAR as HONDA class TestCarDocs(unittest.TestCase): @@ -11,7 +12,7 @@ def setUp(self): self.all_cars = get_all_car_info() def test_generator(self): - generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE) + generated_cars_md = generate_cars_md(self.all_cars, CARS_MD_TEMPLATE, False) with open(CARS_MD_OUT, "r") as f: current_cars_md = f.read() @@ -43,8 +44,9 @@ def test_torque_star(self): # Asserts brand-specific assumptions around steering torque star for car in self.all_cars: with self.subTest(car=car): - if car.car_name == "honda": - self.assertIn(car.row[Column.STEERING_TORQUE], (Star.EMPTY, Star.HALF), f"{car.name} has full torque star") + # honda sanity check, it's the definition of a no torque star + if car.car_fingerprint in (HONDA.ACCORD, HONDA.CIVIC, HONDA.CRV, HONDA.ODYSSEY, HONDA.PILOT): + self.assertEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has full torque star") elif car.car_name in ("toyota", "hyundai"): self.assertNotEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has no torque star") From cc3857eb62cd169be05a4cb22c844a8b2ec94139 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 18 Jul 2022 21:18:10 -0700 Subject: [PATCH 361/436] updated: add branch switching (#25200) * switch param * add to ui * fix that * fetch for new branch Co-authored-by: Comma Device --- common/params.cc | 2 ++ selfdrive/manager/manager.py | 3 ++- selfdrive/ui/qt/offroad/settings.cc | 20 ++++++++++++++++++-- selfdrive/ui/qt/offroad/settings.h | 1 + selfdrive/ui/qt/widgets/controls.h | 4 ++-- selfdrive/updated.py | 20 ++++++++++++++------ 6 files changed, 39 insertions(+), 11 deletions(-) diff --git a/common/params.cc b/common/params.cc index 77be6f36b18c76..1ead28d6cd0d9f 100644 --- a/common/params.cc +++ b/common/params.cc @@ -126,6 +126,7 @@ std::unordered_map keys = { {"IsOnroad", PERSISTENT}, {"IsRHD", PERSISTENT}, {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, + {"IsTestedBranch", CLEAR_ON_MANAGER_START}, {"IsUpdateAvailable", CLEAR_ON_MANAGER_START}, {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"LaikadEphemeris", PERSISTENT | DONT_LOG}, @@ -154,6 +155,7 @@ std::unordered_map keys = { {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_OFF}, {"SshEnabled", PERSISTENT}, {"SubscriberInfo", PERSISTENT}, + {"SwitchToBranch", CLEAR_ON_MANAGER_START}, {"TermsVersion", PERSISTENT}, {"Timezone", PERSISTENT}, {"TrainingVersion", PERSISTENT}, diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 140c7f1d442680..9c370cb3d8c8b2 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -20,7 +20,7 @@ from selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID from system.swaglog import cloudlog, add_file_handler from system.version import is_dirty, get_commit, get_version, get_origin, get_short_branch, \ - terms_version, training_version + terms_version, training_version, is_tested_branch sys.path.append(os.path.join(BASEDIR, "pyextra")) @@ -78,6 +78,7 @@ def manager_init() -> None: params.put("GitCommit", get_commit(default="")) params.put("GitBranch", get_short_branch(default="")) params.put("GitRemote", get_origin(default="")) + params.put_bool("IsTestedBranch", is_tested_branch()) # set dongle id reg_res = register(show_spinner=True) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 7b6be919e033d7..65b9e3e4cc9964 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -24,6 +24,7 @@ #include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/qt_window.h" +#include "selfdrive/ui/qt/widgets/input.h" TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { // param, title, desc, icon @@ -253,7 +254,19 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { } std::system("pkill -1 -f selfdrive.updated"); }); - + connect(uiState(), &UIState::offroadTransition, updateBtn, &QPushButton::setEnabled); + + branchSwitcherBtn = new ButtonControl(tr("Switch Branch"), tr("ENTER")); + connect(branchSwitcherBtn, &ButtonControl::clicked, [=]() { + QString branch = InputDialog::getText(tr("Enter name of new branch"), this); + if (branch.isEmpty()) { + params.remove("SwitchToBranch"); + } else { + params.put("SwitchToBranch", branch.toStdString()); + } + std::system("pkill -1 -f selfdrive.updated"); + }); + connect(uiState(), &UIState::offroadTransition, branchSwitcherBtn, &QPushButton::setEnabled); auto uninstallBtn = new ButtonControl(tr("Uninstall %1").arg(getBrand()), tr("UNINSTALL")); connect(uninstallBtn, &ButtonControl::clicked, [&]() { @@ -263,8 +276,11 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { }); connect(uiState(), &UIState::offroadTransition, uninstallBtn, &QPushButton::setEnabled); - QWidget *widgets[] = {versionLbl, lastUpdateLbl, updateBtn, gitBranchLbl, gitCommitLbl, osVersionLbl, uninstallBtn}; + QWidget *widgets[] = {versionLbl, lastUpdateLbl, updateBtn, branchSwitcherBtn, gitBranchLbl, gitCommitLbl, osVersionLbl, uninstallBtn}; for (QWidget* w : widgets) { + if (w == branchSwitcherBtn && params.getBool("IsTestedBranch")) { + continue; + } addItem(w); } diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 160f10f99ffcd9..45efe255cff930 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -71,6 +71,7 @@ class SoftwarePanel : public ListWidget { LabelControl *versionLbl; LabelControl *lastUpdateLbl; ButtonControl *updateBtn; + ButtonControl *branchSwitcherBtn; Params params; QFileSystemWatcher *fs_watch; diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index aed99edae8da5a..4245a9c04a930d 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -24,9 +24,9 @@ class ElidedLabel : public QLabel { protected: void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent* event) override; - void mouseReleaseEvent(QMouseEvent *event) override { + void mouseReleaseEvent(QMouseEvent *event) override { if (rect().contains(event->pos())) { - emit clicked(); + emit clicked(); } } QString lastText_, elidedText_; diff --git a/selfdrive/updated.py b/selfdrive/updated.py index bdec383f52b824..34ab338bb17983 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -311,21 +311,29 @@ def fetch_update(wait_helper: WaitTimeHelper) -> bool: cur_hash = run(["git", "rev-parse", "HEAD"], OVERLAY_MERGED).rstrip() upstream_hash = run(["git", "rev-parse", "@{u}"], OVERLAY_MERGED).rstrip() - new_version: bool = cur_hash != upstream_hash + new_version = cur_hash != upstream_hash git_fetch_result = check_git_fetch_result(git_fetch_output) + new_branch = Params().get("SwitchToBranch", encoding='utf8') + if new_branch is not None: + new_version = True + cloudlog.info(f"comparing {cur_hash} to {upstream_hash}") if new_version or git_fetch_result: cloudlog.info("Running update") if new_version: cloudlog.info("git reset in progress") - r = [ - run(["git", "reset", "--hard", "@{u}"], OVERLAY_MERGED, low_priority=True), - run(["git", "clean", "-xdf"], OVERLAY_MERGED, low_priority=True ), - run(["git", "submodule", "init"], OVERLAY_MERGED, low_priority=True), - run(["git", "submodule", "update"], OVERLAY_MERGED, low_priority=True), + cmds = [ + ["git", "reset", "--hard", "@{u}"], + ["git", "clean", "-xdf"], + ["git", "submodule", "init"], + ["git", "submodule", "update"], ] + if new_branch is not None: + cloudlog.info(f"switching to branch {repr(new_branch)}") + cmds.insert(0, ["git", "checkout", "-f", new_branch]) + r = [run(cmd, OVERLAY_MERGED, low_priority=True) for cmd in cmds] cloudlog.info("git reset success: %s", '\n'.join(r)) if AGNOS: From 758069464cfcc2d0c3c991533aa800707aa823c1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 22:05:11 -0700 Subject: [PATCH 362/436] Fix release tests --- selfdrive/ui/translations/main_ko.ts | 153 +++++++++++++---------- selfdrive/ui/translations/main_zh-CHS.ts | 153 +++++++++++++---------- selfdrive/ui/translations/main_zh-CHT.ts | 153 +++++++++++++---------- selfdrive/updated.py | 2 +- 4 files changed, 253 insertions(+), 208 deletions(-) diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index d737a9ce3367fa..4662a862bc0177 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A N/A - + Serial Serial - + Driver Camera 운전자 카메라 - + PREVIEW 미리보기 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 운전자 모니터링이 좋은 가시성을 갖도록 운전자를 향한 카메라를 미리 봅니다. (차량연결은 해제되어있어야 합니다) - + Reset Calibration 캘리브레이션 재설정 - + RESET 재설정 - + Are you sure you want to reset calibration? 캘리브레이션을 재설정하시겠습니까? - + Review Training Guide 트레이닝 가이드 다시보기 - + REVIEW 다시보기 - + Review the rules, features, and limitations of openpilot openpilot의 규칙, 기능 및 제한 다시보기 - + Are you sure you want to review the training guide? 트레이닝 가이드를 다시보시겠습니까? - + Regulatory 규제 - + VIEW 보기 - + Change Language 언어 변경 - + CHANGE 변경 - + Select a language 언어를 선택하세요 - + Reboot 재부팅 - + Power Off 전원 종료 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot은 장치를 좌측 또는 우측은 4° 이내, 위쪽 5° 또는 아래쪽은 8° 이내로 설치해야 합니다. openpilot은 지속적으로 보정되므로 리셋이 거의 필요하지 않습니다. - + Your device is pointed %1° %2 and %3° %4. 사용자의 장치가 %1° %2 및 %3° %4를 가리키고 있습니다. - + down 아래로 - + up 위로 - + left 좌측으로 - + right 우측으로 - + Are you sure you want to reboot? 재부팅 하시겠습니까? - + Disengage to Reboot 재부팅 하려면 해제하세요 - + Are you sure you want to power off? 전원을 종료하시겠습니까? - + Disengage to Power Off 전원을 종료하려면 해제하세요 @@ -710,33 +710,33 @@ location set SettingsWindow - + × × - + Device 장치 - - + + Network 네트워크 - + Toggles 토글 - + Software 소프트웨어 - + Navigation 네비게이션 @@ -975,68 +975,83 @@ location set SoftwarePanel - + Git Branch Git 브렌치 - + Git Commit Git 커밋 - + OS Version OS 버전 - + Version 버전 - + Last Update Check 최신 업데이트 검사 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 최근에 openpilot이 업데이트를 성공적으로 확인했습니다. 업데이트 프로그램은 차량 연결이 해제되었을때만 작동합니다. - + Check for Update 업데이트 확인 - + CHECKING 확인중 - + + Switch Branch + + + + + ENTER + + + + + Enter name of new branch + + + + UNINSTALL 제거 - + Uninstall %1 제거 %1 - + Are you sure you want to uninstall? 제거하시겠습니까? - + failed to fetch update 업데이트를 가져올수없습니다 - - + + CHECK 확인 @@ -1124,92 +1139,92 @@ location set TogglesPanel - + Enable openpilot openpilot 사용 - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 어댑티브 크루즈 컨트롤 및 차선 유지 운전자 보조를 위해 openpilot 시스템을 사용하십시오. 이 기능을 사용하려면 항상 주의를 기울여야 합니다. 이 설정을 변경하면 차량 전원이 꺼질 때 적용됩니다. - + Enable Lane Departure Warnings 차선 이탈 경고 사용 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 차량이 50km/h(31mph) 이상의 속도로 주행하는 동안 방향 지시등이 활성화되지 않은 상태에서 감지된 차선 위를 주행할 경우 차선이탈 경고를 사용합니다. - + Enable Right-Hand Drive 우측핸들 사용 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. openpilot이 좌측 교통 규칙을 준수하고 우측 운전석에서 운전자 모니터링을 수행합니다. - + Use Metric System 미터법 사용 - + Display speed in km/h instead of mph. mph 대신 km/h로 속도를 표시합니다. - + Record and Upload Driver Camera 운전자 카메라 녹화 및 업로드 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 운전자 카메라에서 데이터를 업로드하고 운전자 모니터링 알고리즘을 개선합니다. - + Disengage On Accelerator Pedal 가속페달 조작시 해제 - + When enabled, pressing the accelerator pedal will disengage openpilot. 활성화된 경우 가속 페달을 누르면 openpilot이 해제됩니다. - + Show ETA in 24h format 24시간 형식으로 도착예정시간 표시 - + Use 24h format instead of am/pm 오전/오후 대신 24시간 형식 사용 - + Show Map on Left Side of UI - + Show map on left side when in split screen view. - + openpilot Longitudinal Control openpilot Longitudinal Control - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot은 차량'의 레이더를 무력화시키고 가속페달과 브레이크의 제어를 인계받을 것이다. 경고: AEB를 비활성화합니다! diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 682d1a05cf5c93..744cf90345f3cf 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID 设备ID(Dongle ID) - + N/A N/A - + Serial 序列号 - + Driver Camera 驾驶员摄像头 - + PREVIEW 预览 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 打开并预览驾驶员摄像头,以确保驾驶员监控具有良好视野。仅熄火时可用。 - + Reset Calibration 重置设备校准 - + RESET 重置 - + Are you sure you want to reset calibration? 您确定要重置设备校准吗? - + Review Training Guide 新手指南 - + REVIEW 查看 - + Review the rules, features, and limitations of openpilot 查看openpilot的使用规则,以及其功能和限制。 - + Are you sure you want to review the training guide? 您确定要查看新手指南吗? - + Regulatory 监管信息 - + VIEW 查看 - + Change Language 切换语言 - + CHANGE 切换 - + Select a language 选择语言 - + Reboot 重启 - + Power Off 关机 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot要求设备安装的偏航角在左4°和右4°之间,俯仰角在上5°和下8°之间。一般来说,openpilot会持续更新校准,很少需要重置。 - + Your device is pointed %1° %2 and %3° %4. 您的设备校准为%1° %2、%3° %4。 - + down 朝下 - + up 朝上 - + left 朝左 - + right 朝右 - + Are you sure you want to reboot? 您确定要重新启动吗? - + Disengage to Reboot 取消openpilot以重新启动 - + Are you sure you want to power off? 您确定要关机吗? - + Disengage to Power Off 取消openpilot以关机 @@ -708,33 +708,33 @@ location set SettingsWindow - + × × - + Device 设备 - - + + Network 网络 - + Toggles 设定 - + Software 软件 - + Navigation 导航 @@ -973,68 +973,83 @@ location set SoftwarePanel - + Git Branch Git Branch - + Git Commit Git Commit - + OS Version 系统版本 - + Version 软件版本 - + Last Update Check 上次检查更新 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上一次成功检查更新的时间。更新程序仅在汽车熄火时运行。 - + Check for Update 检查更新 - + CHECKING 正在检查更新 - + + Switch Branch + + + + + ENTER + + + + + Enter name of new branch + + + + UNINSTALL 卸载 - + Uninstall %1 卸载 %1 - + Are you sure you want to uninstall? 您确定要卸载吗? - + failed to fetch update 获取更新失败 - - + + CHECK 查看 @@ -1122,92 +1137,92 @@ location set TogglesPanel - + Enable openpilot 启用openpilot - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 使用openpilot进行自适应巡航和车道保持辅助。使用此功能时您必须时刻保持注意力。该设置的更改在熄火时生效。 - + Enable Lane Departure Warnings 启用车道偏离警告 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 车速超过31mph(50km/h)时,若检测到车辆越过车道线且未打转向灯,系统将发出警告以提醒您返回车道。 - + Enable Right-Hand Drive 启用右舵模式 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. 允许openpilot遵守左侧交通惯例并在右侧驾驶座上执行驾驶员监控。 - + Use Metric System 使用公制单位 - + Display speed in km/h instead of mph. 显示车速时,以km/h代替mph。 - + Record and Upload Driver Camera 录制并上传驾驶员摄像头 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上传驾驶员摄像头的数据,帮助改进驾驶员监控算法。 - + Disengage On Accelerator Pedal 踩油门时取消控制 - + When enabled, pressing the accelerator pedal will disengage openpilot. 启用后,踩下油门踏板将取消openpilot。 - + Show ETA in 24h format 以24小时格式显示预计到达时间 - + Use 24h format instead of am/pm 使用24小时制代替am/pm - + Show Map on Left Side of UI - + Show map on left side when in split screen view. - + openpilot Longitudinal Control openpilot纵向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot将禁用车辆的雷达并接管油门和刹车的控制。警告:AEB将被禁用! diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index c04a3053552108..30fd666edd86f3 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -108,152 +108,152 @@ DevicePanel - + Dongle ID Dongle ID - + N/A 無法使用 - + Serial 序號 - + Driver Camera 駕駛員攝像頭 - + PREVIEW 預覽 - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。僅在熄火時可用。 - + Reset Calibration 重置校準 - + RESET 重置 - + Are you sure you want to reset calibration? 您確定要重置校準嗎? - + Review Training Guide 觀看使用教學 - + REVIEW 觀看 - + Review the rules, features, and limitations of openpilot 觀看 openpilot 的使用規則、功能和限制 - + Are you sure you want to review the training guide? 您確定要觀看使用教學嗎? - + Regulatory 法規/監管 - + VIEW 觀看 - + Change Language 更改語言 - + CHANGE 更改 - + Select a language 選擇語言 - + Reboot 重新啟動 - + Power Off 關機 - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. openpilot 需要將裝置固定在左右偏差 4° 以內,朝上偏差 5° 以内或朝下偏差 8° 以内。鏡頭在後台會持續自動校準,很少有需要重置的情况。 - + Your device is pointed %1° %2 and %3° %4. 你的設備目前朝%2 %1° 以及朝%4 %3° 。 - + down - + up - + left - + right - + Are you sure you want to reboot? 您確定要重新啟動嗎? - + Disengage to Reboot 請先取消控車才能重新啟動 - + Are you sure you want to power off? 您確定您要關機嗎? - + Disengage to Power Off 請先取消控車才能關機 @@ -713,33 +713,33 @@ location set SettingsWindow - + × × - + Device 設備 - - + + Network 網路 - + Toggles 設定 - + Software 軟體 - + Navigation 導航 @@ -978,68 +978,83 @@ location set SoftwarePanel - + Git Branch Git 分支 - + Git Commit Git 提交 - + OS Version 系統版本 - + Version 版本 - + Last Update Check 上次檢查時間 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. 上次成功檢查更新的時間。更新系統只會在車子熄火時執行。 - + Check for Update 檢查更新 - + CHECKING 檢查中 - + + Switch Branch + + + + + ENTER + + + + + Enter name of new branch + + + + UNINSTALL 卸載 - + Uninstall %1 卸載 %1 - + Are you sure you want to uninstall? 您確定您要卸載嗎? - + failed to fetch update 下載更新失敗 - - + + CHECK 檢查 @@ -1127,92 +1142,92 @@ location set TogglesPanel - + Enable openpilot 啟用 openpilot - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. 使用 openpilot 的主動式巡航和車道保持功能,開啟後您需要持續集中注意力,設定變更在重新啟動車輛後生效。 - + Enable Lane Departure Warnings 啟用車道偏離警告 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 車速在時速 50 公里 (31 英里) 以上且未打方向燈的情況下,如果偵測到車輛駛出目前車道線時,發出車道偏離警告。 - + Enable Right-Hand Drive 啟用右駕模式 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. openpilot 將對右側駕駛進行監控 (但仍遵守靠左駕的交通慣例)。 - + Use Metric System 使用公制單位 - + Display speed in km/h instead of mph. 啟用後,速度單位顯示將從 mp/h 改為 km/h。 - + Record and Upload Driver Camera 記錄並上傳駕駛監控影像 - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. 上傳駕駛監控的錄像來協助我們提升駕駛監控的準確率。 - + Disengage On Accelerator Pedal 油門取消控車 - + When enabled, pressing the accelerator pedal will disengage openpilot. 啟用後,踩踏油門將會取消 openpilot 控制。 - + Show ETA in 24h format 預計到達時間單位改用 24 小時制 - + Use 24h format instead of am/pm 使用 24 小時制。(預設值為 12 小時制) - + Show Map on Left Side of UI - + Show map on left side when in split screen view. - + openpilot Longitudinal Control openpilot 縱向控制 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! openpilot 將會關閉雷達訊號並接管油門和剎車的控制。注意:這也會關閉自動緊急煞車 (AEB) 系統! diff --git a/selfdrive/updated.py b/selfdrive/updated.py index 34ab338bb17983..e8905962b2d6ef 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -311,7 +311,7 @@ def fetch_update(wait_helper: WaitTimeHelper) -> bool: cur_hash = run(["git", "rev-parse", "HEAD"], OVERLAY_MERGED).rstrip() upstream_hash = run(["git", "rev-parse", "@{u}"], OVERLAY_MERGED).rstrip() - new_version = cur_hash != upstream_hash + new_version: bool = cur_hash != upstream_hash git_fetch_result = check_git_fetch_result(git_fetch_output) new_branch = Params().get("SwitchToBranch", encoding='utf8') From b30871daada85f63558518daa3157aae0b5fb79f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 22:34:50 -0700 Subject: [PATCH 363/436] CarInfo: parse out model years (#25208) * add function * split model and years * typing and clean uop * one function to split into make, model, years * add test for bad years * simpler re * don't match only numbers * clean up * we can just use name * clean up --- docs/CARS.md | 6 +++--- selfdrive/car/docs.py | 5 ++--- selfdrive/car/docs_definitions.py | 29 ++++++++++++++++++++--------- selfdrive/car/tests/test_docs.py | 6 ++++++ selfdrive/car/toyota/values.py | 6 +++--- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 0fee4ddc840e2d..834e8e402fe7bf 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -117,7 +117,7 @@ How We Rate The Cars |Lexus|NX 2020-21|All||||| |Lexus|NX Hybrid 2018-19|All|[3](#footnotes)|||| |Lexus|NX Hybrid 2020-21|All||||| -|Lexus|RC 2017-2020|All||||| +|Lexus|RC 2017-20|All||||| |Lexus|RX 2016-18|All|[3](#footnotes)|||| |Lexus|RX 2020-22|All||||| |Lexus|RX Hybrid 2016-19|All|[3](#footnotes)|||| @@ -162,8 +162,8 @@ How We Rate The Cars |Toyota|Camry Hybrid 2021-22|All||||| |Toyota|Corolla 2017-19|All|[3](#footnotes)|||| |Toyota|Corolla 2020-22|All||||| -|Toyota|Corolla Cross 2020-21 (Non-US only)|All||||| -|Toyota|Corolla Cross Hybrid 2020-22 (Non-US only)|All||||| +|Toyota|Corolla Cross (Non-US only) 2020-21|All||||| +|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All||||| |Toyota|Corolla Hatchback 2019-22|All||||| |Toyota|Corolla Hybrid 2020-22|All||||| |Toyota|Highlander 2017-19|All|[3](#footnotes)|||| diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 5831916039d517..eb2f1923c8569e 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -10,7 +10,6 @@ from selfdrive.car.docs_definitions import STAR_DESCRIPTIONS, StarColumns, TierColumns, CarInfo, Column, Star from selfdrive.car.car_helpers import interfaces, get_interface_attr from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR -from selfdrive.car.tests.routes import non_tested_cars def get_all_footnotes() -> Dict[Enum, int]: @@ -40,10 +39,10 @@ def get_all_car_info() -> List[CarInfo]: car_info = (car_info,) for _car_info in car_info: - all_car_info.append(_car_info.init(CP, non_tested_cars, ALL_FOOTNOTES)) + all_car_info.append(_car_info.init(CP, ALL_FOOTNOTES)) # Sort cars by make and model + year - sorted_cars: List[CarInfo] = natsorted(all_car_info, key=lambda car: (car.make + car.model).lower()) + sorted_cars: List[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower()) return sorted_cars diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 99873a9c23f64b..3c58b30436cc0a 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -1,12 +1,13 @@ +import re + from cereal import car from collections import namedtuple from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Optional, Union, no_type_check +from typing import Dict, List, Optional, Tuple, Union, no_type_check -TACO_TORQUE_THRESHOLD = 2.5 # m/s^2 -GREAT_TORQUE_THRESHOLD = 1.4 # m/s^2 GOOD_TORQUE_THRESHOLD = 1.0 # m/s^2 +MODEL_YEARS_RE = r"(?<= )((\d{4}-\d{2})|(\d{4}))(,|$)" class Tier(Enum): @@ -45,6 +46,16 @@ def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[En return None +def split_name(name: str) -> Tuple[str, str, str]: + make, model = name.split(" ", 1) + years = "" + match = re.search(MODEL_YEARS_RE, model) + if match is not None: + years = model[match.start():] + model = model[:match.start() - 1] + return make, model, years + + @dataclass class CarInfo: name: str @@ -55,7 +66,7 @@ class CarInfo: min_enable_speed: Optional[float] = None harness: Optional[Enum] = None - def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dict[Enum, int]): + def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]): # TODO: set all the min steer speeds in carParams and remove this min_steer_speed = CP.minSteerSpeed if self.min_steer_speed is not None: @@ -71,7 +82,7 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic self.car_name = CP.carName self.car_fingerprint = CP.carFingerprint - self.make, self.model = self.name.split(' ', 1) + self.make, self.model, self.years = split_name(self.name) self.row = { Column.MAKE: self.make, Column.MODEL: self.model, @@ -80,15 +91,13 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic Column.LONGITUDINAL: Star.FULL if CP.openpilotLongitudinalControl and not CP.radarOffCan else Star.EMPTY, Column.FSR_LONGITUDINAL: Star.FULL if min_enable_speed <= 0. else Star.EMPTY, Column.FSR_STEERING: Star.FULL if min_steer_speed <= 0. else Star.EMPTY, - # Column.STEERING_TORQUE set below + Column.STEERING_TORQUE: Star.EMPTY, } # Set steering torque star from max lateral acceleration assert CP.maxLateralAccel > 0.1 if CP.maxLateralAccel >= GOOD_TORQUE_THRESHOLD: self.row[Column.STEERING_TORQUE] = Star.FULL - else: - self.row[Column.STEERING_TORQUE] = Star.EMPTY if CP.notCar: for col in StarColumns: @@ -105,7 +114,7 @@ def init(self, CP: car.CarParams, non_tested_cars: List[str], all_footnotes: Dic full_stars = [s for col, s in self.row.items() if col in TierColumns].count(Star.FULL) if full_stars == len(TierColumns): self.tier = Tier.GOLD - elif full_stars == (len(TierColumns)-1): + elif full_stars == len(TierColumns) - 1: self.tier = Tier.SILVER else: self.tier = Tier.BRONZE @@ -117,6 +126,8 @@ def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: item: Union[str, Star] = self.row[column] if column in StarColumns: item = star_icon.format(item.value) + elif column == Column.MODEL and len(self.years): + item += f" {self.years}" footnote = get_footnote(self.footnotes, column) if footnote is not None: diff --git a/selfdrive/car/tests/test_docs.py b/selfdrive/car/tests/test_docs.py index dbdd9769decbae..e31ad0d5c1325c 100755 --- a/selfdrive/car/tests/test_docs.py +++ b/selfdrive/car/tests/test_docs.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import re import unittest from selfdrive.car.car_helpers import interfaces, get_interface_attr @@ -50,6 +51,11 @@ def test_torque_star(self): elif car.car_name in ("toyota", "hyundai"): self.assertNotEqual(car.row[Column.STEERING_TORQUE], Star.EMPTY, f"{car.name} has no torque star") + def test_year_format(self): + for car in self.all_cars: + with self.subTest(car=car): + self.assertIsNone(re.search(r"\d{4}-\d{4}", car.name), f"Format years correctly: {car.name}") + if __name__ == "__main__": unittest.main() diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 49aa3c9a949283..08ee9593c64b3e 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -119,12 +119,12 @@ class ToyotaCarInfo(CarInfo): CAR.COROLLA: ToyotaCarInfo("Toyota Corolla 2017-19", footnotes=[Footnote.DSU]), CAR.COROLLA_TSS2: [ ToyotaCarInfo("Toyota Corolla 2020-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), - ToyotaCarInfo("Toyota Corolla Cross 2020-21 (Non-US only)", min_enable_speed=7.5), + ToyotaCarInfo("Toyota Corolla Cross (Non-US only) 2020-21", min_enable_speed=7.5), ToyotaCarInfo("Toyota Corolla Hatchback 2019-22", video_link="https://www.youtube.com/watch?v=_66pXk0CBYA"), ], CAR.COROLLAH_TSS2: [ ToyotaCarInfo("Toyota Corolla Hybrid 2020-22"), - ToyotaCarInfo("Toyota Corolla Cross Hybrid 2020-22 (Non-US only)", min_enable_speed=7.5), + ToyotaCarInfo("Toyota Corolla Cross Hybrid (Non-US only) 2020-22", min_enable_speed=7.5), ToyotaCarInfo("Lexus UX Hybrid 2019-22"), ], CAR.HIGHLANDER: ToyotaCarInfo("Toyota Highlander 2017-19", video_link="https://www.youtube.com/watch?v=0wS0wXSLzoo", footnotes=[Footnote.DSU]), @@ -159,7 +159,7 @@ class ToyotaCarInfo(CarInfo): CAR.LEXUS_NXH: ToyotaCarInfo("Lexus NX Hybrid 2018-19", footnotes=[Footnote.DSU]), CAR.LEXUS_NX_TSS2: ToyotaCarInfo("Lexus NX 2020-21"), CAR.LEXUS_NXH_TSS2: ToyotaCarInfo("Lexus NX Hybrid 2020-21"), - CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2017-2020"), + CAR.LEXUS_RC: ToyotaCarInfo("Lexus RC 2017-20"), CAR.LEXUS_RX: ToyotaCarInfo("Lexus RX 2016-18", footnotes=[Footnote.DSU]), CAR.LEXUS_RXH: ToyotaCarInfo("Lexus RX Hybrid 2016-19", footnotes=[Footnote.DSU]), CAR.LEXUS_RX_TSS2: ToyotaCarInfo("Lexus RX 2020-22"), From 777b090b4e41974f83a9de56c795e2560dba6de1 Mon Sep 17 00:00:00 2001 From: ridgewayer25 <76834151+ridgewayer25@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:16:09 -0400 Subject: [PATCH 364/436] Car docs: add video for 2016 RAV4 Hybrid (#25209) * Video for 16-18 rav4 hybrid https://youtu.be/LhT5VzJVfNI * Update selfdrive/car/toyota/values.py * Update selfdrive/car/toyota/values.py Co-authored-by: Shane Smiskol --- selfdrive/car/toyota/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 08ee9593c64b3e..2bbf951d78589b 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -141,7 +141,7 @@ class ToyotaCarInfo(CarInfo): ToyotaCarInfo("Toyota Prius Prime 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"), ], CAR.RAV4: ToyotaCarInfo("Toyota RAV4 2016-18", "TSS-P", footnotes=[Footnote.DSU]), - CAR.RAV4H: ToyotaCarInfo("Toyota RAV4 Hybrid 2016-18", "TSS-P", footnotes=[Footnote.DSU]), + CAR.RAV4H: ToyotaCarInfo("Toyota RAV4 Hybrid 2016-18", "TSS-P", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]), CAR.RAV4_TSS2: ToyotaCarInfo("Toyota RAV4 2019-21", video_link="https://www.youtube.com/watch?v=wJxjDd42gGA"), CAR.RAV4_TSS2_2022: ToyotaCarInfo("Toyota RAV4 2022"), CAR.RAV4H_TSS2: ToyotaCarInfo("Toyota RAV4 Hybrid 2019-21"), From 7351384f155a767d8db809e538946a54cb376a9b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 23:17:12 -0700 Subject: [PATCH 365/436] Hyundai: add missing 2022 Santa Fe FW versions (#25205) Add missing 2022 Santa Fe FW versions --- selfdrive/car/hyundai/values.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index ffa29c60d45a86..15675447450c78 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -617,6 +617,7 @@ class Buttons: b'\xf1\x82TACVN5GSI3XXXH0A', b'\xf1\x82TMCFD5MMCXXXXG0A', b'\xf1\x870\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf1\x82TMDWN5TMD3TXXJ1A', + b'\xf1\x81HM6M2_0a0_G00', ], (Ecu.eps, 0x7d4, None): [ b'\xf1\x00TM MDPS C 1.00 1.02 56370-S2AA0 0B19', @@ -635,6 +636,7 @@ class Buttons: b'\xf1\x87954A02N250\x00\x00\x00\x00\x00\xf1\x81T02730A1 \xf1\x00T02601BL T02730A1 VTMPT25XXX730NS2\xa6\x06\x88\xf7', b'\xf1\x87KMMYBU034207SB72x\x89\x88\x98h\x88\x98\x89\x87fhvvfWf33_\xff\x87\xff\x8f\xfa\x81\xe5\xf1\x89HT6TAF00A1\xf1\x82STM0M25GS1\x00\x00\x00\x00\x00\x00', b'\xf1\x87954A02N250\x00\x00\x00\x00\x00\xf1\x81T02730A1 \xf1\x00T02601BL T02730A1 VTMPT25XXX730NS2\xa6', + b'\xf1\x00HT6TA290BLHT6TAF00A1STM0M25GS1\x00\x00\x00\x00\x00\x006\xd8\x97\x15', ], }, CAR.SANTA_FE_HEV_2022: { From b5e3678245cfbe29041593742f94acc4f7e057cc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 23:17:29 -0700 Subject: [PATCH 366/436] Car info diff comment: don't fail if car info structure changes (#25193) Don't fail if car info structure changes --- .github/workflows/selfdrive_tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index fbc0d94194f5e0..7800323198ba94 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -553,7 +553,7 @@ jobs: id: save_diff run: | ${{ env.RUN }} "scons -j$(nproc)" - output=$(${{ env.RUN }} "python selfdrive/debug/print_docs_diff.py --path /tmp/openpilot_cache/base_car_info") + output=$(${{ env.RUN }} "python selfdrive/debug/print_docs_diff.py --path /tmp/openpilot_cache/base_car_info") || true output="${output//$'\n'/'%0A'}" echo "::set-output name=diff::$output" - name: Find comment From 5ea641b79aaa57115b71313eee4cb47b78394c28 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 18 Jul 2022 23:54:45 -0700 Subject: [PATCH 367/436] Highlander: merge params (#25210) * merge highlander params * weight is average of tss normal/hybrid and tss2 normal/hybrid --- selfdrive/car/toyota/interface.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 337c039565b5d7..21a5d47f62557c 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -93,20 +93,12 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl if candidate not in (CAR.CAMRY_TSS2, CAR.CAMRYH_TSS2): set_lat_tune(ret.lateralTuning, LatTunes.PID_C) - elif candidate in (CAR.HIGHLANDER_TSS2, CAR.HIGHLANDERH_TSS2): + elif candidate in (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.HIGHLANDER_TSS2, CAR.HIGHLANDERH_TSS2): stop_and_go = True - ret.wheelbase = 2.84988 # 112.2 in = 2.84988 m + ret.wheelbase = 2.8194 # average of 109.8 and 112.2 in ret.steerRatio = 16.0 tire_stiffness_factor = 0.8 - ret.mass = 4700. * CV.LB_TO_KG + STD_CARGO_KG # 4260 + 4-5 people - set_lat_tune(ret.lateralTuning, LatTunes.PID_G) - - elif candidate in (CAR.HIGHLANDER, CAR.HIGHLANDERH): - stop_and_go = True - ret.wheelbase = 2.78 - ret.steerRatio = 16.0 - tire_stiffness_factor = 0.8 - ret.mass = 4607. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid limited + ret.mass = 4516. * CV.LB_TO_KG + STD_CARGO_KG # mean between normal and hybrid set_lat_tune(ret.lateralTuning, LatTunes.PID_G) elif candidate in (CAR.AVALON, CAR.AVALON_2019, CAR.AVALONH_2019, CAR.AVALON_TSS2, CAR.AVALONH_TSS2): From 06acb1234f3df9fae833d5e99e4a07e8c95fd9bf Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 19 Jul 2022 15:56:13 +0200 Subject: [PATCH 368/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index a4c1afa3bfcbba..1c2cba75d66383 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit a4c1afa3bfcbba989c128ec9b5092f6c91f4da22 +Subproject commit 1c2cba75d66383fb0d0957f9cb2f6ffebb4f8915 From 86c1e8164ab6b1c3363f7833b7dc0ea735dd26db Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Tue, 19 Jul 2022 16:57:13 +0200 Subject: [PATCH 369/436] navd: add back VisionIPC map renderer (#25212) * builds standalone * process live msg * render into nv12a * VISION_STREAM_RGB_MAP -> VISION_STREAM_MAP * cleanup sconscript * main include cleanup --- SConstruct | 1 + release/files_common | 4 +- selfdrive/navd/.gitignore | 5 + selfdrive/navd/SConscript | 20 +++ selfdrive/navd/main.cc | 31 ++++ selfdrive/navd/map_renderer.cc | 236 ++++++++++++++++++++++++++++ selfdrive/navd/map_renderer.h | 53 +++++++ selfdrive/navd/map_renderer.py | 78 +++++++++ selfdrive/ui/qt/maps/map_helpers.cc | 42 +++++ selfdrive/ui/qt/maps/map_helpers.h | 1 + selfdrive/ui/watch3.cc | 1 + 11 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 selfdrive/navd/.gitignore create mode 100644 selfdrive/navd/SConscript create mode 100644 selfdrive/navd/main.cc create mode 100644 selfdrive/navd/map_renderer.cc create mode 100644 selfdrive/navd/map_renderer.h create mode 100755 selfdrive/navd/map_renderer.py diff --git a/SConstruct b/SConstruct index 1209894ba46d31..940b2367fa4b35 100644 --- a/SConstruct +++ b/SConstruct @@ -415,6 +415,7 @@ SConscript(['selfdrive/loggerd/SConscript']) SConscript(['selfdrive/locationd/SConscript']) SConscript(['selfdrive/sensord/SConscript']) SConscript(['selfdrive/ui/SConscript']) +SConscript(['selfdrive/navd/SConscript']) SConscript(['tools/replay/SConscript']) diff --git a/release/files_common b/release/files_common index 38f86d247a5ca9..411e4ff6dc324f 100644 --- a/release/files_common +++ b/release/files_common @@ -380,7 +380,9 @@ selfdrive/modeld/runners/run.h selfdrive/monitoring/dmonitoringd.py selfdrive/monitoring/driver_monitor.py -selfdrive/navd/*.py +selfdrive/navd/__init__.py +selfdrive/navd/navd.py +selfdrive/navd/helpers.py selfdrive/assets/.gitignore selfdrive/assets/assets.qrc diff --git a/selfdrive/navd/.gitignore b/selfdrive/navd/.gitignore new file mode 100644 index 00000000000000..a070fe32bb58dd --- /dev/null +++ b/selfdrive/navd/.gitignore @@ -0,0 +1,5 @@ +moc_* +*.moc + +map_renderer +libmap_renderer.so diff --git a/selfdrive/navd/SConscript b/selfdrive/navd/SConscript new file mode 100644 index 00000000000000..4fbe41e80b46b6 --- /dev/null +++ b/selfdrive/navd/SConscript @@ -0,0 +1,20 @@ +Import('qt_env', 'arch', 'common', 'messaging', 'visionipc', 'cereal', 'transformations') + +base_libs = [common, messaging, cereal, visionipc, transformations, 'zmq', + 'capnp', 'kj', 'm', 'OpenCL', 'ssl', 'crypto', 'pthread'] + qt_env["LIBS"] + +if arch == 'larch64': + base_libs.append('EGL') + +if arch in ['larch64', 'x86_64']: + if arch == 'x86_64': + rpath = [Dir(f"#third_party/mapbox-gl-native-qt/{arch}").srcnode().abspath] + qt_env["RPATH"] += rpath + + qt_libs = ["qt_widgets", "qt_util", "qmapboxgl"] + base_libs + + nav_src = ["main.cc", "map_renderer.cc"] + qt_env.Program("map_renderer", nav_src, LIBS=qt_libs + ['common', 'json11']) + + if GetOption('extras'): + qt_env.SharedLibrary("map_renderer", ["map_renderer.cc"], LIBS=qt_libs + ['common', 'messaging']) diff --git a/selfdrive/navd/main.cc b/selfdrive/navd/main.cc new file mode 100644 index 00000000000000..3a2fedb7d258d1 --- /dev/null +++ b/selfdrive/navd/main.cc @@ -0,0 +1,31 @@ +#include +#include +#include + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/qt/maps/map_helpers.h" +#include "selfdrive/navd/map_renderer.h" +#include "selfdrive/hardware/hw.h" + + + +void sigHandler(int s) { + qInfo() << "Shutting down"; + std::signal(s, SIG_DFL); + + qApp->quit(); +} + + +int main(int argc, char *argv[]) { + qInstallMessageHandler(swagLogMessageHandler); + + QApplication app(argc, argv); + std::signal(SIGINT, sigHandler); + std::signal(SIGTERM, sigHandler); + + MapRenderer * m = new MapRenderer(get_mapbox_settings()); + assert(m); + + return app.exec(); +} diff --git a/selfdrive/navd/map_renderer.cc b/selfdrive/navd/map_renderer.cc new file mode 100644 index 00000000000000..cb24f2087e4358 --- /dev/null +++ b/selfdrive/navd/map_renderer.cc @@ -0,0 +1,236 @@ +#include "selfdrive/navd/map_renderer.h" + +#include +#include +#include + +#include "common/timing.h" +#include "selfdrive/ui/qt/maps/map_helpers.h" + +const float ZOOM = 13.5; // Don't go below 13 or features will start to disappear +const int WIDTH = 256; +const int HEIGHT = WIDTH; + +const int NUM_VIPC_BUFFERS = 4; + +MapRenderer::MapRenderer(const QMapboxGLSettings &settings, bool online) : m_settings(settings) { + QSurfaceFormat fmt; + fmt.setRenderableType(QSurfaceFormat::OpenGLES); + + ctx = std::make_unique(); + ctx->setFormat(fmt); + ctx->create(); + assert(ctx->isValid()); + + surface = std::make_unique(); + surface->setFormat(ctx->format()); + surface->create(); + + ctx->makeCurrent(surface.get()); + assert(QOpenGLContext::currentContext() == ctx.get()); + + gl_functions.reset(ctx->functions()); + gl_functions->initializeOpenGLFunctions(); + + QOpenGLFramebufferObjectFormat fbo_format; + fbo.reset(new QOpenGLFramebufferObject(WIDTH, HEIGHT, fbo_format)); + + m_map.reset(new QMapboxGL(nullptr, m_settings, fbo->size(), 1)); + m_map->setCoordinateZoom(QMapbox::Coordinate(0, 0), ZOOM); + m_map->setStyleUrl("mapbox://styles/commaai/ckvmksrpd4n0a14pfdo5heqzr"); + m_map->createRenderer(); + + m_map->resize(fbo->size()); + m_map->setFramebufferObject(fbo->handle(), fbo->size()); + gl_functions->glViewport(0, 0, WIDTH, HEIGHT); + + if (online) { + vipc_server.reset(new VisionIpcServer("navd")); + vipc_server->create_buffers(VisionStreamType::VISION_STREAM_MAP, NUM_VIPC_BUFFERS, false, WIDTH, HEIGHT); + vipc_server->start_listener(); + + pm.reset(new PubMaster({"navThumbnail"})); + sm.reset(new SubMaster({"liveLocationKalman", "navRoute"})); + + timer = new QTimer(this); + QObject::connect(timer, SIGNAL(timeout()), this, SLOT(msgUpdate())); + timer->start(50); + } +} + +void MapRenderer::msgUpdate() { + sm->update(0); + + if (sm->updated("liveLocationKalman")) { + auto location = (*sm)["liveLocationKalman"].getLiveLocationKalman(); + auto pos = location.getPositionGeodetic(); + auto orientation = location.getCalibratedOrientationNED(); + + bool localizer_valid = (location.getStatus() == cereal::LiveLocationKalman::Status::VALID) && pos.getValid(); + if (localizer_valid) { + updatePosition(QMapbox::Coordinate(pos.getValue()[0], pos.getValue()[1]), RAD2DEG(orientation.getValue()[2])); + } + } + + if (sm->updated("navRoute")) { + QList route; + auto coords = (*sm)["navRoute"].getNavRoute().getCoordinates(); + for (auto const &c : coords) { + route.push_back(QGeoCoordinate(c.getLatitude(), c.getLongitude())); + } + updateRoute(route); + } +} + +void MapRenderer::updatePosition(QMapbox::Coordinate position, float bearing) { + if (m_map.isNull()) { + return; + } + + m_map->setCoordinate(position); + m_map->setBearing(bearing); + update(); +} + +bool MapRenderer::loaded() { + return m_map->isFullyLoaded(); +} + +void MapRenderer::update() { + gl_functions->glClear(GL_COLOR_BUFFER_BIT); + m_map->render(); + gl_functions->glFlush(); + + sendVipc(); +} + +void MapRenderer::sendVipc() { + if (!vipc_server || !loaded()) { + return; + } + + QImage cap = fbo->toImage().convertToFormat(QImage::Format_RGB888, Qt::AutoColor); + uint64_t ts = nanos_since_boot(); + VisionBuf* buf = vipc_server->get_buffer(VisionStreamType::VISION_STREAM_MAP); + VisionIpcBufExtra extra = { + .frame_id = frame_id, + .timestamp_sof = ts, + .timestamp_eof = ts, + }; + + assert(cap.sizeInBytes() >= buf->len); + uint8_t* dst = (uint8_t*)buf->addr; + uint8_t* src = cap.bits(); + + // RGB to greyscale + memset(dst, 128, buf->len); + for (int i = 0; i < WIDTH * HEIGHT; i++) { + dst[i] = src[i * 3]; + } + + vipc_server->send(buf, &extra); + + if (frame_id % 100 == 0) { + // Write jpeg into buffer + QByteArray buffer_bytes; + QBuffer buffer(&buffer_bytes); + buffer.open(QIODevice::WriteOnly); + cap.save(&buffer, "JPG", 50); + + kj::Array buffer_kj = kj::heapArray((const capnp::byte*)buffer_bytes.constData(), buffer_bytes.size()); + + // Send thumbnail + MessageBuilder msg; + auto thumbnaild = msg.initEvent().initNavThumbnail(); + thumbnaild.setFrameId(frame_id); + thumbnaild.setTimestampEof(ts); + thumbnaild.setThumbnail(buffer_kj); + pm->send("navThumbnail", msg); + } + + frame_id++; +} + +uint8_t* MapRenderer::getImage() { + QImage cap = fbo->toImage().convertToFormat(QImage::Format_RGB888, Qt::AutoColor); + uint8_t* buf = new uint8_t[cap.sizeInBytes()]; + memcpy(buf, cap.bits(), cap.sizeInBytes()); + + return buf; +} + +void MapRenderer::updateRoute(QList coordinates) { + if (m_map.isNull()) return; + initLayers(); + + auto route_points = coordinate_list_to_collection(coordinates); + QMapbox::Feature feature(QMapbox::Feature::LineStringType, route_points, {}, {}); + QVariantMap navSource; + navSource["type"] = "geojson"; + navSource["data"] = QVariant::fromValue(feature); + m_map->updateSource("navSource", navSource); + m_map->setLayoutProperty("navLayer", "visibility", "visible"); +} + +void MapRenderer::initLayers() { + if (!m_map->layerExists("navLayer")) { + QVariantMap nav; + nav["id"] = "navLayer"; + nav["type"] = "line"; + nav["source"] = "navSource"; + m_map->addLayer(nav, "road-intersection"); + m_map->setPaintProperty("navLayer", "line-color", QColor("grey")); + m_map->setPaintProperty("navLayer", "line-width", 3); + m_map->setLayoutProperty("navLayer", "line-cap", "round"); + } +} + +MapRenderer::~MapRenderer() { +} + +extern "C" { + MapRenderer* map_renderer_init() { + char *argv[] = { + (char*)"navd", + nullptr + }; + int argc = 0; + QApplication *app = new QApplication(argc, argv); + assert(app); + + QMapboxGLSettings settings; + settings.setApiBaseUrl(MAPS_HOST); + settings.setAccessToken(get_mapbox_token()); + + return new MapRenderer(settings, false); + } + + void map_renderer_update_position(MapRenderer *inst, float lat, float lon, float bearing) { + inst->updatePosition({lat, lon}, bearing); + QApplication::processEvents(); + } + + void map_renderer_update_route(MapRenderer *inst, char* polyline) { + inst->updateRoute(polyline_to_coordinate_list(QString::fromUtf8(polyline))); + } + + void map_renderer_update(MapRenderer *inst) { + inst->update(); + } + + void map_renderer_process(MapRenderer *inst) { + QApplication::processEvents(); + } + + bool map_renderer_loaded(MapRenderer *inst) { + return inst->loaded(); + } + + uint8_t * map_renderer_get_image(MapRenderer *inst) { + return inst->getImage(); + } + + void map_renderer_free_image(MapRenderer *inst, uint8_t * buf) { + delete[] buf; + } +} diff --git a/selfdrive/navd/map_renderer.h b/selfdrive/navd/map_renderer.h new file mode 100644 index 00000000000000..855dc918946f68 --- /dev/null +++ b/selfdrive/navd/map_renderer.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cereal/visionipc/visionipc_server.h" +#include "cereal/messaging/messaging.h" + + +class MapRenderer : public QObject { + Q_OBJECT + +public: + MapRenderer(const QMapboxGLSettings &, bool online=true); + uint8_t* getImage(); + void update(); + bool loaded(); + ~MapRenderer(); + + +private: + std::unique_ptr ctx; + std::unique_ptr surface; + std::unique_ptr gl_functions; + std::unique_ptr fbo; + + std::unique_ptr vipc_server; + std::unique_ptr pm; + std::unique_ptr sm; + void sendVipc(); + + QMapboxGLSettings m_settings; + QScopedPointer m_map; + + void initLayers(); + + uint32_t frame_id = 0; + + QTimer* timer; + +public slots: + void updatePosition(QMapbox::Coordinate position, float bearing); + void updateRoute(QList coordinates); + void msgUpdate(); +}; diff --git a/selfdrive/navd/map_renderer.py b/selfdrive/navd/map_renderer.py new file mode 100755 index 00000000000000..dc39f335c7945c --- /dev/null +++ b/selfdrive/navd/map_renderer.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# You might need to uninstall the PyQt5 pip package to avoid conflicts + +import os +import time +from cffi import FFI + +from common.ffi_wrapper import suffix +from common.basedir import BASEDIR + +HEIGHT = WIDTH = 256 + + +def get_ffi(): + lib = os.path.join(BASEDIR, "selfdrive", "navd", "libmap_renderer" + suffix()) + + ffi = FFI() + ffi.cdef(""" +void* map_renderer_init(); +void map_renderer_update_position(void *inst, float lat, float lon, float bearing); +void map_renderer_update_route(void *inst, char *polyline); +void map_renderer_update(void *inst); +void map_renderer_process(void *inst); +bool map_renderer_loaded(void *inst); +uint8_t* map_renderer_get_image(void *inst); +void map_renderer_free_image(void *inst, uint8_t *buf); +""") + return ffi, ffi.dlopen(lib) + + +def wait_ready(lib, renderer): + while not lib.map_renderer_loaded(renderer): + lib.map_renderer_update(renderer) + + # The main qt app is not execed, so we need to periodically process events for e.g. network requests + lib.map_renderer_process(renderer) + + time.sleep(0.01) + + +def get_image(lib, renderer): + buf = lib.map_renderer_get_image(renderer) + r = list(buf[0:3 * WIDTH * HEIGHT]) + lib.map_renderer_free_image(renderer, buf) + + # Convert to numpy + r = np.asarray(r) + return r.reshape((WIDTH, HEIGHT, 3)) + + +if __name__ == "__main__": + import matplotlib.pyplot as plt + import numpy as np + + ffi, lib = get_ffi() + renderer = lib.map_renderer_init() + wait_ready(lib, renderer) + + geometry = r"{yxk}@|obn~Eg@@eCFqc@J{RFw@?kA@gA?q|@Riu@NuJBgi@ZqVNcRBaPBkG@iSD{I@_H@cH?gG@mG@gG?aD@{LDgDDkVVyQLiGDgX@q_@@qI@qKhS{R~[}NtYaDbGoIvLwNfP_b@|f@oFnF_JxHel@bf@{JlIuxAlpAkNnLmZrWqFhFoh@jd@kX|TkJxH_RnPy^|[uKtHoZ~Um`DlkCorC``CuShQogCtwB_ThQcr@fk@sVrWgRhVmSb\\oj@jxA{Qvg@u]tbAyHzSos@xjBeKbWszAbgEc~@~jCuTrl@cYfo@mRn\\_m@v}@ij@jp@om@lk@y|A`pAiXbVmWzUod@xj@wNlTw}@|uAwSn\\kRfYqOdS_IdJuK`KmKvJoOhLuLbHaMzGwO~GoOzFiSrEsOhD}PhCqw@vJmnAxSczA`Vyb@bHk[fFgl@pJeoDdl@}}@zIyr@hG}X`BmUdBcM^aRR}Oe@iZc@mR_@{FScHxAn_@vz@zCzH~GjPxAhDlB~DhEdJlIbMhFfG|F~GlHrGjNjItLnGvQ~EhLnBfOn@p`@AzAAvn@CfC?fc@`@lUrArStCfSxEtSzGxM|ElFlBrOzJlEbDnC~BfDtCnHjHlLvMdTnZzHpObOf^pKla@~G|a@dErg@rCbj@zArYlj@ttJ~AfZh@r]LzYg@`TkDbj@gIdv@oE|i@kKzhA{CdNsEfOiGlPsEvMiDpLgBpHyB`MkB|MmArPg@|N?|P^rUvFz~AWpOCdAkB|PuB`KeFfHkCfGy@tAqC~AsBPkDs@uAiAcJwMe@s@eKkPMoXQux@EuuCoH?eI?Kas@}Dy@wAUkMOgDL" + lib.map_renderer_update_route(renderer, geometry.encode()) + + POSITIONS = [ + (32.71569271952601, -117.16384270868463, 0), (32.71569271952601, -117.16384270868463, 45), # San Diego + (52.378641991483136, 4.902623379456488, 0), (52.378641991483136, 4.902623379456488, 45), # Amsterdam + ] + plt.figure() + + for i, pos in enumerate(POSITIONS): + t = time.time() + lib.map_renderer_update_position(renderer, *pos) + wait_ready(lib, renderer) + + print(f"{pos} took {time.time() - t:.2f} s") + + plt.subplot(2, 2, i + 1) + plt.imshow(get_image(lib, renderer)) + + plt.show() diff --git a/selfdrive/ui/qt/maps/map_helpers.cc b/selfdrive/ui/qt/maps/map_helpers.cc index 66acb7a25d9a39..f97137a7f90d9d 100644 --- a/selfdrive/ui/qt/maps/map_helpers.cc +++ b/selfdrive/ui/qt/maps/map_helpers.cc @@ -101,6 +101,48 @@ QMapbox::CoordinatesCollections coordinate_list_to_collection(QList polyline_to_coordinate_list(const QString &polylineString) { + QList path; + if (polylineString.isEmpty()) + return path; + + QByteArray data = polylineString.toLatin1(); + + bool parsingLatitude = true; + + int shift = 0; + int value = 0; + + QGeoCoordinate coord(0, 0); + + for (int i = 0; i < data.length(); ++i) { + unsigned char c = data.at(i) - 63; + + value |= (c & 0x1f) << shift; + shift += 5; + + // another chunk + if (c & 0x20) + continue; + + int diff = (value & 1) ? ~(value >> 1) : (value >> 1); + + if (parsingLatitude) { + coord.setLatitude(coord.latitude() + (double)diff/1e6); + } else { + coord.setLongitude(coord.longitude() + (double)diff/1e6); + path.append(coord); + } + + parsingLatitude = !parsingLatitude; + + value = 0; + shift = 0; + } + + return path; +} + std::optional coordinate_from_param(std::string param) { QString json_str = QString::fromStdString(Params().get(param)); if (json_str.isEmpty()) return {}; diff --git a/selfdrive/ui/qt/maps/map_helpers.h b/selfdrive/ui/qt/maps/map_helpers.h index 1c08c541c3d77a..2e1402cceacf14 100644 --- a/selfdrive/ui/qt/maps/map_helpers.h +++ b/selfdrive/ui/qt/maps/map_helpers.h @@ -24,6 +24,7 @@ QMapbox::CoordinatesCollections model_to_collection( QMapbox::CoordinatesCollections coordinate_to_collection(QMapbox::Coordinate c); QMapbox::CoordinatesCollections capnp_coordinate_list_to_collection(const capnp::List::Reader &coordinate_list); QMapbox::CoordinatesCollections coordinate_list_to_collection(QList coordinate_list); +QList polyline_to_coordinate_list(const QString &polylineString); std::optional coordinate_from_param(std::string param); double angle_difference(double angle1, double angle2); diff --git a/selfdrive/ui/watch3.cc b/selfdrive/ui/watch3.cc index 00d23ea97665b4..d6b5cc67a7b9a3 100644 --- a/selfdrive/ui/watch3.cc +++ b/selfdrive/ui/watch3.cc @@ -19,6 +19,7 @@ int main(int argc, char *argv[]) { { QHBoxLayout *hlayout = new QHBoxLayout(); layout->addLayout(hlayout); + hlayout->addWidget(new CameraViewWidget("navd", VISION_STREAM_MAP, false)); hlayout->addWidget(new CameraViewWidget("camerad", VISION_STREAM_ROAD, false)); } From 8553893e73c605dac86df3862caf840eb7985270 Mon Sep 17 00:00:00 2001 From: bnoeldner <108907208+bnoeldner@users.noreply.github.com> Date: Tue, 19 Jul 2022 12:43:23 -0500 Subject: [PATCH 370/436] Kia Niro PHEV 2018: add missing FW versions (#25187) * Kia: Niro PHEV 2018 fingerprint * Correct formatting * update years Co-authored-by: Shane Smiskol --- docs/CARS.md | 2 +- selfdrive/car/hyundai/values.py | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 834e8e402fe7bf..f584890b0f64ff 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -100,7 +100,7 @@ How We Rate The Cars |Kia|Niro Electric 2022|All||||| |Kia|Niro Hybrid 2021|SCC + LKAS||||| |Kia|Niro Hybrid 2022|SCC + LKAS||||| -|Kia|Niro Plug-in Hybrid 2019|SCC + LKAS||||| +|Kia|Niro Plug-in Hybrid 2018-19|SCC + LKAS||||| |Kia|Optima 2017|SCC + LKAS||||| |Kia|Optima 2019|SCC + LKAS||||| |Kia|Seltos 2021|SCC + LKAS||||| diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 15675447450c78..303558b5a136ec 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -136,7 +136,7 @@ class HyundaiCarInfo(CarInfo): HyundaiCarInfo("Kia Niro Electric 2021", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_c), HyundaiCarInfo("Kia Niro Electric 2022", "All", video_link="https://www.youtube.com/watch?v=lT7zcG6ZpGo", harness=Harness.hyundai_h), ], - CAR.KIA_NIRO_HEV: HyundaiCarInfo("Kia Niro Plug-in Hybrid 2019", min_enable_speed=10. * CV.MPH_TO_MS, harness=Harness.hyundai_c), + CAR.KIA_NIRO_HEV: HyundaiCarInfo("Kia Niro Plug-in Hybrid 2018-19", min_enable_speed=10. * CV.MPH_TO_MS, harness=Harness.hyundai_c), CAR.KIA_NIRO_HEV_2021: [ HyundaiCarInfo("Kia Niro Hybrid 2021", harness=Harness.hyundai_f), # TODO: could be hyundai_d, verify HyundaiCarInfo("Kia Niro Hybrid 2022", harness=Harness.hyundai_h), @@ -986,20 +986,24 @@ class Buttons: }, CAR.KIA_NIRO_HEV: { (Ecu.engine, 0x7e0, None): [ - b'\xf1\x816H6F4051\000\000\000\000\000\000\000\000', + b'\xf1\x816H6F4051\x00\x00\x00\x00\x00\x00\x00\x00', + b'\xf1\x816H6D1051\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.transmission, 0x7e1, None): [ - b"\xf1\x816U3J2051\000\000\xf1\0006U3H0_C2\000\0006U3J2051\000\000PDE0G16NS2\xf4\'\\\x91", - b'\xf1\x816U3J2051\000\000\xf1\0006U3H0_C2\000\0006U3J2051\000\000PDE0G16NS2\000\000\000\000', + b"\xf1\x816U3J2051\x00\x00\xf1\x006U3H0_C2\x00\x006U3J2051\x00\x00PDE0G16NS2\xf4'\\\x91", + b'\xf1\x816U3J2051\x00\x00\xf1\x006U3H0_C2\x00\x006U3J2051\x00\x00PDE0G16NS2\x00\x00\x00\x00', + b'\xf1\x816U3H3051\x00\x00\xf1\x006U3H0_C2\x00\x006U3H3051\x00\x00PDE0G16NS1\x00\x00\x00\x00', + b'\xf1\x816U3H3051\x00\x00\xf1\x006U3H0_C2\x00\x006U3H3051\x00\x00PDE0G16NS1\x13\xcd\x88\x92', ], (Ecu.eps, 0x7D4, None): [ - b'\xf1\000DE MDPS C 1.00 1.09 56310G5301\000 4DEHC109', + b'\xf1\x00DE MDPS C 1.00 1.09 56310G5301\x00 4DEHC109', ], (Ecu.fwdCamera, 0x7C4, None): [ - b'\xf1\000DEP MFC AT USA LHD 1.00 1.01 95740-G5010 170424', + b'\xf1\x00DEP MFC AT USA LHD 1.00 1.01 95740-G5010 170424', + b'\xf1\x00DEP MFC AT USA LHD 1.00 1.00 95740-G5010 170117', ], (Ecu.fwdRadar, 0x7D0, None): [ - b'\xf1\000DEhe SCC H-CUP 1.01 1.02 96400-G5100 ', + b'\xf1\x00DEhe SCC H-CUP 1.01 1.02 96400-G5100 ', ], }, CAR.KIA_NIRO_HEV_2021: { From 50f7545ed7ef008526b6b081bbc2f96e05fc54e1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 19 Jul 2022 10:45:39 -0700 Subject: [PATCH 371/436] UI: add description for branch switcher --- selfdrive/ui/qt/offroad/settings.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 65b9e3e4cc9964..0956f6c8dde523 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -256,9 +256,10 @@ SoftwarePanel::SoftwarePanel(QWidget* parent) : ListWidget(parent) { }); connect(uiState(), &UIState::offroadTransition, updateBtn, &QPushButton::setEnabled); - branchSwitcherBtn = new ButtonControl(tr("Switch Branch"), tr("ENTER")); + branchSwitcherBtn = new ButtonControl(tr("Switch Branch"), tr("ENTER"), tr("The new branch will be pulled the next time the updater runs.")); connect(branchSwitcherBtn, &ButtonControl::clicked, [=]() { - QString branch = InputDialog::getText(tr("Enter name of new branch"), this); + QString branch = InputDialog::getText(tr("Enter branch name"), this, tr("The new branch will be pulled the next time the updater runs."), + false, -1, QString::fromStdString(params.get("SwitchToBranch"))); if (branch.isEmpty()) { params.remove("SwitchToBranch"); } else { From 30f21cb8fd2cae562fa7f2f5486d360366b6e938 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 19 Jul 2022 12:34:06 -0700 Subject: [PATCH 372/436] Update translations --- selfdrive/ui/translations/main_ko.ts | 34 ++++++++++++++---------- selfdrive/ui/translations/main_zh-CHS.ts | 34 ++++++++++++++---------- selfdrive/ui/translations/main_zh-CHT.ts | 34 ++++++++++++++---------- 3 files changed, 60 insertions(+), 42 deletions(-) diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 4662a862bc0177..ec8f3a52d6abc3 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -710,33 +710,33 @@ location set SettingsWindow - + × × - + Device 장치 - - + + Network 네트워크 - + Toggles 토글 - + Software 소프트웨어 - + Navigation 네비게이션 @@ -1025,33 +1025,39 @@ location set + - Enter name of new branch + The new branch will be pulled the next time the updater runs. - + + Enter branch name + + + + UNINSTALL 제거 - + Uninstall %1 제거 %1 - + Are you sure you want to uninstall? 제거하시겠습니까? - + failed to fetch update 업데이트를 가져올수없습니다 - - + + CHECK 확인 diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 744cf90345f3cf..9485e9449add74 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -708,33 +708,33 @@ location set SettingsWindow - + × × - + Device 设备 - - + + Network 网络 - + Toggles 设定 - + Software 软件 - + Navigation 导航 @@ -1023,33 +1023,39 @@ location set + - Enter name of new branch + The new branch will be pulled the next time the updater runs. - + + Enter branch name + + + + UNINSTALL 卸载 - + Uninstall %1 卸载 %1 - + Are you sure you want to uninstall? 您确定要卸载吗? - + failed to fetch update 获取更新失败 - - + + CHECK 查看 diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 30fd666edd86f3..9a3b800417a451 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -713,33 +713,33 @@ location set SettingsWindow - + × × - + Device 設備 - - + + Network 網路 - + Toggles 設定 - + Software 軟體 - + Navigation 導航 @@ -1028,33 +1028,39 @@ location set + - Enter name of new branch + The new branch will be pulled the next time the updater runs. - + + Enter branch name + + + + UNINSTALL 卸載 - + Uninstall %1 卸載 %1 - + Are you sure you want to uninstall? 您確定您要卸載嗎? - + failed to fetch update 下載更新失敗 - - + + CHECK 檢查 From 464db126a551639b036c4a5e4e494684c68f56f2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 19 Jul 2022 12:38:58 -0700 Subject: [PATCH 373/436] Multilanguage: readme improvements (#25214) * catch2 needs to iterate through the whole function each time to test this (spams warnings) * Update readme * Add update the ui header * build in scons * release by default * scons in another pr Co-authored-by: Vincent Wright --- selfdrive/ui/tests/test_translations.cc | 30 +++++++++++-------------- selfdrive/ui/tests/test_translations.py | 11 +++++---- selfdrive/ui/translations/README.md | 21 ++++++++++++----- selfdrive/ui/update_translations.py | 10 ++++----- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/selfdrive/ui/tests/test_translations.cc b/selfdrive/ui/tests/test_translations.cc index ba0612b4c0fb56..fcefc5784fee7a 100644 --- a/selfdrive/ui/tests/test_translations.cc +++ b/selfdrive/ui/tests/test_translations.cc @@ -17,25 +17,21 @@ QStringList getParentWidgets(QWidget* widget){ template void checkWidgetTrWrap(MainWindow &w) { - int i = 0; for (auto widget : w.findChildren()) { - const QString text = widget->text(); - SECTION(text.toStdString() + "-" + std::to_string(i)) { - bool isNumber = RE_NUM.exactMatch(text); - bool wrapped = text.contains(TEST_TEXT); - QString parentWidgets = getParentWidgets(widget).join("->"); - - if (!text.isEmpty() && !isNumber && !wrapped) { - FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString()); - } - - // warn if source string wrapped, but UI adds text - // TODO: add way to ignore this - if (wrapped && text != TEST_TEXT) { - WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString()); - } + const QString text = widget->text(); + bool isNumber = RE_NUM.exactMatch(text); + bool wrapped = text.contains(TEST_TEXT); + QString parentWidgets = getParentWidgets(widget).join("->"); + + if (!text.isEmpty() && !isNumber && !wrapped) { + FAIL(("\"" + text + "\" must be wrapped. Parent widgets: " + parentWidgets).toStdString()); + } + + // warn if source string wrapped, but UI adds text + // TODO: add way to ignore this + if (wrapped && text != TEST_TEXT) { + WARN(("\"" + text + "\" is dynamic and needs a custom retranslate function. Parent widgets: " + parentWidgets).toStdString()); } - i++; } } diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index d5409dd4162908..59751eec19493a 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -35,13 +35,12 @@ def test_missing_translation_files(self): if not len(file): self.skipTest(f"{name} translation has no defined file") - self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")), - f"{name} has no XML translation file, run selfdrive/ui/update_translations.py") - self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.qm")), - f"{name} has no compiled QM translation file, run selfdrive/ui/update_translations.py --release") + if not (os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")) and + os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.qm"))): + self.fail(f"{name} is missing translation files, run selfdrive/ui/update_translations.py") def test_translations_updated(self): - update_translations(release=True, translations_dir=TMP_TRANSLATIONS_DIR) + update_translations(translations_dir=TMP_TRANSLATIONS_DIR) for name, file in self.translation_files.items(): with self.subTest(name=name, file=file): @@ -59,7 +58,7 @@ def test_translations_updated(self): new_translations = self._read_translation_file(TMP_TRANSLATIONS_DIR, file, file_ext) self.assertEqual(cur_translations, new_translations, - f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py --release to update the translation files") + f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") @unittest.skip("Only test unfinished translations before going to release") def test_unfinished_translations(self): diff --git a/selfdrive/ui/translations/README.md b/selfdrive/ui/translations/README.md index 0ddb03a24c6b77..4038b1e0ce0bec 100644 --- a/selfdrive/ui/translations/README.md +++ b/selfdrive/ui/translations/README.md @@ -1,7 +1,5 @@ # Multilanguage -![multilanguage_onroad](https://user-images.githubusercontent.com/25857203/178912800-2c798af8-78e3-498e-9e19-35906e0bafff.png) - ## Contributing Before getting started, make sure you have set up the openpilot Ubuntu development environment by reading the [tools README.md](/tools/README.md). @@ -28,12 +26,25 @@ openpilot provides a few tools to help contributors manage their translations an Follow the steps above, omitting steps 1. and 2. Any time you edit translations you'll want to make sure to compile them. +### Updating the UI + +Any time you edit source code in the UI, you need to update and compile the translations to ensure the line numbers and contexts are up to date (last step above). + ### Testing -openpilot has a unit test to make sure all translations are up to date with the text in openpilot and that all translations are completed. +openpilot has a few unit tests to make sure all translations are up to date and that all strings are wrapped in a translation marker. -Run and fix any issues: +Tests translation files up to date: -```python +```shell selfdrive/ui/tests/test_translations.py ``` + +Tests all static source strings are wrapped: + +```shell +selfdrive/ui/tests/create_test_translations.sh && selfdrive/ui/tests/test_translations +``` + +--- +![multilanguage_onroad](https://user-images.githubusercontent.com/25857203/178912800-2c798af8-78e3-498e-9e19-35906e0bafff.png) diff --git a/selfdrive/ui/update_translations.py b/selfdrive/ui/update_translations.py index f06d54b2d50a31..5ccae0eb02ac9f 100755 --- a/selfdrive/ui/update_translations.py +++ b/selfdrive/ui/update_translations.py @@ -10,7 +10,7 @@ LANGUAGES_FILE = os.path.join(TRANSLATIONS_DIR, "languages.json") -def update_translations(release=False, vanish=False, translations_dir=TRANSLATIONS_DIR): +def update_translations(vanish=False, translations_dir=TRANSLATIONS_DIR): with open(LANGUAGES_FILE, "r") as f: translation_files = json.load(f) @@ -26,16 +26,14 @@ def update_translations(release=False, vanish=False, translations_dir=TRANSLATIO ret = os.system(args) assert ret == 0 - if release: - ret = os.system(f"lrelease {tr_file}") - assert ret == 0 + ret = os.system(f"lrelease {tr_file}") + assert ret == 0 if __name__ == "__main__": parser = argparse.ArgumentParser(description="Update translation files for UI", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--release", action="store_true", help="Create compiled QM translation files used by UI") parser.add_argument("--vanish", action="store_true", help="Remove translations with source text no longer found") args = parser.parse_args() - update_translations(args.release, args.vanish) + update_translations(args.vanish) From 38e8abed1e640a8a01d08d0ca4b593d4fdbcf3d0 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Tue, 19 Jul 2022 14:48:05 -0500 Subject: [PATCH 374/436] VW MQB: Add FW for 2016 Volkswagen Golf (#25213) --- selfdrive/car/volkswagen/values.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 8f38a00d4d84e2..91297843507209 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -266,6 +266,7 @@ class VWCarInfo(CarInfo): b'\xf1\x8704E906023BN\xf1\x894518', b'\xf1\x8704E906024K \xf1\x896811', b'\xf1\x8704E906027GR\xf1\x892394', + b'\xf1\x8704E906027HD\xf1\x892603', b'\xf1\x8704E906027HD\xf1\x893742', b'\xf1\x8704E906027MA\xf1\x894958', b'\xf1\x8704L906021DT\xf1\x895520', @@ -303,6 +304,7 @@ class VWCarInfo(CarInfo): (Ecu.transmission, 0x7e1, None): [ b'\xf1\x8709G927749AP\xf1\x892943', b'\xf1\x8709S927158A \xf1\x893585', + b'\xf1\x870CW300040H \xf1\x890606', b'\xf1\x870CW300041H \xf1\x891010', b'\xf1\x870CW300042F \xf1\x891604', b'\xf1\x870CW300043B \xf1\x891601', @@ -379,6 +381,7 @@ class VWCarInfo(CarInfo): b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0521A00502A0', b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\00511A00403A0', b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\00516A00604A1', + b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00404A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\00516A00604A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\00516A07A02A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\00521A00507A1', From 2de0af2aa5ae88c4bb5db50d713400bce096f29a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 19 Jul 2022 13:37:03 -0700 Subject: [PATCH 375/436] jenkins: patch sshd config in continue.sh --- selfdrive/test/setup_device_ci.sh | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index 2e5ffeacc66cca..bf2f93e1c35aef 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -21,28 +21,26 @@ umount /data/safe_staging/merged/ || true sudo umount /data/safe_staging/merged/ || true rm -rf /data/safe_staging/* || true -export KEYS_PARAM_PATH="/data/params/d/GithubSshKeys" -export KEYS_PATH="/usr/comma/setup_keys" -export CONTINUE_PATH="/data/continue.sh" - -if ! grep -F "$KEYS_PATH" /etc/ssh/sshd_config; then - echo "setting up keys" - sudo mount -o rw,remount / - sudo systemctl enable ssh - sudo sed -i "s,$KEYS_PARAM_PATH,$KEYS_PATH," /etc/ssh/sshd_config - sudo mount -o ro,remount / -fi - +CONTINUE_PATH="/data/continue.sh" tee $CONTINUE_PATH << EOF #!/usr/bin/bash sudo abctl --set_success +# patch sshd config +sudo mount -o rw,remount / +sudo sed -i "s,/data/params/d/GithubSshKeys,/usr/comma/setup_keys," /etc/ssh/sshd_config +sudo systemctl daemon-reload +sudo systemctl restart ssh +sudo systemctl disable ssh-param-watcher.path +sudo systemctl disable ssh-param-watcher.service +sudo mount -o ro,remount / + while true; do if ! sudo systemctl is-active -q ssh; then sudo systemctl start ssh fi - sleep 10s + sleep 5s done sleep infinity From 26dad2423bd4078aab249ea9db3301a2a8a6c73e Mon Sep 17 00:00:00 2001 From: cydia2020 <12470297+cydia2020@users.noreply.github.com> Date: Wed, 20 Jul 2022 08:56:06 +1000 Subject: [PATCH 376/436] Multilang: Improve Japanese translations (#25108) * improve japanese translation * characteristics is probably the better word to use * more explicit * misc fix * space * translate Dongle ID * drive -> driving history * more appropriate translation for minimum character requirement This is hacky and should be fixed * copy the home work of the chinese translators * full width -> half width * better speed translations * device pairing procedures * main features -> perks * subscribed -> joined (as member) * stop update -> pause update * add japanese to language.json * qm * honorific * shorter texts for buttons * close -> recent * maximising driver monitoring performance -> improving driver monitoring performance * developer perks -> developer facing perks * expressions * expressions * full width exclamation mark * accel pedal to cancel -> tap accel to cancel openpilot * qm * deal with unfinished translations * qm * change all mentions of driver camera to interior camera * failed to obtain update -> error while downloading update * update translation file * update qm * remove google translate * this device is facing %2's %1 deg, %4's %3 deg * ja qm * reword this * ensure -> confirm * better wording for device reset page * more consistent * wording * qm * mark as finished * update after translation refactor * bump translation * update translation to reflect new changes * Japanese qm * update translations * remove from selector Co-authored-by: Shane Smiskol --- selfdrive/ui/translations/main_ja.qm | Bin 19159 -> 20204 bytes selfdrive/ui/translations/main_ja.ts | 538 ++++++++++++++++----------- 2 files changed, 318 insertions(+), 220 deletions(-) diff --git a/selfdrive/ui/translations/main_ja.qm b/selfdrive/ui/translations/main_ja.qm index 552545cf879ed5c9a4009e63d334dee10c479e33..1c98d5a1e38da73116211e91d817d4c4bbf65293 100644 GIT binary patch delta 5988 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 delta 5251 zcmcIn30TzS);}}MFe@|cAdCEm#SsO0QQTNmP*E1y4T?xbhGArwAskTjDq2{IYrLW5 z(lyh(MTS>O(=5G7u9XyTXpj{JR|LAlIk?UF_>3yO;?-IF~A&){%gS-j3f~c=I zkqqm)9YpdNqNoH&{Gys5i#zv-GBkilu^an*iM0EO!m@})Hxk9ZM3g=sIQVYJ%*V%J zqTE;_Wgo~2BJX>U^8kp04vc+jNcQ3?BD&fsT{=2tSZ$|FK1Q-vE)YqANY)Vtythbp z_yfqBBs(G_>ghr9CV?nAg5;Yv5^3&}>((<+%18|gp$Hd@B}Lz_Ffopp9#S)o#vGVQ z)Q@P)_wN%8JWpd)r9>VAr4MR>A~0YYnCe?X*;WnGgx3 z6OD|bEskf1BHyMCuWX{=<8-m97Qpcm-=R@N;RzD|*TRX6fs%ns;G+HklH9$Kn7=Jq zc?|Oj8p-DcGT=^>oRq#n6!etj@_6;zEa`9E7EO?pxA(N>BnhHh}?apU*-ZhI8u800DK(dCOx~>2Y}06g0IQ3|C&qa z!r?Gzl}l|d@PEf>)*PH)?XqJF_Qxi>bQovD|1L6@Bc8Y6yJnX=wHrWe zv~2X{KNERPl1<#$4uuZLENPf_KO?I@H;hPdk-aFpMKolh?3J{g2(~V%f3tHO(o?p2 z{T;ZjMYirj8RQqTcaF3m7)Q$9t6UF8C(5>KMc0^QhrT;S4HViYJ9Ono_;#%9-0=5^ z6kFx$L7;rZJ94*$&l1INl#kR!z=C(=MHAtY!9U8)>W>jCQ{_{_p>Y39`4lmz)CNcx zqRf}qmruw3U*$97bcp&D@`cNxh&sDd_8izLL#jmCfFM{YU%eE;pgAI6|H3?2{118C zzOzK^sQi=rpjx_3zQ^zdk-kQL>NFGyy(mBb`~mp5rBf#VxBTvXC>Ar{Rca^z-Y2fQ z>-~sAxNG9&R2bC5baz*M8+_7yPWa!v7b!X^0JPdPHO1FvC_ zGTjA=yXPxSKl}`ZRm#P+D~STvDF4_BJg*4l`xnD7E>$VqCLo3;tHK&qEAhNqRmnHa zI51E(KH~y3wy2(X9@LHeOjZ0esO}}G%J!x}v0$t6p$981KFWGKnZ+wtC?uJ;r;~FaH7;N3^P6+4%+(YE?I^J~uHH2NWl;FDdIyu^9jn!!m~l{EqwY}mM3A0Rclahkp=;{zCp$o4mPWF5 zHujfk!gha(pxmY@Ifr}@7NmLhOBob?Q!}^D8w~ns_FjXFjP;uRft{21>4T91LXi~ z&Nbz&;_s?b2z(Yyz9hDw&=C zqIqF(A_;6CI|`ib%-)GWo1L;hRw|rd1G;A7SaW^6^X5_feQmCvV2+xQ&nlPyY+D9& z1)B_=o_o&D4Q`npzMX5iix*GJJBBl}d86BCV^rrX+r-X;!Q<>2^xnoU!G>mxKV*Ag ziMaP`V+=Ry3?boN`}aF{9uhfc??6}ZxL_gnP0|(l1$5uvW!ds`b7NT{AFm(k7tnQo zx7A&1yYz$fI-rPnB5!FliU+v&*0^KtGVqVGdwh(0u|~~K;N~ycMT|GPKiRPS;W}$& z-(s)r;SO**pEWT_H=nlrx1t3)Q?FDdn2axlxlY|DS<<2?+L7x^@g@R$mdwOX)OJ1kX}87863RA-rN z5~}RAHiyM#7OL%2Ee?y_CY0N2gi_N~2mf}2m)bPF+EimP*(RI#XCBeY2aI!xXShn) zZD$|PTn~4Q-i6iO{^5gRdd@vd!#x;wGzXiU!4S^Mp?q%w?G@1&(l%C!!?TXT3UWWnCkytT0qgX5-l+ z1nMX}1RFCmNofWG)ck;qZVWq{$toCUmi7dnbcwRdS;zTe-+c%Rr}M?o=9PZ^y_6(0 z`RCvMRf2MaLV}-`2lyL!uihbir=Q0-Pcvm_Ezfc4HU){rMnd;aU6#pICQNb&R@0=Z z4ncv#nlxK=tz(pBlGSdOxM0E)A=&-Rd7eM>@n>)2`Fy}OvNn+3oPQ$w+pK8b<{zi` zge4!dtL*rK^VS)dyX!x|!b5_g6%eudwwUNWaxA;bepEmVr)vq z*xY%naef7!dS%Nxk&lU=7R9pW2`j)@J~+t)TjRPyQ{Zy~(gUV;h7j6SGnO4hG;Dd0Ab3&m z0DXX&iaX$0TiuU!jfISRB>C_^2S@U~Ju5T;50flU3Q6pthIf(Z@VG;}xt5F1f&VeY zM=r}7v^^U_6fQqoo3Nr3N&7hTYJ=17u{IuW==JX@5~&ki{|I#tLQCRM3(h}@cB|m1uQnwa zV~xU8N4*taRg-GW7F)E{RPKluVh2`FPcRPa+Aau*R?Bbqxks1U9S(a{yfBbCxAOMT zsF=hUtL1+<^e<<8Lx=pI&rC6l^nLW6#2CA^^G^OvsIR7HcVVmGy0E9@I*NV9!R=v* zl6by0PzXT3AXY=Mex5~byd_pq&^v`K|w-?g7GPXB9cXl@U>>((OPx!%hSB7TeFqMLgd<*L4@a6wyzmGYG68<^Wr%Uao4ZZ(E^B6HruCAW|-)}ysU%fVkahCgUn@`BAF z<a#{cGc9$%Jy8-H!h3EIORdrSYuzLGnX#tVb!aId$9%$PQK^COVdxm%hBrS!g;d zxZtdux9|>;f=hSeTt#PR!c~1X>A;3jFL~KVBCDD4jN0fRN9`BuukF?$G(GtTiLw=Hz7!DgoR)}H_@Bx zld>f@_=Kb?%_B$nsieV=j@u2xyro`ZMt!Vn=IJ8>{Bs=4wk%`Gxo4um(!-|B#;1?i z*u%do!Cqy5N2np;E;tX@qiXV;4%R-)c6ODBhfGcp?j^~g~l^DlRMSFYlYN;mt zAtWytwL19ehvTjuGlMNajO~MYyZDXK1~(h7yvp7~LfwIw?wz_=6NUeOj00p2jcflT zwM?o>b^3HlQCxwN_e=H*u|K9|^AbIq!z!G*q95XxG^?by*emJ9CU#-rb9FzNwikFX z8^4j3;#KoI#EePLac9lX8W1{_ot-Kfcx(DH`E1Ijd|s3h&Sz(2$YbHD6#mzYuO17{ z@b(j#C6d9u591XNOnfw8P6&RGR>?QjUSl>nghESnszt~$Ii}fbDuqzKG^^yFkmf=* diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index f3b8733128cfcd..dd7643e21788a5 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -11,7 +11,7 @@ Snooze Update - 更新停止 + 更新の一時停止 @@ -24,17 +24,17 @@ Back - もどる + 戻る Enable Tethering - テザリングを有効化 + テザリングを有効化 Tethering Password - テザリングパスワード + テザリングパスワード @@ -45,17 +45,17 @@ Enter new tethering password - 新しいテザリングパスワードを入力 + 新しいテザリングパスワードを入力してください IP Address - IPアドレス + IP アドレス Enable Roaming - ローミングを有効化 + ローミングを有効化 @@ -65,24 +65,24 @@ Enter APN - APN 入力 + APN を入力 leave blank for automatic configuration - 空欄で自動設定 + 空白のままにして、自動設定にします ConfirmationDialog - - + + Ok OK - + Cancel キャンセル @@ -92,7 +92,7 @@ You must accept the Terms and Conditions in order to use openpilot. - openpilotを利用するためには、利用規約に同意する必要があります。 + openpilot をご利用される前に、利用規約に同意する必要があります。 @@ -102,157 +102,160 @@ Decline, uninstall %1 - 拒否してアンインストール %1 + 拒否して %1 をアンインストール DevicePanel - + Dongle ID - ドングル ID + ドングル番号 (Dongle ID) - + N/A N/A - + Serial - シリアル + シリアル番号 - + Driver Camera - ドライバーカメラ + 車内カメラ - + PREVIEW - プレビュー + 見る - + Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - ドライバー向けカメラをプレビューする、ドライバーモニタリングの視認性を確保します。(車両の電源を切る必要があります) + 車内カメラをプレビューして、ドライバー監視システムの視界を確認ができます。(車両の電源を切る必要があります) - + Reset Calibration - キャリブレーションリセット + キャリブレーションをリセット - + RESET - リセット + リセット - + Are you sure you want to reset calibration? - 本当にキャリブレーションをリセットしますか? + キャリブレーションをリセットしてもよろしいですか? - + Review Training Guide - トレーニングガイドを見る + 入門書を見る - + REVIEW - レビュー + 見る - + Review the rules, features, and limitations of openpilot - openpilot 規約 機能 制約を見る + openpilot の特徴を見る - + Are you sure you want to review the training guide? - 本当にトレーニングガイドを見ますか? + 入門書を見てもよろしいですか? - + Regulatory - レギュレーション + 認証情報 - + VIEW - ビュー + 見る + Change Language - 言語を変更 + 言語を変更 + CHANGE - 変更 + 変更 + Select a language - 言語を選択する + 言語を選択 - + Reboot 再起動 - + Power Off 電源を切る - + openpilot requires the device to be mounted within 4° left or right and within 5° up or 8° down. openpilot is continuously calibrating, resetting is rarely required. - openpilot は、左右に4°、上に5°、下に8°の範囲に設置する必要があります。openpilot は継続的に校正されているので、手動でリセットする必要はほとんどありません。 + openpilot は、左または右の4°以内、上の5°または下の8°以内にデバイスを取付ける必要があります。キャリブレーションを引き続きます、リセットはほとんど必要ありません。 - + Your device is pointed %1° %2 and %3° %4. - デバイスは %1° %2 と %3° %4を示しています。 + このデバイスは%2の%1°、%4の%3°に向けます。 - + down - + up - + left - + right - + Are you sure you want to reboot? - 本当に再起動しますか? + 再起動してもよろしいですか? - + Disengage to Reboot - 再起動するために離脱します + openpilot をキャンセルして再起動ができます - + Are you sure you want to power off? - 本当に電源を切っても良いですか? + シャットダウンしてもよろしいですか? - + Disengage to Power Off - 電源を切るために離脱します + openpilot をキャンセルしてシャットダウンができます @@ -260,7 +263,7 @@ Drives - ドライブ + 運転履歴 @@ -293,25 +296,20 @@ camera starting - カメラ起動 + カメラを起動しています InputDialog - + Cancel キャンセル - - Need at least - 最低限必要なもの - - - - characters! - 記号! + + Need at least %1 characters! + パスワードは%%1文字以上で入力してください! @@ -319,22 +317,73 @@ Installing... - インストール... + インストールしています... Receiving objects: - オブジェクトを受信中: + オブジェクトをダウンロードしています: Resolving deltas: - リゾルブ解決中: + デルタを解決しています: Updating files: - ファイルを更新中: + ファイルを更新しています: + + + + MapETA + + + eta + 予定到着時間 + + + + min + + + + + hr + 時間 + + + + km + キロメートル + + + + mi + マイル + + + + MapInstructions + + + km + キロメートル + + + + m + メートル + + + + mi + マイル + + + + ft + フィート @@ -347,7 +396,7 @@ CLEAR - クリア + 削除 @@ -357,39 +406,59 @@ Try the Navigation Beta - ベータ版ナビゲーション + ベータ版ナビゲーションを試し - Get turn-by-turn directions displayed and more with a comma + Get turn-by-turn directions displayed and more with a comma prime subscription. Sign up now: https://connect.comma.ai - より詳細な案内や表示に関する情報を得ることができます。 詳しくはこちら:https://connect.comma.ai + より詳細な案内情報を得ることができます。 +詳しくはこちら:https://connect.comma.ai No home location set - 自宅の登録なし -位置を設定 + 自宅の住所はまだ +設定されていません No work location set - 職場の登録なし -位置を設定 + 職場の住所はまだ +設定されていません no recent destinations - 最寄りの目的地がありません + 最近の目的地履歴がありません + + + + MapWindow + + + Map Loading + マップを読み込んでいます + + + + Waiting for GPS + GPS信号を探しています MultiOptionDialog + Select - 選択 + 選択 + + + + Cancel + キャンセル @@ -407,44 +476,44 @@ location set - for " - のため " + for "%1" + ネットワーク名:%1 Wrong password - パスワードが間違っています + パスワードが間違っています NvgWindow - + km/h - km/時 + km/h - + mph - マイル/時 + mph - - + + MAX - 最大 + 最高速度 - - + + SPEED 速度 - - + + LIMIT - 制限 + 制限速度 @@ -470,78 +539,76 @@ location set Pair your device to your comma account - デバイスとアカウントを連携する - - - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>Go to https://connect.comma.ai on your phone</li> - <li style='margin-bottom: 50px;'>Click "add new device" and scan the QR code on the right</li> - <li style='margin-bottom: 50px;'>Bookmark connect.comma.ai to your home screen to use it like an app</li> - </ol> - - - <ol type='1' style='margin-left: 15px;'> - <li style='margin-bottom: 50px;'>モバイルでアクセス https://connect.comma.ai</li> - <li style='margin-bottom: 50px;'>“新しいデバイスを追加”をクリックし,QRコードを読み込みます</li> - <li style='margin-bottom: 50px;'>connect.comma.aiをホーム画面にブックマークして、アプリのように使うことができます</li> - </ol> - + デバイスと comma アカウントを連携する + + + + Go to https://connect.comma.ai on your phone + モバイルデバイスで「connect.comma.ai」にアクセスして + + + + Click "add new device" and scan the QR code on the right + 「新しいデバイスを追加」を押すと、右側のQRコードをスキャンしてください + + + + Bookmark connect.comma.ai to your home screen to use it like an app + 「connect.comma.ai」をホーム画面に追加して、アプリのように使うことができます PrimeAdWidget - + Upgrade Now - いますぐアップグレード + 今すぐアップグレート - + Become a comma prime member at connect.comma.ai - connect.comma.ai のプライムメンバーになる + connect.comma.ai でプライム会員に登録できます - + PRIME FEATURES: - 主な機能: + 特典: - + Remote access リモートアクセス - + 1 year of storage - 1年の保存期間 + 一年間の保存期間 - + Developer perks - 開発者特典 + 開発者向け特典 PrimeUserWidget - + ✓ SUBSCRIBED - ✓ 購読しました + ✓ 入会しました - + comma prime comma prime - + CONNECT.COMMA.AI CONNECT.COMMA.AI - + COMMA POINTS COMMA POINTS @@ -556,30 +623,30 @@ location set Exit - 退出 + 閉じる - + dashcam - ダッシュカム + ドライブレコーダー - + openpilot - オープンパイロット + openpilot - + %1 minute%2 ago %1 分%2 前 - + %1 hour%2 ago %1 時間%2 前 - + %1 day%2 ago %1 日%2 前 @@ -594,7 +661,7 @@ location set Are you sure you want to reset your device? - 本当に初期化しますか? + 初期化してもよろしいですか? @@ -609,7 +676,7 @@ location set System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - システムを初期化させます。 すべてのコンテンツと設定が削除されます。 キャンセルを押すと再起動します。 + システムの初期化をリクエストしました。「確認」ボタンを押すとデバイスが初期化されます。「キャンセル」ボタンを押すと起動を続行します。 @@ -629,13 +696,13 @@ location set Unable to mount data partition. Press confirm to reset your device. - dataパーティションをマウントできません。 確認を押すとデバイスが初期化されます。 + 「data」パーティションをマウントできません。「確認」ボタンを押すとデバイスが初期化されます。 RichTextDialog - + Ok OK @@ -643,35 +710,35 @@ location set SettingsWindow - + × × - + Device デバイス - - + + Network - ネットワーク + ネットワーク - + Toggles 切り替え - + Software - ソフトウェア + ソフトウェア - + Navigation - ナビゲーション + ナビゲーション @@ -711,7 +778,7 @@ location set Connect to Wi-Fi - Wi-Fiに接続 + Wi-Fi に接続 @@ -737,7 +804,7 @@ location set Dashcam - ダッシュカム + ドライブレコーダー @@ -747,7 +814,7 @@ location set Enter URL - URLを入力 + URL を入力 @@ -767,7 +834,7 @@ location set Ensure the entered URL is valid, and the device’s internet connection is good. - 入力されたURLが有効であること、デバイスがインターネットに接続されていることを確認してください。 + 入力された URL を確認し、デバイスがインターネットに接続されていることを確認してください。 @@ -777,23 +844,23 @@ location set Start over - 再スタート + 最初からやり直す SetupWidget - + Finish Setup セットアップ完了 - + Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. デバイスを comma connect (connect.comma.ai)でペアリングし comma prime 特典を申請してください。 - + Pair device デバイスをペアリング @@ -857,7 +924,7 @@ location set PANDA - パンダ + PANDA @@ -908,68 +975,89 @@ location set SoftwarePanel - + Git Branch Git ブランチ - + Git Commit Git コミット - + OS Version OS バージョン - + Version バージョン - + Last Update Check 最終更新確認 - + The last time openpilot successfully checked for an update. The updater only runs while the car is off. openpilotが最後にアップデートの確認に成功してからの時間です。アップデート処理は、車の電源が切れているときのみ実行されます。 - + Check for Update - 更新確認 + 更新プログラムをチェック - + CHECKING 確認中 - - Uninstall - アンインストール + + Switch Branch + - + + ENTER + + + + + + The new branch will be pulled the next time the updater runs. + + + + + Enter branch name + + + + UNINSTALL アンインストール - + + Uninstall %1 + %1をアンインストール + + + Are you sure you want to uninstall? - 本当にアンインストールしますか? + アンインストールしてもよろしいですか? - + failed to fetch update - 更新の取得に失敗しました + 更新のダウンロードにエラーが発生しました - - + + CHECK 確認 @@ -984,7 +1072,7 @@ location set Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 警告: これは、GitHub の設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外のGitHubのユーザー名を入力しないでください。コンマのスタッフがGitHubのユーザー名を追加するようお願いすることはありません。 + 警告: これは、GitHub の設定にあるすべての公開鍵への SSH アクセスを許可するものです。自分以外の GitHub のユーザー名を入力しないでください。コンマのスタッフが GitHub のユーザー名を追加するようお願いすることはありません。 @@ -995,7 +1083,7 @@ location set Enter your GitHub username - GitHubのユーザー名を入力してください + GitHub のユーザー名を入力してください @@ -1010,7 +1098,7 @@ location set Username '%1' has no keys on GitHub - ユーザー名“%1”は GitHub に鍵がありません + ユーザー名 “%1” は GitHub に鍵がありません @@ -1057,84 +1145,94 @@ location set TogglesPanel - + Enable openpilot openpilot を有効化 - + Use the openpilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. Changing this setting takes effect when the car is powered off. アダプティブクルーズコントロールとレーンキーピングドライバーアシスト(openpilotシステム)。この機能を使用するには、常に注意が必要です。この設定を変更すると、車の電源が切れたときに有効になります。 - + Enable Lane Departure Warnings 車線逸脱警報機能を有効化 - + Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). 時速31マイル(50km)を超えるスピードで走行中、方向指示器を作動させずに検出された車線ライン上に車両が触れた場合、車線に戻るアラートを受信します。 - + Enable Right-Hand Drive 右ハンドルを有効化 - + Allow openpilot to obey left-hand traffic conventions and perform driver monitoring on right driver seat. - openpilotが左側通行規則を遵守し、右側の運転席でドライバーの監視を行うことを可能にします。 + openpilot が左側通行規則を遵守し、右ハンドルでドライバー監視を行うことを可能にします。 - + Use Metric System メートル法を有効化 - + Display speed in km/h instead of mph. - 速度はmphではなくkm/hで表示されます。 + 速度は mph ではなく km/h で表示されます。 - + Record and Upload Driver Camera - ドライバーカメラの録画とアップロード + 車内カメラの録画とアップロード - + Upload data from the driver facing camera and help improve the driver monitoring algorithm. - ドライバーカメラからのデータをアップロードし、ドライバー監視のアルゴリズム向上に役立てます。 + 車内カメラの映像をアップロードし、ドライバー監視システムのアルゴリズムの向上に役立てます。 - + Disengage On Accelerator Pedal - アクセルペダルで解除する + アクセル踏むと openpilot をキャンセル - + When enabled, pressing the accelerator pedal will disengage openpilot. - 有効な場合 openpilot はアクセルペダルを踏むと解除されます。 + 有効な場合は、アクセルを踏むと openpilot をキャンセルします。 - + Show ETA in 24h format 24時間表示 - + Use 24h format instead of am/pm - AM/PMの代わりに24時間形式を使用します + AM/PM の代わりに24時間形式を使用します + + + + Show Map on Left Side of UI + ディスプレイの左側にマップを表示 + + + + Show map on left side when in split screen view. + 分割画面表示の場合、ディスプレイの左側にマップを表示します。 - + openpilot Longitudinal Control - openpilot による垂直方向の制御 + openpilot 縦方向制御 - + openpilot will disable the car's radar and will take over control of gas and brakes. Warning: this disables AEB! - openpilotは、車のレーダーを無効化し、アクセルとブレーキの制御を引き継ぎます。注意:AEBを無効にします! + openpilot は、車のレーダーを無効化し、アクセルとブレーキの制御を引き継ぎます。注意:AEB を無効化にします! @@ -1147,12 +1245,12 @@ location set An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OSのアップデートが必要です。Wi-Fiに接続することで、最速のアップデートを体験できます。ダウンロードサイズは約1GBです。 + オペレーティングシステムのアップデートが必要です。Wi-Fi に接続することで、最速のアップデートを体験できます。ダウンロードサイズは約 1GB です。 Connect to Wi-Fi - Wi-Fiに接続 + Wi-Fi に接続 @@ -1200,8 +1298,8 @@ location set - Forget Wi-Fi Network " - Wi-Fiを削除する” + Forget Wi-Fi Network "%1"? + Wi-Fiネットワーク%1を削除してもよろしいですか? From 5f021ac1d6b307fae892e2966cb2b2699d2799cb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 19 Jul 2022 17:21:07 -0700 Subject: [PATCH 377/436] Subaru: use CANPacker for counter (#25216) * Subaru: use CANPacker for counter * bump opendbc * bump panda --- opendbc | 2 +- panda | 2 +- selfdrive/car/subaru/carcontroller.py | 12 ++++++------ selfdrive/car/subaru/carstate.py | 6 +++--- selfdrive/car/subaru/subarucan.py | 6 ++---- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/opendbc b/opendbc index 3fb3f5e82129ad..e2465cc701e878 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 3fb3f5e82129ad76232bcdca10632ed0566b20f8 +Subproject commit e2465cc701e878fdae5b4e363496c2fa5d2f7c08 diff --git a/panda b/panda index e51aa5ebce031c..1dfee5973bc288 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit e51aa5ebce031c96e802b07d13120a039fa7b82f +Subproject commit 1dfee5973bc2884b61e55b7614f2fe90d0e2a1f3 diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index dca86c30a6ee80..3fb4bc8553c246 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -49,7 +49,7 @@ def update(self, CC, CS): # *** alerts and pcm cancel *** if self.CP.carFingerprint in PREGLOBAL_CARS: - if self.es_distance_cnt != CS.es_distance_msg["Counter"]: + if self.es_distance_cnt != CS.es_distance_msg["COUNTER"]: # 1 = main, 2 = set shallow, 3 = set deep, 4 = resume shallow, 5 = resume deep # disengage ACC when OP is disengaged if pcm_cancel_cmd: @@ -66,16 +66,16 @@ def update(self, CC, CS): self.cruise_button_prev = cruise_button can_sends.append(subarucan.create_preglobal_es_distance(self.packer, cruise_button, CS.es_distance_msg)) - self.es_distance_cnt = CS.es_distance_msg["Counter"] + self.es_distance_cnt = CS.es_distance_msg["COUNTER"] else: - if self.es_distance_cnt != CS.es_distance_msg["Counter"]: + if self.es_distance_cnt != CS.es_distance_msg["COUNTER"]: can_sends.append(subarucan.create_es_distance(self.packer, CS.es_distance_msg, pcm_cancel_cmd)) - self.es_distance_cnt = CS.es_distance_msg["Counter"] + self.es_distance_cnt = CS.es_distance_msg["COUNTER"] - if self.es_lkas_cnt != CS.es_lkas_msg["Counter"]: + if self.es_lkas_cnt != CS.es_lkas_msg["COUNTER"]: can_sends.append(subarucan.create_es_lkas(self.packer, CS.es_lkas_msg, CC.enabled, hud_control.visualAlert, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart)) - self.es_lkas_cnt = CS.es_lkas_msg["Counter"] + self.es_lkas_cnt = CS.es_lkas_msg["COUNTER"] new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / self.p.STEER_MAX diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 45ea66fb27cf17..b2611869775c8e 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -173,7 +173,7 @@ def get_cam_can_parser(CP): ("Standstill_2", "ES_Distance"), ("Cruise_Fault", "ES_Distance"), ("Signal5", "ES_Distance"), - ("Counter", "ES_Distance"), + ("COUNTER", "ES_Distance"), ("Signal6", "ES_Distance"), ("Cruise_Button", "ES_Distance"), ("Signal7", "ES_Distance"), @@ -188,7 +188,7 @@ def get_cam_can_parser(CP): ("Cruise_Set_Speed", "ES_DashStatus"), ("Conventional_Cruise", "ES_DashStatus"), - ("Counter", "ES_Distance"), + ("COUNTER", "ES_Distance"), ("Signal1", "ES_Distance"), ("Cruise_Fault", "ES_Distance"), ("Cruise_Throttle", "ES_Distance"), @@ -206,7 +206,7 @@ def get_cam_can_parser(CP): ("Cruise_Resume", "ES_Distance"), ("Signal6", "ES_Distance"), - ("Counter", "ES_LKAS_State"), + ("COUNTER", "ES_LKAS_State"), ("LKAS_Alert_Msg", "ES_LKAS_State"), ("Signal1", "ES_LKAS_State"), ("LKAS_ACTIVE", "ES_LKAS_State"), diff --git a/selfdrive/car/subaru/subarucan.py b/selfdrive/car/subaru/subarucan.py index 63511d183e503d..e6efe7aa7b79a9 100644 --- a/selfdrive/car/subaru/subarucan.py +++ b/selfdrive/car/subaru/subarucan.py @@ -8,13 +8,12 @@ def create_steering_control(packer, apply_steer, frame, steer_step): idx = (frame / steer_step) % 16 values = { - "Counter": idx, "LKAS_Output": apply_steer, "LKAS_Request": 1 if apply_steer != 0 else 0, "SET_1": 1 } - return packer.make_can_msg("ES_LKAS", 0, values) + return packer.make_can_msg("ES_LKAS", 0, values, idx) def create_steering_status(packer, apply_steer, frame, steer_step): return packer.make_can_msg("ES_LKAS_State", 0, {}) @@ -72,13 +71,12 @@ def create_preglobal_steering_control(packer, apply_steer, frame, steer_step): idx = (frame / steer_step) % 8 values = { - "Counter": idx, "LKAS_Command": apply_steer, "LKAS_Active": 1 if apply_steer != 0 else 0 } values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_LKAS") - return packer.make_can_msg("ES_LKAS", 0, values) + return packer.make_can_msg("ES_LKAS", 0, values, idx) def create_preglobal_es_distance(packer, cruise_button, es_distance_msg): From 0eacab70a079ce67b1fe5443a146a535e467674c Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 19 Jul 2022 18:10:08 -0700 Subject: [PATCH 378/436] Car docs: hide footnotes for hidden columns (#25219) * Hide footnotes for hidden column * fix, this should only affect website --- selfdrive/car/docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index eb2f1923c8569e..26f8949e8f5be8 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -58,7 +58,7 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str, only_tier_co for c in hide_cols: del car.row[c] - footnotes = [fn.value.text for fn in ALL_FOOTNOTES] + footnotes = [fn.value.text for fn in ALL_FOOTNOTES if fn.value.column in cols] cars_md: str = template.render(all_car_info=all_car_info, footnotes=footnotes, Star=Star, Column=cols, star_descriptions=STAR_DESCRIPTIONS) return cars_md From 1b2d0ce2ac0108fcba6e38c65a9b5666c943952d Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 20 Jul 2022 17:32:26 +0200 Subject: [PATCH 379/436] bump cereal --- cereal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cereal b/cereal index 1c2cba75d66383..7870a1123d4e8d 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 1c2cba75d66383fb0d0957f9cb2f6ffebb4f8915 +Subproject commit 7870a1123d4e8dfe5c39e86224c2f851382f3fed From 4212aface76b7a780f94a1f237d846821461fd29 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 12:56:22 -0700 Subject: [PATCH 380/436] Car docs diff bot: detect name + year changes (#25224) * These cars have TSS-P standard * temp * debug close matching * check everything * clean up * combine and use a dict * programmatic * revert * clean up match_cars * comment * clean up * clean up * restore car changes * test * Revert "test" This reverts commit e96f6936816051720db44072768f4df4e2cf5e82. * we don't need this * fix footnotes on model column --- selfdrive/debug/print_docs_diff.py | 110 +++++++++++++++++------------ 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py index 5cf3867b2d8d8a..c02649e3fab4e7 100755 --- a/selfdrive/debug/print_docs_diff.py +++ b/selfdrive/debug/print_docs_diff.py @@ -1,13 +1,16 @@ #!/usr/bin/env python3 import argparse +from collections import defaultdict +import difflib import pickle from selfdrive.car.docs import get_all_car_info from selfdrive.car.docs_definitions import Column +FOOTNOTE_TAG = "{}" STAR_ICON = '' COLUMNS = "|" + "|".join([column.value for column in Column]) + "|" -COLUMN_HEADER = "|---|---|---|:---:|:---:|:---:|:---:|:---:|" +COLUMN_HEADER = "|---|---|---|:---:|:---:|:---:|:---:|" ARROW_SYMBOL = "➡️" @@ -16,69 +19,84 @@ def load_base_car_info(path): return pickle.load(f) -def get_star_diff(base_car, new_car): - return [column for column, value in base_car.row.items() if value != new_car.row[column]] - +def match_cars(base_cars, new_cars): + """Matches CarInfo by name similarity and finds additions and removals""" + changes = [] + additions = [] + for new in new_cars: + closest_match = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.)[0] -def format_row(builder): - return "|" + "|".join(builder) + "|" + if closest_match not in [c[1].name for c in changes]: + changes.append((new, next(car for car in base_cars if car.name == closest_match))) + else: + additions.append(new) + removals = [b for b in base_cars if b.name not in [c[1].name for c in changes]] + return changes, additions, removals -def print_car_info_diff(path): - base_car_info = {f"{i.make} {i.model}": i for i in load_base_car_info(path)} - new_car_info = {f"{i.make} {i.model}": i for i in get_all_car_info()} - tier_changes = [] - star_changes = [] - removals = [] - additions = [] +def build_column_diff(base_car, new_car): + row_builder = [] + for column in Column: + base_column = base_car.get_column(column, STAR_ICON, FOOTNOTE_TAG) + new_column = new_car.get_column(column, STAR_ICON, FOOTNOTE_TAG) - # Changes (tier + stars) - for base_car_model, base_car in base_car_info.items(): - if base_car_model not in new_car_info: - continue + if base_column != new_column: + row_builder.append(f"{base_column} {ARROW_SYMBOL} {new_column}") + else: + row_builder.append(new_column) - new_car = new_car_info[base_car_model] + return format_row(row_builder) - # Tier changes - if base_car.tier != new_car.tier: - tier_changes.append(f"- Tier for {base_car.make} {base_car.model} changed! ({base_car.tier.name.title()} {ARROW_SYMBOL} {new_car.tier.name.title()})") - # Star changes - diff = get_star_diff(base_car, new_car) - if not len(diff): - continue - - row_builder = [] - for column in list(Column): - if column not in diff: - row_builder.append(new_car.get_column(column, STAR_ICON, "{}")) - else: - row_builder.append(base_car.get_column(column, STAR_ICON, "{}") + ARROW_SYMBOL + new_car.get_column(column, STAR_ICON, "{}")) - - star_changes.append(format_row(row_builder)) +def format_row(builder): + return "|" + "|".join(builder) + "|" - # Removals - for model in set(base_car_info) - set(new_car_info): - car_info = base_car_info[model] - removals.append(format_row([car_info.get_column(column, STAR_ICON, "{}") for column in Column])) - # Additions - for model in set(new_car_info) - set(base_car_info): - car_info = new_car_info[model] - additions.append(format_row([car_info.get_column(column, STAR_ICON, "{}") for column in Column])) +def print_car_info_diff(path): + base_car_info = defaultdict(list) + new_car_info = defaultdict(list) + + for car in load_base_car_info(path): + base_car_info[car.car_fingerprint].append(car) + for car in get_all_car_info(): + new_car_info[car.car_fingerprint].append(car) + + changes = defaultdict(list) + for base_car_model, base_cars in base_car_info.items(): + # Match car info changes, and get additions and removals + new_cars = new_car_info[base_car_model] + car_changes, car_additions, car_removals = match_cars(base_cars, new_cars) + + # Removals + for car_info in car_removals: + changes["removals"].append(format_row([car_info.get_column(column, STAR_ICON, FOOTNOTE_TAG) for column in Column])) + + # Additions + for car_info in car_additions: + changes["additions"].append(format_row([car_info.get_column(column, STAR_ICON, FOOTNOTE_TAG) for column in Column])) + + for new_car, base_car in car_changes: + # Tier changes + if base_car.tier != new_car.tier: + changes["tier"].append(f"- Tier for {base_car.make} {base_car.model} changed! ({base_car.tier.name.title()} {ARROW_SYMBOL} {new_car.tier.name.title()})") + + # Column changes + row_diff = build_column_diff(base_car, new_car) + if ARROW_SYMBOL in row_diff: + changes["column"].append(row_diff) # Print diff - if len(star_changes) or len(tier_changes) or len(removals) or len(additions): + if any(len(c) for c in changes.values()): markdown_builder = ["### ⚠️ This PR makes changes to [CARS.md](../blob/master/docs/CARS.md) ⚠️"] - for title, category in (("## 🏅 Tier Changes", tier_changes), ("## 🔀 Star Changes", star_changes), ("## ❌ Removed", removals), ("## ➕ Added", additions)): - if len(category): + for title, category in (("## 🏅 Tier Changes", "tier"), ("## 🔀 Column Changes", "column"), ("## ❌ Removed", "removals"), ("## ➕ Added", "additions")): + if len(changes[category]): markdown_builder.append(title) if "Tier" not in title: markdown_builder.append(COLUMNS) markdown_builder.append(COLUMN_HEADER) - markdown_builder.extend(category) + markdown_builder.extend(changes[category]) print("\n".join(markdown_builder)) From a1f4723d1cbdd1fdb4f7d2553e2fb4a07eb7aed6 Mon Sep 17 00:00:00 2001 From: Thomas Staudinger Date: Wed, 20 Jul 2022 22:02:35 +0200 Subject: [PATCH 381/436] docs: fix Table of Contents link to "in a car" section (#25225) * Fix Table of Contents link to "in a car" section The heading for "Running in a car" changed to "Running on a dedicated device in a car" so the Table of Contents link no longer worked. This PR updates the link and text in the table. * Update README.md Co-authored-by: Shane Smiskol --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3dce9d4475bb87..94837575b456e9 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Table of Contents ======================= * [What is openpilot?](#what-is-openpilot) -* [Running in a car](#running-in-a-car) +* [Running in a car](#running-on-a-dedicated-device-in-a-car) * [Running on PC](#running-on-pc) * [Community and Contributing](#community-and-contributing) * [User Data and comma Account](#user-data-and-comma-account) From c50a14a7d3baf2449234e9d17ba809e667f5f4e8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 13:59:26 -0700 Subject: [PATCH 382/436] Car docs: update Toyota supported packages (#25218) These cars have TSS-P standard --- docs/CARS.md | 18 +++++++++++------- selfdrive/car/toyota/values.py | 22 ++++++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index f584890b0f64ff..3462673aa5270c 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -18,7 +18,7 @@ How We Rate The Cars - - Limited ability to make turns. -# 192 Supported Cars +# 196 Supported Cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque| |---|---|---|:---:|:---:|:---:|:---:| @@ -149,10 +149,11 @@ How We Rate The Cars |Škoda|Superb 2015-18|Driver Assistance||||| |Toyota|Alphard 2019-20|All||||| |Toyota|Alphard Hybrid 2021|All||||| -|Toyota|Avalon 2016-18|TSS-P|[3](#footnotes)|||| -|Toyota|Avalon 2019-21|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon 2016|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon 2017-18|All|[3](#footnotes)|||| +|Toyota|Avalon 2019-21|All|[3](#footnotes)|||| |Toyota|Avalon 2022|All||||| -|Toyota|Avalon Hybrid 2019-21|TSS-P|[3](#footnotes)|||| +|Toyota|Avalon Hybrid 2019-21|All|[3](#footnotes)|||| |Toyota|Avalon Hybrid 2022|All||||| |Toyota|C-HR 2017-21|All||||| |Toyota|C-HR Hybrid 2017-19|All||||| @@ -171,15 +172,18 @@ How We Rate The Cars |Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)|||| |Toyota|Highlander Hybrid 2020-22|All||||| |Toyota|Mirai 2021|All||||| -|Toyota|Prius 2016-20|TSS-P|[3](#footnotes)|||| +|Toyota|Prius 2016|TSS-P|[3](#footnotes)|||| +|Toyota|Prius 2017-20|All|[3](#footnotes)|||| |Toyota|Prius 2021-22|All||||| |Toyota|Prius Prime 2017-20|All|[3](#footnotes)|||| |Toyota|Prius Prime 2021-22|All||||| |Toyota|Prius v 2017|TSS-P|[3](#footnotes)|||| -|Toyota|RAV4 2016-18|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 2016|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 2017-18|All|[3](#footnotes)|||| |Toyota|RAV4 2019-21|All||||| |Toyota|RAV4 2022|All||||| -|Toyota|RAV4 Hybrid 2016-18|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 Hybrid 2016|TSS-P|[3](#footnotes)|||| +|Toyota|RAV4 Hybrid 2017-18|All|[3](#footnotes)|||| |Toyota|RAV4 Hybrid 2019-21|All||||| |Toyota|RAV4 Hybrid 2022|All||||| |Toyota|Sienna 2018-20|All|[3](#footnotes)|||| diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 2bbf951d78589b..46b43ba25ce70b 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -105,9 +105,12 @@ class ToyotaCarInfo(CarInfo): # Toyota CAR.ALPHARD_TSS2: ToyotaCarInfo("Toyota Alphard 2019-20"), CAR.ALPHARDH_TSS2: ToyotaCarInfo("Toyota Alphard Hybrid 2021"), - CAR.AVALON: ToyotaCarInfo("Toyota Avalon 2016-18", "TSS-P", footnotes=[Footnote.DSU]), - CAR.AVALON_2019: ToyotaCarInfo("Toyota Avalon 2019-21", "TSS-P", footnotes=[Footnote.DSU]), - CAR.AVALONH_2019: ToyotaCarInfo("Toyota Avalon Hybrid 2019-21", "TSS-P", footnotes=[Footnote.DSU]), + CAR.AVALON: [ + ToyotaCarInfo("Toyota Avalon 2016", "TSS-P", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota Avalon 2017-18", footnotes=[Footnote.DSU]), + ], + CAR.AVALON_2019: ToyotaCarInfo("Toyota Avalon 2019-21", footnotes=[Footnote.DSU]), + CAR.AVALONH_2019: ToyotaCarInfo("Toyota Avalon Hybrid 2019-21", footnotes=[Footnote.DSU]), CAR.AVALON_TSS2: ToyotaCarInfo("Toyota Avalon 2022"), CAR.AVALONH_TSS2: ToyotaCarInfo("Toyota Avalon Hybrid 2022"), CAR.CAMRY: ToyotaCarInfo("Toyota Camry 2018-20", video_link="https://www.youtube.com/watch?v=fkcjviZY9CM", footnotes=[Footnote.CAMRY]), @@ -132,7 +135,8 @@ class ToyotaCarInfo(CarInfo): CAR.HIGHLANDERH: ToyotaCarInfo("Toyota Highlander Hybrid 2017-19", footnotes=[Footnote.DSU]), CAR.HIGHLANDERH_TSS2: ToyotaCarInfo("Toyota Highlander Hybrid 2020-22"), CAR.PRIUS: [ - ToyotaCarInfo("Toyota Prius 2016-20", "TSS-P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota Prius 2016", "TSS-P", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota Prius 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), ToyotaCarInfo("Toyota Prius Prime 2017-20", video_link="https://www.youtube.com/watch?v=8zopPJI8XQ0", footnotes=[Footnote.DSU]), ], CAR.PRIUS_V: ToyotaCarInfo("Toyota Prius v 2017", "TSS-P", min_enable_speed=MIN_ACC_SPEED, footnotes=[Footnote.DSU]), @@ -140,8 +144,14 @@ class ToyotaCarInfo(CarInfo): ToyotaCarInfo("Toyota Prius 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"), ToyotaCarInfo("Toyota Prius Prime 2021-22", video_link="https://www.youtube.com/watch?v=J58TvCpUd4U"), ], - CAR.RAV4: ToyotaCarInfo("Toyota RAV4 2016-18", "TSS-P", footnotes=[Footnote.DSU]), - CAR.RAV4H: ToyotaCarInfo("Toyota RAV4 Hybrid 2016-18", "TSS-P", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]), + CAR.RAV4: [ + ToyotaCarInfo("Toyota RAV4 2016", "TSS-P", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota RAV4 2017-18", footnotes=[Footnote.DSU]) + ], + CAR.RAV4H: [ + ToyotaCarInfo("Toyota RAV4 Hybrid 2016", "TSS-P", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]), + ToyotaCarInfo("Toyota RAV4 Hybrid 2017-18", video_link="https://youtu.be/LhT5VzJVfNI?t=26", footnotes=[Footnote.DSU]) + ], CAR.RAV4_TSS2: ToyotaCarInfo("Toyota RAV4 2019-21", video_link="https://www.youtube.com/watch?v=wJxjDd42gGA"), CAR.RAV4_TSS2_2022: ToyotaCarInfo("Toyota RAV4 2022"), CAR.RAV4H_TSS2: ToyotaCarInfo("Toyota RAV4 Hybrid 2019-21"), From 180c2abdd9a99f0851a1bc0b3dc2a4f3ab55ddf8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 14:11:59 -0700 Subject: [PATCH 383/436] Improve Japanese translations (#25226) * Minor Japanese improvements * fix rm Co-authored-by: PONPC --- selfdrive/ui/translations/main_ja.qm | Bin 20204 -> 20202 bytes selfdrive/ui/translations/main_ja.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/translations/main_ja.qm b/selfdrive/ui/translations/main_ja.qm index 1c98d5a1e38da73116211e91d817d4c4bbf65293..b70c5abe0d4e0c023a452eda5df85614d61b81f2 100644 GIT binary patch delta 1667 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 diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index dd7643e21788a5..c3b2b850eebe54 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -406,7 +406,7 @@ Try the Navigation Beta - ベータ版ナビゲーションを試し + β版ナビゲーションを試す @@ -567,7 +567,7 @@ location set Become a comma prime member at connect.comma.ai - connect.comma.ai でプライム会員に登録できます + connect.comma.ai でプライム会員に登録できます From 9c9a0f0ef35933e354c52b39cfe9e7849f1d8d7e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 20 Jul 2022 14:27:10 -0700 Subject: [PATCH 384/436] remove comma two branches --- Jenkinsfile | 1 - release/verify.sh | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4e13717851a755..b8ed52611dcf99 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -70,7 +70,6 @@ pipeline { not { anyOf { branch 'master-ci'; branch 'devel'; branch 'devel-staging'; - branch 'release2'; branch 'release2-staging'; branch 'dashcam'; branch 'dashcam-staging'; branch 'release3'; branch 'release3-staging'; branch 'dashcam3'; branch 'dashcam3-staging'; branch 'testing-closet*'; branch 'hotfix-*' } diff --git a/release/verify.sh b/release/verify.sh index 2ebd50a29dedde..56f21183f1b81e 100755 --- a/release/verify.sh +++ b/release/verify.sh @@ -6,7 +6,7 @@ RED="\033[0;31m" GREEN="\033[0;32m" CLEAR="\033[0m" -BRANCHES="devel dashcam dashcam3 release2 release3" +BRANCHES="devel dashcam3 release3" for b in $BRANCHES; do if git diff --quiet origin/$b origin/$b-staging && [ "$(git rev-parse origin/$b)" = "$(git rev-parse origin/$b-staging)" ]; then printf "%-10s $GREEN ok $CLEAR\n" "$b" From 1f221017cb47eab270656dbf771ddd41e7feb665 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 20 Jul 2022 18:33:07 -0700 Subject: [PATCH 385/436] car incompatibility docs (#25227) * car incompatibility docs * we're getting somewhere * try the table * run generator --- docs/CARS.md | 43 +++++++++++++++++++++++++++++++--- selfdrive/car/CARS_template.md | 41 ++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 3462673aa5270c..073b5343ec5ee3 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -2,8 +2,7 @@ A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. -How We Rate The Cars ---- +## How We Rate The Cars ### Stop and Go - - Adaptive Cruise Control (ACC) operates down to 0 mph. @@ -230,4 +229,42 @@ How We Rate The Cars 8Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)
## Community Maintained Cars -Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). \ No newline at end of file +Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). + +# Don't see your car here? + +**openpilot can support many more cars than it currently does.** There are a few reasons your car may not be supported. +If your car doesn't fit into any of the incompatibility criteria here, then there's a good chance it can be supported! We're adding support for new cars all the time. We don't have a roadmap for car support, and in fact, most car support comes from users like you! + +### Which cars are able to supported? + +openpilot uses the existing steering, gas, and brake interfaces in your car. If your car lacks any one of these interfaces, openpilot will not be able to control the car. If your car has any form of [LKAS](https://en.wikipedia.org/wiki/Automated_Lane_Keeping_Systems)/[LCA](https://en.wikipedia.org/wiki/Lane_centering) and [ACC](https://en.wikipedia.org/wiki/Adaptive_cruise_control), then it almost certainly has these interfaces. These interfaces generally started shipping on cars around 2016. + +If your car has the following packages or features, then it's a good candidate for support. If it does not, then it's unlikely able to be supported. + +| Make | Required Package/Features | +| ---- | ------------------------- | +| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. | +| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. | +| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | +| Nissan | Any car with ProPILOT will likely work. | +| Toyota & Lexus | Any car that has Toyota/Lexus Safety Sense with "Lane Departure Alert with Steering Assist (LDA w/SA)" and/or "Lane Tracing Assist (LTA)" will work. Note that LDA without Steering Assist will not work. These features come standard on most newer models. | +| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA come standard on most newer models. Any form of SCC will work, such as NSCC. | +| Chrysler, Jeep, & Ram | Any car with LaneSense and Adaptive Cruise Control will likely work. These come standard on many newer models. | + +### FlexRay + +All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a CAN bus isn't the only way that the cars in your computer can communicate. Most, if not all, vehicles from the following manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars may one day be supported, but we have no immediate plans to support FlexRay. + +### Toyota Security + +Specific new Toyota models are shipping with a new message authentication method that openpilot does not yet support. +So far, this list includes: +* Toyota Rav4 Prime 2022+ +* Toyota Sienna 2021+ +* Toyota Venza 2021+ +* Toyota Sequoia 2023+ +* Toyota Tundra 2022+ +* Toyota Yaris 2022+ +* Toyota Corolla Cross (only US model) +* Lexus NX 2021+ \ No newline at end of file diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 8603f31434f840..22228c1e07ac62 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -5,8 +5,7 @@ A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. -How We Rate The Cars ---- +## How We Rate The Cars {% for star_row in star_descriptions.values() %} {% for name, stars in star_row.items() %} @@ -34,3 +33,41 @@ How We Rate The Cars ## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). + +# Don't see your car here? + +**openpilot can support many more cars than it currently does.** There are a few reasons your car may not be supported. +If your car doesn't fit into any of the incompatibility criteria here, then there's a good chance it can be supported! We're adding support for new cars all the time. We don't have a roadmap for car support, and in fact, most car support comes from users like you! + +### Which cars are able to supported? + +openpilot uses the existing steering, gas, and brake interfaces in your car. If your car lacks any one of these interfaces, openpilot will not be able to control the car. If your car has any form of [LKAS](https://en.wikipedia.org/wiki/Automated_Lane_Keeping_Systems)/[LCA](https://en.wikipedia.org/wiki/Lane_centering) and [ACC](https://en.wikipedia.org/wiki/Adaptive_cruise_control), then it almost certainly has these interfaces. These interfaces generally started shipping on cars around 2016. + +If your car has the following packages or features, then it's a good candidate for support. If it does not, then it's unlikely able to be supported. + +| Make | Required Package/Features | +| ---- | ------------------------- | +| Acura | Any car with AcuraWatch Plus will work. AcuraWatch Plus comes standard on many newer models. | +| Honda | Any car with Honda Sensing will work. Honda Sensing comes standard on many newer models. | +| Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | +| Nissan | Any car with ProPILOT will likely work. | +| Toyota & Lexus | Any car that has Toyota/Lexus Safety Sense with "Lane Departure Alert with Steering Assist (LDA w/SA)" and/or "Lane Tracing Assist (LTA)" will work. Note that LDA without Steering Assist will not work. These features come standard on most newer models. | +| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA come standard on most newer models. Any form of SCC will work, such as NSCC. | +| Chrysler, Jeep, & Ram | Any car with LaneSense and Adaptive Cruise Control will likely work. These come standard on many newer models. | + +### FlexRay + +All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wiki/CAN_bus) for communication between all the car's computers, however a CAN bus isn't the only way that the cars in your computer can communicate. Most, if not all, vehicles from the following manufacturers use [FlexRay](https://en.wikipedia.org/wiki/FlexRay) instead of a CAN bus: **BMW, Mercedes, Audi, Land Rover, and some Volvo**. These cars may one day be supported, but we have no immediate plans to support FlexRay. + +### Toyota Security + +Specific new Toyota models are shipping with a new message authentication method that openpilot does not yet support. +So far, this list includes: +* Toyota Rav4 Prime 2022+ +* Toyota Sienna 2021+ +* Toyota Venza 2021+ +* Toyota Sequoia 2023+ +* Toyota Tundra 2022+ +* Toyota Yaris 2022+ +* Toyota Corolla Cross (only US model) +* Lexus NX 2021+ \ No newline at end of file From c875f540a87a7226f53e83b8817791ee0764b3b4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 18:40:48 -0700 Subject: [PATCH 386/436] Car docs: fix typo --- docs/CARS.md | 4 ++-- selfdrive/car/CARS_template.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 073b5343ec5ee3..eb4dfd884769ff 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -236,7 +236,7 @@ Although they're not upstream, the community has openpilot running on other make **openpilot can support many more cars than it currently does.** There are a few reasons your car may not be supported. If your car doesn't fit into any of the incompatibility criteria here, then there's a good chance it can be supported! We're adding support for new cars all the time. We don't have a roadmap for car support, and in fact, most car support comes from users like you! -### Which cars are able to supported? +### Which cars are able to be supported? openpilot uses the existing steering, gas, and brake interfaces in your car. If your car lacks any one of these interfaces, openpilot will not be able to control the car. If your car has any form of [LKAS](https://en.wikipedia.org/wiki/Automated_Lane_Keeping_Systems)/[LCA](https://en.wikipedia.org/wiki/Lane_centering) and [ACC](https://en.wikipedia.org/wiki/Adaptive_cruise_control), then it almost certainly has these interfaces. These interfaces generally started shipping on cars around 2016. @@ -249,7 +249,7 @@ If your car has the following packages or features, then it's a good candidate f | Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | | Nissan | Any car with ProPILOT will likely work. | | Toyota & Lexus | Any car that has Toyota/Lexus Safety Sense with "Lane Departure Alert with Steering Assist (LDA w/SA)" and/or "Lane Tracing Assist (LTA)" will work. Note that LDA without Steering Assist will not work. These features come standard on most newer models. | -| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA come standard on most newer models. Any form of SCC will work, such as NSCC. | +| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA comes standard on most newer models. Any form of SCC will work, such as NSCC. | | Chrysler, Jeep, & Ram | Any car with LaneSense and Adaptive Cruise Control will likely work. These come standard on many newer models. | ### FlexRay diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index 22228c1e07ac62..ba95eeccb8f3a1 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -39,7 +39,7 @@ Although they're not upstream, the community has openpilot running on other make **openpilot can support many more cars than it currently does.** There are a few reasons your car may not be supported. If your car doesn't fit into any of the incompatibility criteria here, then there's a good chance it can be supported! We're adding support for new cars all the time. We don't have a roadmap for car support, and in fact, most car support comes from users like you! -### Which cars are able to supported? +### Which cars are able to be supported? openpilot uses the existing steering, gas, and brake interfaces in your car. If your car lacks any one of these interfaces, openpilot will not be able to control the car. If your car has any form of [LKAS](https://en.wikipedia.org/wiki/Automated_Lane_Keeping_Systems)/[LCA](https://en.wikipedia.org/wiki/Lane_centering) and [ACC](https://en.wikipedia.org/wiki/Adaptive_cruise_control), then it almost certainly has these interfaces. These interfaces generally started shipping on cars around 2016. @@ -52,7 +52,7 @@ If your car has the following packages or features, then it's a good candidate f | Subaru | Any car with EyeSight will work. EyeSight comes standard on many newer models. | | Nissan | Any car with ProPILOT will likely work. | | Toyota & Lexus | Any car that has Toyota/Lexus Safety Sense with "Lane Departure Alert with Steering Assist (LDA w/SA)" and/or "Lane Tracing Assist (LTA)" will work. Note that LDA without Steering Assist will not work. These features come standard on most newer models. | -| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA come standard on most newer models. Any form of SCC will work, such as NSCC. | +| Hyundai, Kia, & Genesis | Any car with Smart Cruise Control (SCC) and Lane Following Assist (LFA) or Lane Keeping Assist (LKAS) will work. LKAS/LFA comes standard on most newer models. Any form of SCC will work, such as NSCC. | | Chrysler, Jeep, & Ram | Any car with LaneSense and Adaptive Cruise Control will likely work. These come standard on many newer models. | ### FlexRay @@ -70,4 +70,4 @@ So far, this list includes: * Toyota Tundra 2022+ * Toyota Yaris 2022+ * Toyota Corolla Cross (only US model) -* Lexus NX 2021+ \ No newline at end of file +* Lexus NX 2021+ From c528decd17a71bb8f9eb0be2f0d78ccc2e74f70a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 18:44:47 -0700 Subject: [PATCH 387/436] multilanguage: compile QM in scons (#25217) * All in scons * delete all this * delete the qm files * No need to check QM files in test_translations.py anymore * readme * add lupdate to third party * fix * one line * update files_common * readme imp * add j flag * add to path * duplicate scons! * update readme * fix path fix path fix path * no path --- .gitignore | 1 + release/files_common | 5 +++- selfdrive/ui/SConscript | 7 ++++++ selfdrive/ui/tests/test_translations.py | 29 ++++++++++------------- selfdrive/ui/translations/README.md | 12 +++++----- selfdrive/ui/translations/main_ja.qm | Bin 20202 -> 0 bytes selfdrive/ui/translations/main_ko.qm | Bin 19439 -> 0 bytes selfdrive/ui/translations/main_zh-CHS.qm | Bin 17931 -> 0 bytes selfdrive/ui/translations/main_zh-CHT.qm | Bin 17969 -> 0 bytes selfdrive/ui/update_translations.py | 3 --- third_party/qt5/larch64/bin/lrelease | Bin 0 -> 546824 bytes third_party/qt5/larch64/bin/lupdate | Bin 0 -> 1095744 bytes 12 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 selfdrive/ui/translations/main_ja.qm delete mode 100644 selfdrive/ui/translations/main_ko.qm delete mode 100644 selfdrive/ui/translations/main_zh-CHS.qm delete mode 100644 selfdrive/ui/translations/main_zh-CHT.qm create mode 100755 third_party/qt5/larch64/bin/lrelease create mode 100755 third_party/qt5/larch64/bin/lupdate diff --git a/.gitignore b/.gitignore index e1ff5d500826d8..07626f4e285ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ a.out *.class *.pyxbldc *.vcd +*.qm config.json clcache compile_commands.json diff --git a/release/files_common b/release/files_common index 411e4ff6dc324f..0980f7929dfda9 100644 --- a/release/files_common +++ b/release/files_common @@ -303,7 +303,8 @@ selfdrive/ui/soundd/*.cc selfdrive/ui/soundd/*.h selfdrive/ui/soundd/soundd selfdrive/ui/soundd/.gitignore -selfdrive/ui/translations/* +selfdrive/ui/translations/*.ts +selfdrive/ui/translations/languages.json selfdrive/ui/qt/*.cc selfdrive/ui/qt/*.h @@ -421,6 +422,8 @@ third_party/acados/x86_64/** third_party/acados/larch64/** third_party/acados/include/** +third_party/qt5/larch64/bin/** + scripts/update_now.sh scripts/stop_updater.sh diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 0835c163727f8a..9e89576a540129 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -63,6 +63,13 @@ if GetOption('test'): qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) +# build translation files +translation_sources = Glob("#selfdrive/ui/translations/*.ts", strings=True) +translation_targets = [src.replace(".ts", ".qm") for src in translation_sources] +lrelease = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease' +qt_env.Command(translation_targets, translation_sources, f"{lrelease} $SOURCES") + + # setup and factory resetter if GetOption('extras'): qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) diff --git a/selfdrive/ui/tests/test_translations.py b/selfdrive/ui/tests/test_translations.py index 59751eec19493a..b44ab11ede8222 100755 --- a/selfdrive/ui/tests/test_translations.py +++ b/selfdrive/ui/tests/test_translations.py @@ -23,8 +23,8 @@ def tearDownClass(cls): shutil.rmtree(TMP_TRANSLATIONS_DIR, ignore_errors=True) @staticmethod - def _read_translation_file(path, file, file_ext): - tr_file = os.path.join(path, f"{file}.{file_ext}") + def _read_translation_file(path, file): + tr_file = os.path.join(path, f"{file}.ts") with open(tr_file, "rb") as f: # fix relative path depth return f.read().replace(b"filename=\"../../", b"filename=\"../") @@ -35,9 +35,8 @@ def test_missing_translation_files(self): if not len(file): self.skipTest(f"{name} translation has no defined file") - if not (os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")) and - os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.qm"))): - self.fail(f"{name} is missing translation files, run selfdrive/ui/update_translations.py") + self.assertTrue(os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")), + f"{name} has no XML translation file, run selfdrive/ui/update_translations.py") def test_translations_updated(self): update_translations(translations_dir=TMP_TRANSLATIONS_DIR) @@ -47,18 +46,14 @@ def test_translations_updated(self): if not len(file): self.skipTest(f"{name} translation has no defined file") - for file_ext in ["ts", "qm"]: - with self.subTest(file_ext=file_ext): + # caught by test_missing_translation_files + if not os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.ts")): + self.skipTest(f"{name} missing translation file") - # caught by test_missing_translation_files - if not os.path.exists(os.path.join(TRANSLATIONS_DIR, f"{file}.{file_ext}")): - self.skipTest(f"{name} missing translation file") - - cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file, file_ext) - new_translations = self._read_translation_file(TMP_TRANSLATIONS_DIR, file, file_ext) - - self.assertEqual(cur_translations, new_translations, - f"{file} ({name}) {file_ext.upper()} translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") + cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) + new_translations = self._read_translation_file(TMP_TRANSLATIONS_DIR, file) + self.assertEqual(cur_translations, new_translations, + f"{file} ({name}) XML translation file out of date. Run selfdrive/ui/update_translations.py to update the translation files") @unittest.skip("Only test unfinished translations before going to release") def test_unfinished_translations(self): @@ -67,7 +62,7 @@ def test_unfinished_translations(self): if not len(file): raise self.skipTest(f"{name} translation has no defined file") - cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file, "ts") + cur_translations = self._read_translation_file(TRANSLATIONS_DIR, file) self.assertTrue(b"" not in cur_translations, f"{file} ({name}) translation file has unfinished translations. Finish translations or mark them as completed in Qt Linguist") diff --git a/selfdrive/ui/translations/README.md b/selfdrive/ui/translations/README.md index 4038b1e0ce0bec..a6f2f665ebdd86 100644 --- a/selfdrive/ui/translations/README.md +++ b/selfdrive/ui/translations/README.md @@ -9,7 +9,7 @@ Before getting started, make sure you have set up the openpilot Ubuntu developme openpilot provides a few tools to help contributors manage their translations and to ensure quality. To get started: 1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Simplified Chinese is `中文(繁體)`). -2. Generate the translation file (`*.ts`): +2. Generate the XML translation file (`*.ts`): ```shell selfdrive/ui/update_translations.py ``` @@ -17,22 +17,22 @@ openpilot provides a few tools to help contributors manage their translations an ```shell linguist selfdrive/ui/translations/your_language_file.ts ``` -4. Save your file and generate the compiled QM file used by the Qt UI: +4. View your finished translations by compiling and starting the UI, then find it in the language selector: ```shell - selfdrive/ui/update_translations.py --release + scons -j$(nproc) selfdrive/ui && selfdrive/ui/ui ``` ### Improving an Existing Language -Follow the steps above, omitting steps 1. and 2. Any time you edit translations you'll want to make sure to compile them. +Follow step 3. above, you can review existing translations and add missing ones. Once you're done, just open a pull request to openpilot. ### Updating the UI -Any time you edit source code in the UI, you need to update and compile the translations to ensure the line numbers and contexts are up to date (last step above). +Any time you edit source code in the UI, you need to update the translations to ensure the line numbers and contexts are up to date (first step above). ### Testing -openpilot has a few unit tests to make sure all translations are up to date and that all strings are wrapped in a translation marker. +openpilot has a few unit tests to make sure all translations are up to date and that all strings are wrapped in a translation marker. They are run in CI, but you can also run them locally. Tests translation files up to date: diff --git a/selfdrive/ui/translations/main_ja.qm b/selfdrive/ui/translations/main_ja.qm deleted file mode 100644 index b70c5abe0d4e0c023a452eda5df85614d61b81f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20202 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 K_D3RbgZ~2o8}qCH diff --git a/selfdrive/ui/translations/main_ko.qm b/selfdrive/ui/translations/main_ko.qm deleted file mode 100644 index d59698e07432f37ed105ada86a1c83bc815e0e0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19439 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 diff --git a/selfdrive/ui/translations/main_zh-CHS.qm b/selfdrive/ui/translations/main_zh-CHS.qm deleted file mode 100644 index eed52b27788c46e02689a14fb9f11e17c776755a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17931 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

1^_%_V-A^fhj?$nU3=#4@Dte+V4dVJ<5 zSo4E^ucqDg#0c&8ukn5IPkS1{$>*_j%yR=-dkDLgdrY&^4OaJVx>b1%eoR1mbokpo zZG>BQiofa0%qKE5;Cv!ef*FM;#|HYQq$B)zZ~R8)Fpw=fzT7 z>&I4p`X~PVxPdP1^{qY^ES(qb>X(x2~f#+|)wd+G2eVw{$Sf2ZAp z8+)4CSX0Z_^mz+PYkKCO(j1qs>0S8AlQ$D%^J%%`I+gsr^vZTYW+*YFNr$kqH?Rw54D$nGb`ecp&hWS1^do7;}yUrjfQ^6+Ukxv$EWYdqP6J=rfz_1tX9vFhK-gdQ-!ZgyVe$dy0=v=n zCp(Zs{HtDipf6314QTQx{cKl!A|z{Dp>;2P_)Rd@l+j3QoOJ)OAsV?M-s}2OX^sxC zBj&?bfj&VB5C*Ty2IkSIba|P)CfD`yS+Bp?RVJ@9p-F$X@fpav=C52{t>2yb zNhucv+jx0g^LY>YG_V(5M3yDD8SL(ITVc~>{o4vJD;5;kV(XLq9q6*e+<|GB!1uH? z>|#c-#2 zw?YHi35s(`Kg+g|-Mc$DJD@o3JB-utH@~-&2kzTVb=g-~k$5h)|gSON1SaQo@1a ze(-Z-q3ZgcM%L@)qC=I58}T*+`%&!=&@Xc`4xH6b17oZwzfI@CVuz(WTG+qX4i9Ys z_mu8qV7ynahA$XvI>~&pkFkAYrMG)&rMG8orGJ*@;NaXJb*ixomJFjFvBnI2Ok_S< z4@q|zUu_k(EV4@+d#1g2ek?R=W$h~G*Li1Xov}VcDd+wy#^%pgZT5Y|;RF6k%@~AV zPb=|j>XU=iJC%CqWUb|?>4Vg3re5F|q!NSFi&GDJIZ@qX1id$6lR;PUK}L6r!hd5$ z+oirgnmdR0f~kkyyWHuFd2hh$;^hkHC0GO zxxe)Z?yUQEr89dC`=|0p#k(%}{TLkBt0T5lbNM022Iq1ngnz$oj{kdGWY0;mr6LtI z+$&pS>Rf?5`r{j&y+w0sbx?3m12oxuko0>$xI9a){tn>meEl@yTj}9}oo3*EJ_L86 z;L`6;$+7n9$XBY#9?#k$Ie3(Es|T+DSJhpkGRD|uK5Mdfna|qneSD5&?^t+-*V{Mf zufMq9{5ickuh;f4d4FZ?pQF8vO_1~2*y_ibmOIIM?|gVcK9KTadhutDB~KTJ?9pk$ zJ)mD#)4;sh#htxYcC4-0c1h<+~{FZ|n*7e)gS2?gTL{#aJG> zlJ@Y--FzM{zNL>a-);d;S3h$9XUcy@dHFul4=5|QrR!p&hKjQPvx+HLVe6ruGR}4{;WxdB>d`hY7Mr{)}x^j_XbM z1hMCchrsOijH>$p!?@LSd)08&_I9eEsC_7E`e4<3pt0}ueJ`eP?^%hWe zrOLqDmVKGBt5p`7i_0jxoU$M`cyu~=_xT?B2>kb>lR??-l$Foz*ThGRHOxmdf%zVD zpuWT9pQijBmD7K?{4~mSH*7gS#3_HKpSk>B)eVh*3iVFJ50o7uzOZ)bMXCqC+wo7L z-g5!p+p^;*J2HT0`JkRMJ0I@%OY<>?@(0yEm}k|mrHnPY4?e>w`(;_#;goGt+YsKv zC_7JnZ3tckW$%@>eINbtHn&Cv z=m`fKSNjeZ-HBddEt0KJ0Ph0!we0AX~Gz2!H!W{B}}s{_cnT zZGC`lY_bAAOxOn)iS8W82bdP@E!_f)`M}Gg$26ByDZf(l72@?rppWir5A*uZC>y0R zc+<}HeUwGZ`2QZtE-KScYbcuv9jzbmS^AT0*~X{kG3707CYMXGMd)Sr{FU!>Oa6WA z-rDT93O%dH0jM=L7xBiUzu?FC&II;u+U0dF?Hb~QoPiPlz77tp^nstpd4*HF5v&2q z-|%C?&|kFUJ}$wQk9VE1>*}&6LZ2Jx$L5Co^A5kAa!pJd$8YnMpuWx}Uak5;u1Pob z$$!n}yZt$Q*6i8WWk*vlcS=I}_nY(6m6yfaa~HBVf)A-UYLXns9S>;zPS0HFI1h=3 zk21cC&Yx9h-(*Y&;LjODi~kt`J}&)OT*x^zZF+I|gYV$+I(eI74r}nWH>A?M3craJ@F(&+*?W(LK=cH+GK3 zX>&=>hX0d?;45h#b{2VKC-k))wnm-VDa9ft_btDf9PsoK$4l|Q$Xc5u4y0I=ctyTf zEQtTE<81}`y~+ja8tqI;ZHP}z;`isf-Kjb3w(|Nna~H7WQ}x}E&Xgpv+#Goi<@Gk{ zj^G)7y;L>zh;!=x>hgN0>y0(`WDUHdoGF^OKitS1VBcWNV7pXjU*5_78Fir9gcSJK zgMWm(0Ot&lf0^Sxc++=Kw*{MRpt|v))YW)`x&^)uR5!{SWQJB2?jUuY5yW4i{Wstj z$)9MRA2NvTSOx48{CcUu>J6tJG^u%_XVt@c6BE0t4c|V>K0$7{?&hzW5WdTH|DtEi z{($Hqew56|#?L5z3>^?2cgvohCi~lD{U)BUG_k*-CnoWm#&v3FT&ZH|T-VppF?Jkz z%B|kJg7VGiJw@{}X&Pio(4&DNsHI7#UoDYa5!J29Bl_v-7fU=_~D_?`%e#&-f zorl+N2(jZjD*(vE!jfKj*f=MQe!eTuFd0IFn!M-+7tj z&P%Ob@jiF1RArmBR_Z(pKBVNazy5@8Fy10-RYx=ZSX~+MX>*Q!d%YREOgKha({}$w z`Q~MO(^%3kOO7nTDW_W+J)dlGy+RN3Kf&iAm-^_4esdrb=;$P}EM9A+?SS>r&yrKs za|f$eO+D5_18(qew@isz|{VapuVD*lo9&%#f4RWt- z0-9MFbO);^dJhTUp;HIMk1_iE2mJ=C2kw1jdJ49M)*#lBA4sN|TYSIXxlR1LrzsZ5 zd>%b-=dvqQ{<_M8xl6oTs{gXegSqMomA|0!U~UrIOZA^tc`z4Uq4JF?4``p@jTej0 zFI67Ux+_%vsLHK=XxSAizgOi(cF~8#T*A6cZP#9iE`!&9g?`caH|Ue` z{zjjSPwzHsJbE)v_-kDizOU#L{Qjs<@cFYo=^xptRex{sxqs0wjQ*}fE@g-6yeu)g z3HfPG$5QQ4Y$s2l$|>H)I{EMs70W)3Ubum_-P6(?H&M2PIw^9I3hZIL#oonVoXEB~ zxpr?7tCGHHjym3t`LA;Ph}n13{?p)c8-Z?iws8mw!rK~7t^o%V0~>+@Ueoy0mP zFIx*2g<-f|*_r>%*j=q;jSD^>zU{|Q`C4nAavTw-E zKS2+M+o^BKQeu#GDuv!_g(f*@QXj`=fhMxKbY5U*6}kT8sPA9&q|woj{h50~4Bf-} z&()a{);8bPX$RMO^tkJ+AAUQskOH;~-ssNWb!iyBPFRizCUH;op*hPoE5YdgXubU-V0Vo$uw5YOQm@+v(sRWj>*U?m=lD)>X%OielpT z?&yWgOX}Lx9_7Ti<_sG5YC#*lNxFa0MnjVrG*kTY->frQ=km~8V@dO0W7pnIoHF8M z9oh@Q=9#@6oc#6D=U?kXS)ABw@y`s5W{(ZJ_1<3x4z1??x)IqJatGark8*Y_SnH4} za|ar9H17>HCjC3r(#jtV)?4$Q>S(>;>Cfpd&X?r*Z|qWHviAIW`{(`h=W{g<#wb69 z_xF0&$OG0k?EJ4s$Yo`0;WOXg=DkuEoA);CFO8!Ucp3husk`bBr=#_dzPz!O`Z@4X z?);LnH$CTZ@15|`Z+g@7_rO@$qH~h{yy;1-HsQ#%9k+?lkmQeA>V3|FRJ6pQK*UcW-=1daayqr2~CF2;|B5(!7=A z_%b7!uLlP{9GCFFo;lPR)C`|%k#Whj&Y=%z~6r-y-6jlRn;50tY%ruarx_K8T< zHtD$C=orQLkgr}J-s+q#=o|%hJvz~04`E@%d3NFZ(^o7W;+!a5IjdsNNm1ULije=g zI1;)18E}chBV*x_@#wM!@eDky^)rcnW*i;$6(!wNvfq8*rk?eW7!>|MP4)$3Lb%DU z2;-(Qi=X62b#H{u!gULEO+V-j&CeCgkM0b&et@N;WNtdR%>uWDhgB{6>Ulk@me5{h z31mwKt65LFLhHQfHw`(|IxoG`;PmzHByS~u&6l@W)bVUPyUuGZAIQdS#fJ&& z2>bWOH%fAA;(Cn1*iuG*hh^7-PhsfLZ1&Y6cpSIy7C z%+=Uoy#33k)s36pCni!RtDQzx!|SOpr;;({BqWhTt&?VbIwjt*AwvHRj@Hu!_d&ZB zekaezyLDgBs<)}9+y#ESd=hKKo7fHdZ5_DZ1MSx^A8$PuTUMFijVkP%VF#x>GKUd= z#O|Bx48!hox7`rv!0N#Ex&iu5bB35XS;~0-Z|B6e)1187Al>T}Ujg@B;C?Z<$_|bP zyeu4Z$k)@1=PRrqdB&N857ka@L%#u^h0C6!Y!>=T_KbT-dZvk8Aip}pD?3+u%pp$j zV8mIrG7`Jvp-Agt%IY1R?{JDM_%A)| z$JDtu5FU};DVWe!da_u6hg4s@W#(fL`#-^4y&~Bh8!u`+QPxpt@^ERazMUw(v}65n zK$G>XOa6EtX)iFgJpaX$)5-bP*fm#C#;0@jDe%g(AK)_Qj_VW)&wXd+hUf14+CJV! zP0_D(ZmiNhH1_plX4QYQ{*LB-Eh`e=Y&{d*YVHAf`Y8V%KVm_wDPyvW$x%wRwbsS3 z{oXwnJzIgVGO2TF+Tj%Dr1(@PIqIYFNzPQ!PdOLT4{g|}x#bT%$Y@ufu6YMJHSgK3))vSHtU$| z-2vXRO|RoOwK4CZGsfo5q28Ur@AAAwQt2y6H+*0>#bjW|4%e^q-iLJ!iwI}bd;U)Mn`kvQMAAhB;&Uh}ajx2DhdCTLd z_%~YRgP488danz96MhFFXTt5T^riQiQy-6iPjTl;d?xW^1!vCwN_=@Y^~@d+%V(uC&&3`|8C)L(IcgEbxwo#3)=W{|7g2% zee61#~33 zvn4Wy{J}93ikd&eyVco?;9cEgW$Z$16kjezWxoMF+8=+8cDd>Pyd(xW)1F|SbJP5K zgUxvi{UaH5;O)i40u*cd<`Cy-(MNCBiyhEMk|dd&En!P>fh=UUgZ>yGYz>118g{32IHVuFV6 z;A76X!^i1y#<#f!`tCw5{JF2q?$w+#_eK^6iKVA(%s3sNnm^oWY9%(gRrcIm?uNPP zpmtAmZjgLeabFzwWT4CRruw0bUw!75#HYrOcAA8r@DW~4+c@u~5H7EUaET3qODu#- zMz|1H6|J{6xZcVQ4)_dd|BCRzS1!j#^7M7^NrO-HFsEtfbQgbIxp~~>ti0mnDDVDW zoT{@A3a5#IObn7g$bDmeOda}k>Af@h7ESY^^DyOe-i3_ipqZ)nA9Sa&S>VM^a2L;} zhnoH3UK`E#=QjB~%X}sRx#@xaN!qyJm3p_Q(M5m%oU-MTFZvGi+YZX#QaQMX%l|>S z;?tso$}Q}zA=s^ZeAsI#)7mJWvi1HPs+S3H_yJ|#Q$6HGINio~&C8vpj5|aqTTYpH zXCY^!N^NsZC$t*I9V%mwVh@zR$Bx9;9i_7eejIRy z`j_lMhvl>Qn?9uP$6o6!c#fRPOogTM2fcqmL$%ZS=z{D{=mU?;?8Xk=-94;5M|nZ;u7=PCwa@@rmO1U7HBQZ$5#0Q9<2j@ABuOGHY zKj5z?Hh*L6cxOT8NbE%Mg!EqLvEEOig`bBlevw^W(|cd?7U=OO>92ssUkcIU|AjZ4 z5O3IaQS6{{oUvpBKi18 zz*DV%H}=)~6ujZYk0rk1R2#Y^Kj?K~b6wgiYd()DKdJ}}|E&q`zMDx*3x1I-8a`U; zdg7zA4gbhaugZQvGz;Vc`*uLSNTHuC&aH#kJgu^MuwRXw?dtL2{o_Dd#{K~Zc0qpvJ;fV^8?=%f%SMeMzp-lPXw6sd$QkVyvzCvl zOn1!XzAWXyPu;utCGJeb&fvTkdj@v_H{y(1mZ2@np8hosjWuWN5$y0m zXdDxbgE>q62fv`m+C`kujNOj=RQgnTx60|)_y_Ru*`fvCPY-Bf>z_=0#X5Wa0ci0k z`hWkTN89tPFS7Gv*oxN1!pHOT^P?a8o{>{;cdk?V))bg{#WKkpDFMfTg|%0 znxOAr(06okTTt(OeMd(F$Nb)e4JupLe>WYS4DPZU+NxwXj4as=ea@r%Hm`qA9rt<~ zdGhb9dz!zs*JXbjq4{O)mOobmj>4I{zgZV_H+EGvLz&*VOw4u6U2{6`b}cacb)!1_ zDtB>JGFHXWyUjT#WB}dR+;*IIDX^PapG};H^B3Yv7g&?1pTxFGK{u_}$``hGW;`6y zbzOtlTp8J1@Pd)!L2Ry!VrPN8yMx4&T=-Pu{qJC&c8eCwQyV_yUE()jy!amaN`KRD zSRQYO9>!k{^r+>_?@|6L_01T~UT01A9LD}@$_fFkpvTN7p;H7M^O#^W7k?9dcpJ9W zmqYG@InJwsF0x0^f$&##Ry_F+;EOj}iNni(6^}Z}8O3JBfT?fz^_`(7ss5|LjK9;< zn6WU^$9eCYxp%S|_A6EAj{&?)iw{q51aA_srVD4$#KZ|7HU5k7m+5~uvg2+XSiTes ze|d>({NRU{_;S=)<4jT6LmT|>4*99(d&D(zhL162C%o|o>S$iXZ<4kB;NtTx{ztdt zz0A18ztINQyOiHX=`5+$J%jijDSVF({|azS|IonO(I+j(`__rXoP zJce~ba+DfGj#?#00bN`FgHFGbb=CL|W%BSmc>H2#Pvg(U=k#mkWE16zL5nW7Y$IhF zv$aqDUF=!aK|Ybs3$@u>(2I)ah;O6RZTm-0W2$%E8t=`pynU?C+YLT%FVEDI$Htsn z-oA=@CN2e>BY>0JhX3RH6r3>~?=@LC$M|rLwr~nYMtb4QrJh-zfU^%8N8d))ES%2c zyao%W#)s3RzVK5s@Ld?6bEx+*#%1;MPpRkY=bG&O{3c!6DZ5KLjd{!S&aka}s)<)f zmyga0A8U6f?0mi5kDc>2G%d3)E~Zb(djy`C!`|2?)+Rq*+238(qs>{}X=l@)<`0}I zu`SB)veuiZ;k&F~0={CK%HNc{J&QbPAHEx!us=84#^$#3eaBLA=)>*UL|2 z9}gR6^9IgCB=~Gjf5huv9BJ=aT+zN|hSPBe<4}SbBXzSn@f8_e~FCPmc}pB{`GEf-~G;M#jg*xH>6y_X4mWDtV5DK zHJ3RXEH_i}m}I?Q54}I2J>8|)4DlG@D42;s>PnZ|+&;A%(8nVW(dPQ8%|P{9RnO*K z4pJ|sdOQ_GfbJ>%#^3h0gnkQX-NkQqY^PA(j%+DjCcO~e z_e-Be-UDrH-H%XLHr)P2>)W>=Q_?N6`*nA=bi}tpzQNtJ`w9Bxt;J4pi1doX{yqC6 zOQOn6Vs9YWdrOmRw}HIm1aIZ-!gq?1-|dnoAs^VmZW_zpd?&H1I$+JIs5Eba?nV#o z-f)VSL+8tn%FT^<#pw~_YvklxbrVa$7S{f$&e26!Yck;T9=byLcn&tih131KJ?$BM zB3KW!f12MI@$%RbivQ>5@X6XMU-U`diP^d|;%UD!KG*TKG(@JpMw!lX@10A%%h=Pr z+$sKY59xT_cB)9N0K1N|1@;E{7Kyr*nJ+keSF`t{qXiK?R%c;9DTdi znhT*B^G(iwaZ4e=etKltka_icjy+lU4#|#7I(r1G9{+P3buQw*J@yWWAy+PQn4h;8 zW1fC1oiWQ^oA>c+Z{VIi-ijm!o$k-;1Oq1y;!g| zr~h@}+MNF>X--k-Zdwcf?Z~UyM0}U%j)@J!UV3 zvhz4=nY&W^?CF`i$QyOh?;~`-^LD4BiS>tA`m&uBN3`qh+WdCb67ciwkE-mH&x(G; za`;@q|D6?|ZGX5PSz726chR0a(cs>f+Uy57KOr1O>k}C>{_EnB{~E9CyH}$w9u$@O^{- zz3r^vdxU$~`91gP;Cr+GJ^Crz-v6GQVcYxP+fKFZEBy8e+ur{koo3tn-;*D=?fvg< zQ*HZ7zx@>3-v1tLwC(-x$rEjR|9jhqZTo}#_7iP;|9kX!+ur}4Jl3}Nzqd8m_Cx&k zQQO}C9zDjk_rE7O4{7MR!T;XIc}Md-GSqKRJjQ%)_Pg;{^M!FYV@t{8;m+ANLzmRXEA3HzfRia-NJ4n^=zNuo=(g(<7YR#}d zzQ;$Yr`Uw*JPVx8-FDdoF4>m?g36*drsD;ihsy^-okmGwG*7P7EWPkpK&N7S2C>I5%22Eq&pK3kY~=+kNL8k^*O-NJdrhokkxz**B5&Kl~C2;iI|`sCq5 z3nzE5*J9y3@5A|oh11d(&T{JYDE7;o9xwX*#ph>(PxN5#YzyZ%KAcGwPJLfES}#p( z8923~&l}Lk;^R#6K4Ibf%7;^D;S>()L!U2G@1+2rgG8S{`21t=>73|IvTz>p;S95I z*7SvQ0rh?vz}b$z=;SRb*W#0&=+#*`_xo^miH4f5mcDRKr`|mQoIG%x--qXGqBqRK z`H2r_i-m&@3(xx$>gjDH(fI}7#DC}CU+B|$qPL527GY&Y50yw_} zPWpL2c4^=^6TK}K&i8#dofgiTzHo*E=aK-X(ihHS)Z4AQGoa5oqR+2F_(Ugo*IGE2`fz4jIQ4zu{6zHvIMYO*$Njs!3_iKz zy-O{eFZytrEu2E64}Dfr@7Vy(heV&pLh{viyf@pz`J4~uBNon@zHpXM&%7(kxJHXU z4~Otc9`7|a7m&Iau`JF5gxwr_tlRk61XT`EU-iaO(TQ`6Bgh z3E=GhH1t^?;%Dc0?-&c`6F!{xgtO7lCPvw-pFd5#g#ny5fs^J=-#qJuPv`O8VHVC5 zAI??_XH8!?AEVwE0yr-Lr!yOZ(>dOIk8v1z|F93|6$_`OFP!nfnE{;b(<|{a^nP|# zb|pUhJHS+aZyI~)cifX_?|)9&_#^q;<$KmCays;`!6U+JS;;`bLPX7C_y0a^`j@bT9hmhlkoZ}-Uy`PF*I~i@eKS;K1(|=P!q3Yd)|t_$`WGsQ*8q2S#hpkH7y#UjO6Rz@nRN z`vuchW2?(vKwIKi!{rJW>?^b$JOLh? zW&4E6eomR>B~J`mZ!anq7ALR5fBU*Fdnaf8%y0B>EbU9wXA@4uz}MQ9|x$zV?@Ut$XV=ckL;{JS@m$Ix|#Xd3XF{+_G4 z+^aR+DV`qmJ05=5xrcaz=@0#Qgjo2o4D~>U=lBG z!|!f~#<{8?Q&n#`a1S-HI~MAdwHW%^PD#MC)L?j`ZqcN*&MC5 zA>G?Tzx!V8X?zYEW$>*8@0y@}j<|;}L)F=1ksIL_CpP^7;EDeaXRJDx>*piYWsm=% z>PXINv!DL5>eOYw_**`w!^5J>Dcrw%OV6sc>I?r%eO(mvr8%{I9cud`mK_QDdY7E) z-_lnGe@pE?+fTdw=oEwFT5@(y_i@}pJFP33g1>&o`)T)2+pfa(-U`~iLc5=tc0Nr$ z+E2T@ZP(y>uLbQk(Qd8U;b#cXakO)o3+|uq$Z>XKD|%VwPijv2JA=Kgao^diQ^sVE z*O=jTjI0l1qvSVkM{A>p}>G3vMP@tgQ>x%nKE{jNUg`wGS4c3D?+c7kx~y>4?Bf%Td#LMRD?%UTqh;3k{E-b{JBKpvY5p9asX69v(IG>8 zsDb$rPHk(3dJmzOjf`S9^q>!NRi{mjGf$R3qR0R?F6T|qE6~~OQO#fnd9yU`G4q z{$&eKJ%rKO_sGY7)6g9H5F?a5V00FGA+EIw8a>MI>h~7)tJr9u-!9qFv+6gD z&Cr%OitGW?b|>eAg0|~uYx(gU=E3Uo&oZ`>t$}}lK6H((0o{?=--A~ky6^^VqiwJC z-(N3AW(`70`8O1MtYz)%S;s;#X&a{^mUBgug&nfr2C3jD@D7WKP$K-a{+)SOvQa{>bkzaQ=^U%c^su=GX=Ng9S zy}jYupUJT|x!>qH;`7knlo5NEd?s!h*!M`>SlZ)qX{Wvn{I(B!*Ubp}N}qt9sLEc& z_Z)lIxrL5_i%n$S@VPnW&dx0ibm}o~lh*4TK4y+RQSBKC2V3`a-k6o&lBa$F{0hL* zTgq|GQvk#Hpxd-V>%7IqIn3L^KJB^cA3n%0alDJQZeHW-e|X_?_ZZ;L`c(T;-YeU= z_8?;;Yrk$M`@G#RMXuYmvBG;Dx+`DEfp(vQj=Rts+TY`T&zaJv%3uGcp+R+ajNu#l zqO6Mf^wrk9>;4Ow7k2=LIYwcSMquQUy0(UKUJRk2iz6{KL4Xo|Y#<|0KX#1XS z{37yV@{@%-cbs2{-K9F0u@5;H8}1E$!#>jcO=cgSb&)&e&6^EtxYH}m`+z@eWz_vkh8 zEOTB|`^icF?vLE(fp?GQ3tl*VV1A6vaQyrd?k}Ii+0Zujc^uk|rSIcERl280yb=30 zxzhCIkYjQ^bD*;{lEGx8?^rhR9-+pV9MZS^^b075pOc65E#IVacsF@O-}2Khq#S!Y zi7jo$5%B*em6Ioutm#{R`bCtJTav8nTfRx<3Ztb_iexK)3iU9@?-m!kN=G9fBX&R_fFQg^oOZKJSz-;EAW@Jkh_Cj($eB|C=cgE z?mH#Rd1@=Xk@^~Co876CE1Y6(==r60EuM9X1$b0ErLzya&zjLLIj$D3&O-LrC8oyr zI8A?q7k90#^yVTD&E$A?FFUF|e<8eZS-g0S)?9eN$WZh|FMlPtG5*5zO0OE3aFB<> znU2|?Z{&V}!d$mQd-6LfMznK2ds}oouLJX7>Md-`5bx_;qC4Y75oPO z`}yU1599iSot5HA`Y`-|O2GeZe}(^%Px(iSpttlvCwhET+ebZ}A+3iF<{cW=^Sz8E z&bSlg)Z~WNGIn=c+cC)TQeTdDAp0$x-6`qEk9tQiHu1iE&hUKCyQlPCyo;{4F{nh@ zJxj4~IgKlMD>FBlxqOkiyv1n}elEIK{p|WAv^#`6ppBKDWU=cO**w8ok)G`RgWvPC z6|G|AtdzeoFz@b?okMO1^CO#JMD{y&esYI;e^MX8{J5)2xR%FF-Mbk39sWGF^0ysR zdA-(Gsi*NAb|77b-NhRZK@2Z*RS8BpUAn&RG&Wro(B-HD;cWUw2~OfO1H;i<#V_lv zV&PMLAe@_Lmf*Ov28I*k-Q?S525_PW!Z{s1YsaPcRV{7G$CW?NX*~Dy0i1CM!Ws6( z5}el1`%mR?GQ9sZ;)}qsypTM}yIk|Aaeg)4(K(mgZgK^3Pjc=~`})5|hWV{APQE#O z#_#0tiWfLTi5v_W=B)fZeAin4F~fJdmwOiS+{+g$N@FUY@4Oe1iR8z;GsKfKq!;ju zu#E&$Z6tt3Wi3Y!O8>=Pqs?XU z;z8$y+BheAN2(wEmw$ea&zq17bV)rruKC&0{Yt^zW4-sm#-D*!=;e3SCeU?i6M1^z zHqIv6K+nv_Mso)Fo>KWH%9H4AH?Vgyk8%I9>@jS@upYlThQ4LKWRFfI zwkO!;_h^YH%jF?9*0-gs{WSg&#h|OQPX%L=-SiRWY#_U7EBh_NrQB{3F8$d}fML?ds2Nn#bMm{n<^!ICI;d-L$Tb@&W9o@#KwLyD9a^(350W*>S)9F!yE*J*{1H zt2z~&Vf`XBkgS_F)NNp1r<2pV_<(u!-#=_)y=}o>>(3r*fABy)OZQ`+9p6vAmGDUg z_aeGc?w7#c{Wkj=;==}f2<1m~!v~YGv%A4H51+_>N;$lJFbq3wSYI9)#(vnsr0#X| z{nYO1PBD1{@dM;rzD+kVjSPKp&VW2t=jcqe({cAbJ*(=(tJF!s^S_55#lJ%-3;3y5 zcKYLmgL!}SfO`Gd)VKV&pL+f9%EA5B>&NE4ODg-R`)^_>V;B|G+yw8PI>AImj*6ZzZPE3t13qMxs0 zJq_&GSg;@DJkZOBz4x!B?q@%UdKT|fzXB=yL~?TX0Y*h@eZpUe>-r_ zT@xCAdW!eW@c4mWKK^-MDvkfj(D>`@_#N#3(D*YK4?O-1zCk(vcmC+W`H%gm@AwyK z{8nc1H_lRjfxpqlThSVS@9&)_c{74BS)U_)XK2jafiz9O8y|jj_Or5O(Iwhf;y!J2 zuj)_PTX_xHewqLHChJEwOyym`sou)MRFAu?Hmg5B-cXgD1gvh_+j}oA#wM8jx~BiF@vgXRDDyw-t-rtzcH8JM^1vTOHMXEP%U-j!>I2eoHu!jeQZ02 zCqs*-WufupCVPhpCiF1AIKFTB91bI1Wqq#qx0PhLopz?*c%%1T!qOq$k(oWn9OA2% z&tcn*2hO2;W8XPE#PV=IKJ_i%>TQqh;mhxFp)v1;*M9$v(3s;N^_~Y`jT!r#xU$0; zicriM-&*m81$KO$CwLn|ira$ z#7x%Pm`S_(2*pg&SF``KjQP0L!7nP^o4)QGY2x!&JnVvP+3`zDu3ak3kZTyGDCw>h4=g_Y8P{qir&3mu~_86~Y z4)H;X`4xS~vuPsZ3D&hPa*YPk{~6t9vl5`)ysf5>T!6HELBw9J8{XsVb(C-&~@ zq0Whp_BboXOc1UKZ04chn!9Ocdw%KgcKJw&i38)1nxOp;|GtO+(tNGJZ^iFSkHwb< zH~rlfb(*YvH>ys6bBwny1{#~rp$eAzj}3Bu)SR2@C(f$~tf17&B}R9|-1Fm9t8oKw8* z)VJ%ql^x!zwthgb>}1Mh=WzOdOz-OFa} zi^}czUKB3|{Ni%n+`@l`^8R?bm2>JlmsWa)uaysZ6LXH-$KWFyhd!M*&@#lK3k{W? z>K0BxCWxgT49)OqCn$G8>rY{k({bz)-*=Rp3KnM(y}}}M@A9^|{BPhcscgc{y>fLTJm9aBHQ8@Ve+ToHVP9z=ImjR*;W5bPrtX6Aoaao86Z{d` z*VR`U-h1G8`Mmw%&AFu6cam$cZ?1AlbobPl?Dg39+S`taX59Ii8zGt1-AiM#dP{za zcs!8v>Eho&&U3k*M$K1XFD#Ealf6?^7rMU5Cp^DA#(JUlz50MN8NK!6lf4sEAOHFf zq53>>6tIl_`k5fyO}G|c?h;F zkfp+jrEOx7hPa|+IJj<&2ePGYfP7`W_ZV$BIv&s5sqEX7i5^_}7|L@hgZ5q7@s!mC zJmSiRQPv-QOlM!`kJ(4i&Zi8SF28v@xqPq9A$D>8-0r{3u{4w04Nj8qhhTqg4rkBy z3%_qe5I338`5E7r)_eZEb6*7QcGB(_fTfc*R7W{D{r1n?ssEqRzxc6F|IB6o{rj;; z^?!i2Po(YA{%@miJsU3k5X%+q-b){s@xPdcILP0pA>i#?V_LrfAMO^+YyLS6E#}?9 zXh?Z5^0!5sqPyvmC(x$lBTJ{IbxgpQRyKgc4**~BJCpl)XGqCAJ9#G)&}Alwo9b-; z(y|LFE4IBph5VU;9avnpoHBBF`;;xCELT$RCd#fWDZ7EP4wd=7#JXtYob37F{MYvG zR=c*fE}A|kdp2b+QC7-JOX#<>j5Di_;lq=uSIV2G(Qj#)U@k3twI6+z?5Ezp2?s^| zcP3*yUHtcxUjBQx#!I}URG-{1p=ZIbOL#5x6F>f48PK_VPx0@1;_(ZQiIe&?o+KWB zfcW8eyO0rq>|2QJ`y;d`Uakq^nzGT4LMvVHP&Dl4&Umajj~!uJ%?Mu>IoY=u`)>c0 ztUEMlUp)Co*mRnF#TJY8OJ8ZEts;Dc>&$I^#JA3 zyBO0a#Q&_}y7k-jf{>AQ&^r$d~FK)Ldl$Pb#QwFK=)-%q_0sP|-O z+`hIDYXP)>kg}IqYl`jF+4|%5r8xh8bU%2i;I3Ka^>F6Q)+n}HB7Kt0@mb86rFWY< zbmwotGfl&@H(r2FTu6QZ^wS>b(Ca9Bn>%Vt=iu*@72fSQEqe|11e@mJW!}Z_Uib3q z)3e(;PtRV@{$J6w@^!j`J^Yp0!;e2C-1_+f6Iw=T4?n(86n)_0;qMbnwE0*2&E58V z?+;&X*V^`z-_S)HT4#$ZSD*g1Prd+Gc1{n8CNkLrUf`i{k8}P9mBKJ)em)F z;@jb1D!-o<4)7}u0*lsk=2IqJl80_;patatxb=Un6}F>`D;ST7n5^bXzCi~$e%tZe zLf8Ry&{_kuzY4pGJ)aBepmF`L3+{<5eor}wvY9@)+n=|lp}q7eT6A=n=ibrJGUuUy zZUwJkT>y@!{Wsv%I;Mkn^?w6i_nw3Iq5$6JSAF;|dc9|3t@ML(qdq43#umKBbYCMn zW$r}xdHCf3v@g2n0HzIB^X%V{IU4^g-WOkPrw!Qw#d(3O1+LS1Cpve@#uKI)KPRsO zznghFjW(*uv-IdA$vo(6q|RyBA&f7^dMEzqHqH;Jwdb-_=x&au?HXquA6tEM=5$W? zg&lx5d>gegcMuv@v;5fR2d zt`Fj5z_a9e_@a%u(w-&bi3Wz{bmwRE&e-~BTTP?x?olp^$Fm4<%T}(2N1lZTO7-mD z13DnD)7~=|qJ9GSou2ExpUAf*k;60G{i652ro7VP`}JreX|g|U!Zu+oOgPK*YnS>u zGecW_?!)69;Xq|^WUcNGyI_u^(~^5tz=?mJkAMGMl)jYv3Er1VZ)SqK=T?JD`B6ud)Z{~NpHs@mbRAs(dMs*a`KN3!3&y)G;MC?11)#Y!YAS$ z-9C5aKEgZoJfDM4B-k^wE4bCJ@(i3kpu1l*SAx;r4+rcIfe*oL%zO2Yy0gg02^`o;xW5`y~Q7ni{c2MTakKTDv{kPyD15PzJx^vxQoZ<&^jQ_{? z6+Vr9B3+w@ewP7{@RQ;r`5F^VbFW6}eTAL$&6#xeE@BTHEi|V4LU36JE|biaTe^zH6JP#JbiZnJO+PR9<_eQo|79NYf!;YQ(B^b-T89r%0(d0@h^ z&nq8%PT3ds?GBf>yhWeSvDaYRKjQ2Cy}urIpQ(JCb{Dbcuz~ZYzVfF#UwWwAyqFA4 zZoK-fPrZ~1O=$c(<1ye+@82E%(nHnYY(wawhZ;gl46tXor^_30%zo zi#iW3wc^$;b0pSSzv349n9)_gVkZCN>&f@bT>_0r*DfD5w$Z<*ZhY=(;B6RH*=TtH zIO>HH{L&=-C zT9FNcZ#TBM;1isJ(deEG${k((1Q{xM=x`|k~z`kkGqU4kA3+Q655<|&(xy!3q zHbr(zMeZv#=EW61uW8&?ZZ21zaf`n6#v3p=9=B}m8{%4ILP9I4=DSR_@O7{`y z$Itt$$nB@S%Y(Wedg`Gm=0)+7zfW#b?g;I_pXVP@Iqk>c*;S#*`#PD|jM`;9sSx=% zw7U#>AV2OY%6$LL-@_`O>8+0F27`JefV*J=7mM=?XY^I!CRO(^#zxbB5Pqc?( z;5*@(;c@1_H3C07UQY%%Oa?fF-e#!1IYXiCLp=-bfi}Lt{I6#IvD4bCDkS-T5}prZr;cXB(jR#&Dec=p7q8)e zM6`{4=+FMGJIIa!_@&qWH~0QM$$0-O z_x{}qd^&Shd{S(`ejnK2xx$<*q23$R^YQ+}mM!p24Y3r9dt~OL{Fe=v#GdhpJL}wC zdw(YW611_4{i503_<|p0?qsLRU-~UDcHrw-IWUNSBpcBBY4}^vu;tN6aCe`xQ`L^6 zpBS+22iEPfcdVcK!pgO6_xE7X#YoZHiIT zzieaS(81IItg@Y(X;bvuU!DIJeB{6>AMVKP|Htsr=9WR>d(6(a>@Bwk&)KgD<~f27 zE_ywHFWGT3IhNp5d)Z`ng*@~Pc#uDwM`oQx-fw- z@wfD`?y*Wu8J3+y8``&ZBC<{Q6n3C9bx&b4ws8~BHuLU;JGrZ`L-!PFO?qtZ5#~>L zT{%Lw^CaSSithmfeyuT+xK}T=T=x{Vp%2_Wg)!`9-BXC1WPLBaOYiFbL$$BCpzbSF z+gaLHn;GQ3vwN&v2X5?{mBg+@QxU}yB8nyS-qU<^e%@Aulvm}=I=rrWbvIWPNe z?hEA3-6h8n?@(Wg0q$OAS{Fh?O}w{z8TsqXjn3eXa|dApKP_bMAS`PP1$PiGI2<=;`>Va(LGc3lg;IvDcTx8-;5C~_@YaV zG4t{pH%$a@wdCl|*t;c^Z3osS`ZFCh*}Zk9`w!5l)nwlO?yd`Eb(c`* zHKzNQ^k>U=gtFrA5N+h4%f0B5^7D{4SNig%9X&XZZf!>oDsE?W@F4579?$&q3b>yX z-K{k!y%P_7BlD+YeWTPq>=f#WKApU(1g;N*7aM~kh7kwPqbbuFjZNgTnDqJC(9RlS z;M_k|7SkP>vyrE*JB)cu@L)3G4dX6^T{QcAS zm}mFi5A@!Ae{TNid(?C9eM|3M*7v=^&fRtIHSyjG%_p$G&9{?jl6UXG7CpMMwGgM& zU6ROE|K40^`$B9{$-mX`<~_)fSI;)xL!gIPB-VN%a~~OQW*B7X0p^%*D=)c&vNp}$ zH=QM|dzp3DeGK1mJ?}3x74|IYC)gi!XpNWsNcaLLcXzJb@!qU|77zC&Ll+tPlwMS9 zMDd0@6MpPfw(Pf=d)d3q*fGdMVqxQQ|2Qg|(>|nex$~G))q9oicFv2g1_!#gNq$w7 zxlh)c>`mB)zF#^a_d8pEV(#6+d%xnl85pDNojZl!9mqlPlAEs%?vpR2kGSHY)Dyo5 z_X*7xc8v7D#-Z_OEbZKVC4Q5<4rKLC>(63)N3dy;InSA*yMX=kRexe-wce2}p*ht2 zeG@#m`BT~Vm^;NHUd8V5bs&DE^dIwPfQz%fYp0U0Cwo#+{x(YXK^kE_^R*3KC`~&IOcY2j`K!sO#9DgV@KD>N9^0zGgHli@xW~5-Q-?b zTH04?l9}=ANx;kAj?y-U)%3QJsu;M940qUdjWIJ1fn%Lt(ZRfG&SKMg{WPnKr^vSV z=S^pNJAM?-zK>qGgL_8MwRU`$LAx5KrMcJz*+#}`e|KDvUF-k;8|Q#$@8P#-IK_U7 zJn^|m@w@@Q+SXl|S_kl;bIucl#+9c!Ay?yQIyNV}jCLGNx3e#1Al=gpE<1>cS-b|a zv8kW4zCF(VSG{6AvYIg_+0Rjm=Np3az61U#|V*B}H=C=!ZMLX7!FK{!i|vD4qB8-eYB6{`F($y^L|pE*h8N|AU;#>*jDnpK+P&PGCyG z(_Q_HYuL!X{SCplL*nB$3n|W9{9_S562anN@&M{P3F+Y8a@-O#O{s`q&{giK} z{3DbHa=VZ#NgVE&vZei$3I5V||DAd#AfE-7i&yx%#N-|;*bQ164dw2l-fb_371P@J zFrTmTF z(_-z#HZQxl+-pf;zkO^}sO6SjWqgLTEc$#o-$VHfYq|bR?~XKgo# z#0lob*fWvF$l~PVT9ZPr&eOMB?{#|E*T0_4;mD&agr^#Gv0GOm-np^*RQBPM@3dGp(J8@!8dEqs*bIamI%l`x(WLp_*tMo}`%%*>Ym)@Xt z7@lj5I<)5e?39t|d2!}s5C6m1c^8`4YDdyXDqrVS$_E2-V zL0?s48^o}4*3zbY;5kY2ckG=7^2vR>e4<^)`|V8fYpJI?v!9yXSh;xia-F-l0Y7aY z-_49E&pK1F4%B+$TXps>DCcXynT{Rv~#S+8y2`Wb$A zysNK$*S7yynGogv2Gw(X`W$%GJ;9|s(m|Q#@*;dD&084yNisIWxbJ41^^C{hzciQ- zKCP+jU|qa^dGc|dzovLG<1Zsi4DY>Oxv#L9?lO%KiWH{)`weey9xYM1UA4Raj`h|fsKm= zc@z=ZME~TDsXM@-;(c!ekGoe{{Bwvkj9x!Pc4}2^0$;?*nmYJsH~ypMShzm0!@u)V ze#Mc)(wp4<$he}(vg|p~QG0!?^)7Toylzf*hW>MAitMf3w?GH@fRat`1lKBado*5K zZ(1d%_6!}`qJ0|Lqp=&mKaW2-=~nt!&%Od;106#zZo%;U+v9v@uNjhkdyAJHA0Em& zJbnvtp-$>m0`F?#O&W(_dkGj_F7pY%lMyZBs}|Gp#jL-ybN-9Y0xB-wOU%igy>SLK zHko*rTffL)uMedAGT7@b7M>p@-hkge<4&IY_BeMJ&H%pQvDWtl_}jyOjoIMaNoI6_ zlTL7@@iw!b*0I~mz64&Iz^T3C{xV`nfnU?Xec{Ejs&_r(IqTmP$JpPbzazx#tsRUm z5Z<1q@73T+YjJU8Ni^^^))Ft%8qW>j($;S)&pshInOoVe(ZF_1_H*xWdvNctJAl%T_(TH&EYs_AALRP(8gfP`!-m z;pf<~3{o$udhA12y=Hz7)L#1t>XDbDdhtQ(rBtunub1cdK<#;|2Q8|-%pmpDhG^I0 ztcFiPll|SJHWYqI!s}`)UviJy>Cm3egX&_hft%aXd(@_>j+@Vtd(RB*nnUZ_d#^_lvPwZprbC22xJqzY0UUH9GnVz}%D7i=NDE#T>z|&o6%tsseF?<6&j_`RFj68HfL3|7-p8QJx>D zlaBH4e%(i%Jhp@6cr|vw4aCE0PHmeleWN&a;?C;%QDoVlc~3Tm;@?(|H4V)I8+VhJ zHL|u@+Zp!t$Hyew0-Gf>h`*DOzk@y&U3r7mLpQZSKeCBT+RL8Ed3hbgFDz{jqAS#< zy>}j)G!3pJ==>(`#xm%IVma?@Qo$Sz=6eP6;b4mwtyFP z?x%t)&DAFC$Q0*deupx(`zL)f2F1&E;|rOHc@f*H>gdXE+IXkzhHl<#*8JOe7_mWY z4B73>Q@8dhG7dZMGtgg(x4ljL>;Se=C$w`$#{V~2lki;$E2$Iwj^l?%1dg!h@UO%t_8tK;XUHNS>$=8n1A zY0}5pyzh<%=J8U>r;rDZO{sivD+6OZ$L8*+qa0f}&w5eM^QKjOMOb@N`}xnD(Bg3( zrpB~)npqHiDL&hq2A{FVI5OS$pRt*(eAYSn>eq`5xAOV)Gm#^g#=3vYcZB!Msb;2V z$Yal-rnO`qKC+i^i96<4car?h-tKY)XwE zczh}Lm2V1<-PcB(#K9K9o+oGTa=|0`Rsoyf(*J1N$n5uN`zwq~a;JTkS)j4C;fv}0 ziNK`snI6R?6(gz0-OT&6cVc0DW<)lp@deL6)@SS^vRm|QOE~*M#?#LF(r3<+4KObE zHgl^nEBEVV-tp&kQm)R8n|a+(Jno`(kswxB$XkuGE~5FHh`r{;+-@T4(|wwpXNGtyvfkTW#beE%1IaQ5n%&{RS;3}w~CkzDzy zl-u=OWRjhGb6oZ^be&|OhiKZi0<3C6G6VM&UB6Q-V=&H z(^h^y>s9>!F8{?Z$-fZ)gI}yHc(rGSXlO=1@C*Z<4z;`G~mAgzR>neUX0KfeX4M zFJV~r4rCFr=B*FMhG%sruWT=^2k%9&>t&Ay@+ca_ z3)0^?HV&BoPvU?@^YjcbrLhH*DwmxRG@Qpi zbhe=vz;C{PEc~e_`ojMpZTt*49o!rE?`*0sa0i*}G}o?hCi=P<|HaC-!1fx#`F5qa zYwA5KuOQRBNBj1D2Ysy}Gx=k+|V4(Qd5?W2_0IVqaIt0=olFcg)|r|f*;#J4w33G*qpqLih5 zJO0FUV82F4!cJRcy*3sX`QcAhTK>u+=j4gh^uuA7dofd;a9$YmU`k* z$@at8SogvQIq3KB&tt8ZiWl)~bT^Fnt7lEwi$_;_El2B=#~E(HS6J7*WJrtj^xMnI zTh1qb*lgD)LM>)SaQ+T9gzV)|nSX{i`|=jV8xpe*HI%hvYPsv`V~N>Ugu~gl){qzV z9y6n^!OLE?h1^!=s2cis1Kkoo8@f@xHMT_*-aBEre^vti_PQ9)^5l%%%eqep8`5BJ zUP)W%&(?18c#cdlvz6Dw**1-@V@pXUyviDO-EP)FiGM4v=Z$H{wcLCcc)2W)-?Imn z$I)|;-`IGfq4?Q@)SJbf=xcnx{h`yG{;tTiq=_AeL)qkJ_F_;TpTh5um(A)oa3*WK z?C1EMU|ex`?i6_V#`)M_*kARd=XS-wB{9+^?I)WBSAPuLKm&8w8q#uc6beC+l&04Al6vei`9e;9oZ6_-HpF#>ddTIweVVz+cJ-sEx3)Hg^Xn#CW$_X zpJjX=dfy=9zYshzXWy2d!+!gw{jSP=op0OToTB!yUYHaT`^<8{4g`F@^ADO6dJ1+N^F=j&~lAaek%ezBow-37+ zT(62*7@Og(f!=SPZe}PBc#xcBCl3efFRuQU`R(TUJy5%uGpQ%}8PHojb=`PN=`DE% zb(aKfMh0#(bvpIHrSP5_tlnwVLx!tfp0jQTf-!}RkuI?94OZ{n)Wfc^?VU78d#6$l zxoX=Rtlqn*H{Y+7`QG=k z#>jC>f@OsdhR)kyIPzTb;mfxn^YHuQ>`#on5N$NTEPF9|ikTT33Cucw zk9R(c{psF+R`2tU-j`muFKFXhbilPEIb*MZb?Tt6(z>f$f8tN-%g<7r;%4kVyT=IJ zffw0>>kKiV1K{M^DZ~{nWB(F;YQDkA(q~ThVTeJaHMA9_E%7k&H`_UMSc}f>SI)Ny z;MmJKYMs3A`%05@=K+V}W5lPr)y6{BzB-6AXir)!X|f~UQ>2$Xe;$%MkF~|xe~vwb z{;Dpr?`L4wE(hPu=t;?z=-!LEU!hEAuBuG`f5-n${%fwCZSzjb7Rug{>?>+7Lb)?lsjDPrhJTWfn0TEf1qqD<8`#v zOUL7LZ`|tZ?rWffYer6LTvIx(sORvu4!r#w+cnuq+?GC_jd9<< z^tCa(ANDNxyKG!^&NAU1T3N1d=;BU&LkDSnBk%6kH}kw!-^}a%`eq*2@$LHkB>n!L z_8cwU#Q$PjqMiP9543xJy?Z_vddB9-{HSNaQJ%Z@{*~v&wn6;Ko&{m(!9Bl%=RJZS zxWzw?whc5~u(7rcJ^M$3wTSO7=Dlx=S^UHI8}n=_eeV^|Oi`luNj~?Fe}b9(1gjZ5V&36FS-rUBsa;&#=FnJ?LTfpr=Gn zfnWFxF}8diah2sOyTT*;K3mJPI5ZIB-3Wb2pG7$9=Nip<(Dy?6zK?p{LErKn_P{@i zEh{!fU$*ZF^vxM-f~C;+F!ep+;+C4_^<5$QE`Ih5`Qz$4S<*NA1M!t6=XSi=vr#(E z=f_F8S8wrUusc`iO5q%sR98CS3q02xh+n@8{3+&MI7kKi?Vs$YOgROCJypoX8>K#j z^RX`o`fxhtPWq6JQ6D-RE*|s|5Bktu!=+`~V=NzS8uL{+&mvgc;yjdEL!M8=UEq;D zIf|*=!+xCW!Qm_D#Oe4U3(b%v7xTTEa&l|tef3&r=#1;o|)Qs%i^hzYi%z{ zndUG_48`qX)cbqrr-j_HFtnpRk}f}8Yit|Xm!$kvt+fVuKDm14w3E2L{LS8+xw4#> zMsKq|x}32{u13i5ki9ecpnvD6a_1s~l{S+%l9z|ST+P`RY3RQGMq}k%X5zp$Q1b*c zBgz=He%XoaO=GV?o6p5AEjs&!d_X^^&d=jcZT4YOd_XX@!_%qle%Xg84 zwn=hIQtvdj4-b0z@3e1dUuzB;V<&lr&74p9JId6b`YrSs8*S}1jptVUoMbJ1*8b1> z+>NeNpRPaA(U0sn$0`?aT^w8|(xbC2lxa?4+=rDB9nq)OlcYbI*e|92i^n_A9x8{+ zr|Fly3RV8uBUQGqL}X0%fuNrzorzF1m*kAC%L7Yj%joQPk#m`_Y5|@ER83)_tnNj{d(Ji zI!&Bw>gc0D@97h>pI{s*U=WU0lW&)ZosvC={>B#JC_YHOLX_OL52FKDT_2h)*ci_| z;@ulp-4vS5xm%VGHI}Hx(sp7t>c+x3Z>k?u`y0kqjQeQFm$x0rxKi6gc#&;#;KiO9 ztLj6uKO5lC;qM@Jk7S|y|0e$r@V`G;_Mt0FVfikw6!S_Z@VnajfhB|Ad;6b!I8wo! zxqg2^zuhWBUj35sikFou@+Ii4X3BBduh5TVhJhxEc^NwQaYgKY2k@mz==5{Em%I_W z3wXJG5ISv#PL&&ZVG&I>izdyO?43dX9VhhdKTm&lu3}@dTj<~Mi1;VbRGYmic(2Xg zQ(r-D6X!H(KTQCS&PUL1hlkG#55O?R!T{}4e;@U$OXh7Y^$yxPz7B~4hoip?a7gal zj-8q;p~3j@fpv3AxvJn_*%IM+P1%Qq>SpIYI)gmzD+%;rDw?k;>a-PhKJ zEw8qIrvJKI*q-xJm3ycF_rzQxIVSrW?~1O|JNIIw=jVH^T6)!)2zsSgt7+U7EgT&8^_JF=qAxtk$oy?&#>R9 z7>@51;H}CfIx4b%DjLZ?=`VzUOc}UCj--CvOlHIEBLRrG{>UPMBA9`#^7CSVT)_poEM=_$^FuP?hopA zl+aOeKaX0x_&QE{BEdeuyMp&TYzXx)*yGp>3HH9-?%E{(I?kAPSo$rZK~s^vDX7!N zSt`N_b2sl0YyMAA_Ym}~JQ3}Y6Ts&O&p&Dm}VNrCLhI}eIcJi=W2|_|8f5Kbml5S z+;ZXa*qa`|<$u)ip2z&A`Mq5*@Luw+-ZTE~n-^q9TA~v+n{Ms>qe;3SC;M=iB6*{j#=Aa7}G+W=Q zGIu1fx!QqA>*>w-SBEH5J^6Oxr^gwW~nl1U?=vpQRms^@$C#g zW(42PoGah1!avuu#J3ChbY~)RTvoE_H}oN2yot7ntAEk}qi_?&2K-(koE?rl6-*KC z#%sDUG5fLYUN(Z>ZaTNuwr%$P)1BP^h`g+{dBxiQHJdZQI!;r)-dJ3JzMAS}pl7F( zzsVe`9m%~@XhU<WJ z$X^&czl&oq%mOkCZ*fqaN><`MUd|w}oFNO`WADEkwhKH$pRC)Q(XU?tk3`8o>sT0{9ecaS+RY(gdW^QGfYa6FImXF%6g~bB zn8>BeHLVV1uVNilV>pEUC%SIh?2&6fJ>>*Z#G%e{0cP z#NkWDE#B$q)ygmBg^ta=PMb|PC1x9Zas8M4y#Mb$U3r-fcpUDV8cd^}rnIn~L1C z?4M|(Kk1cq*neuLo$+m=Z3B%o0k3FeGw~(S2yy& zC<-jjzZpxJZ`ZZh{TYEhu~U0)bT+Hv#X8$$JF>m=d&b6#?fsF!V)t**M+!dOzeRGS zBDY;Ui+$kq<$}NYysu}!<5{N0KNIs}e*3;&Med{EFK_qpRC2zhSNl8evR3`k$RmyU zWacOTz1}lokw@4Q{`uVQUgV=Ie`RwyFOr>X?bJtF)_|M7XO)Hgxc8I(INqAVJwVt` z{up&0T?M&WLt4H>f9UR=(IjJP7@~a`V`N`-U<i`EApEoaiFxFXegt z@V~n+#bbX4Ihf?&fY;ob(b$))_th}(`)PCcvd|p|>U0uko9l7R?FD}@+TIshd2M3<6HKVANeA=g85C{p+wvAEv8`K)Uei? zXG~G{N7S?awto|APRqPjoju+;bX>~|_X5MRcx%&PzmJYB?4@4g=M+SD&h6gK{@{EA zIAsl^vw^Wb${Bu_4Py^8x%bdY=au4tkUhufZD31sze6YOiI*y|OGCul((nyuX!-Ua zdL4PX^qD2_>l#0wB8~n}e-b-P{`_5(-^^SCdldPr{c5WJ$j8_({7&hps@(0+EAv&` z%}4sG0~`2H$C(YH6J)l{J)0HSYmr-Z20Q&|O>P;ml=J+7zOXFj&dhe|MCjAdXEpt4 z{aQGDVV~$lILEJ>8f(2O=v%Uc9K%}Gnf``Pqv~6I%?|obYK#x~bn9I=@cep<$aBN4 zb9Zc~7wJ5L01oz5+4F6fQ__IPjJ$!k2;(&Dx4Z$m1_$;Wa%}TH_&w#L*nx}{`?F8^ zGvqp^hf_YHZ~5YnP(F(C(S6H@T}?TAAtU&gjIXdB`N)-&vwt#D)wlfOD=25rWn@y{ z^1bYN%3zO2b~CTl&?C4mw(~BY&%Tywc&8~gyJnu5m9C-gl)h~|^DnghPRghCEnj>Y zZ71+g>b&fyIqyL97CEhN9pxOZW}kcpJE#U*GJ+hPbRBhf#b;-ry(nknMPBk|{|39- z<=SU19eB<%m!LC;UeRJA!9MXv;ScyqK7w=rXU8l!aM{Hzk^_>_Z?2U*^W%)GkOlgU z?E70ZU74MGa?$^e%4}Wn|Jc~<9RAz7CY(Lr{canZon83tWoNnHk#X6x`0dNU%G^l$ z(pgiXE#fCXUiAGxslUqH<mM?EIXKC+*zn|kf ze=NdjdR+D|Q|X)c9`y0FQ+KAlL7ByC$GGfk{MJ69gEzy&iC#JI$m71;*{?pq*;UAF z$sTyHwu77j$@HJ1N0o2vWY0MP|EF77CxEYyl`G?O-vvHB)7j~fi1q;B(-AktR=%81 zHWr*Qmw>*s?%RYs(zy_G*lUJOzThDF#clX*+u_+bd}(FsL%=E?M&=7wh8T@G6}XZA z#k{-sLFN~_+>5+E!kjoA)Vy(EdT;(Bx)t3L!(Xdr@5N_7ZR{B<9{bXy&kUctfm?eG z$n|R+&e6IAsyE@@>oSTgO3jl9e^!K=&%`Mu&*^m802#eP-9{*&8UEY7%X(=1i6(d)>(6 zK-T&D`)&s|YX_lI9-E@y<8$A!-{W#y`A&j2*|(wr_|Dp2!!9ukCWA|rwO`mvKcz9B ze4Uwb?S*D$k}=`4UoP9|5%>?e>F-Bmf7V9&2=tlVJGx$dvJdYo>^Y?V8IoO`!OJ}8 z{7Tjat##rdHZ9;a1`s_#l3SZ5vaY(+6{S0ee z0nN14nVE;f3tB!h#n*#}x1dL$H_@0!euU^#`%SKxDg605BubybxR3$kvmaHT>!AU3 zQO8E;89I}$k4DI=MUG2HNe<=FS$Za2rMdT>r~NCD-L#$JoG7(9JQz4zO#RNA#&oK29&>!(fe{bNE@GCSMQQpym$DZY-^FfcU{I$O~5E5G zjGgo4d|9@xpR(2cl-*8Ql<`Ixuk>QeH+vR@z_;#0?2XO%F`R~e-oK)svP=6Zi}h1B zv!Alse#)x*DXXMRw*F9XI+WjE0*@I!?oOkc9_}qezcA1NL2RzD zk0xDVW*ofPbZ_lv-_|C|WOq5-zkvRUz%$MlY5y{`8_bJ^^Y$D#Dk__Y^vG25euuaC&u#-p65EJ%gK{z*EYisBR(RgZ*IB$wLuVCHA z7{Wo0pb(wDDXQOT|izSdbCXMFyD>_ z-xbi{9Lh^&!0p88iq}YLSzi$xQN=+-Q{ZkixQjy<(s>ql%=Hz_b*yx*iKW&R=9;s; znd{DlruzW0Y!5W6Jsh12{rOISe>dMhJqucb4n71d>RbG&{NXTrT%#Ay?fz>t;7{6c za+mi9Bz9ZmzI)%)vc=ETawDJB@n9a`XoFOs{>tXQR&3ER~}-^wW>d z=X9`NAlRi7iP?)s(!g%xdeF*+)RWDt`H$jTejWKYihE@pJ^Tm%C;y;l2Ks+%G&_I&0$hK<3wknnPNpft&cjSds={?;Y*G!v30)9N3 za%Vg4eXZAaJej`0F~0HWY>oeWtNi3@SDv1bogBdP_E~BdTNrz1FF2aV*nF8eF88FX zPb}gDemj3A72sbmm)bSRW1V?%;&5Pdv=E0DR5p#dD5JbAknx+4@i(F`gfr#P>z?VU zfvn&2ZPPs(ner;UZ}aN8cRCK8B(X!9kOP~M1I@?*;n6@d;Be_;a%sMb9MHa7e_Um` zS4R(P(Rj{D^=wp3Ub5iT(HFPGfkCu(9c{&tlXu>YtYAKlD}k>To89W30KPv$|M>?l zYS{x^x(i$IMERs?2b;b%Qv%ak)}_+yhmu@R1DoV}W+XHL4HU=C9zlj>Mq=k7*F#}5 zOY^PwB##B_BHDLyUGREEa=q!ne)!-)E7#Yl4JX%c2menXi`6$UTbt@x?xM5%7wSy+ z8fZwk-ctvjp!r_eFk~%vRm_wOD_t*qmNLnPPW&d}a~8BIzscIolqt4ydztU6sa!hc z8~wEN`I2^~x^|#V=a;qb_I1D7X=5%w$vf5edrM+YH79pCgx{Ea0M;uGGe&&_MmcTmQ>E{H}VC+azS=xzHc zc*R7jvO|ORf2DRt`fV6uI<9^bu@JTIo<;ay4BsKRkF!5RFJXV8-_DL4tKVwaJG*cI zc@$y4taQ;|CDQ{QUI<=>pJ{n`UE$7ccsBx{D9$2XA-*;6h==YS*WbO{r;!tc&7<=& z4_bK?!k-+M8^V6c-Hc1~FWcrS-WQF@uK5FbzMatO{{fa_IWr1eTufQ7{`Ga`EXv~e z$#vi-L0nC1D4p!p9t92*dsn=zhG*vzzqppzg?#r-+V@W0g5gZI@yv}elb`Q^PfjK# z7MlWp)?lZ=M}kSXUduV)v1ydE2cfup7;-62Ogl|%dnYzW>@@1I=2%>3Ejb+teC;^# z?A7(AHBEf`x`ot**5kwp(w{V~2A?;x%bVQ^e{NWEPFHm}v@ePL6rQz?a3y2O&*dyR z{-3*ec~=a)!~cEyc?4KuLn$xsTYj-%A3-^JD8+E?C7&oZn(|S7%hw_oV-=K->05pZ z`2evh%E$FB*WRYsB+4iDEniIjeY~3TPR4f9v2yVl*15IbAh}q-f9To8vg-|U6YqKz z9Q897&+@F42Kt+u_xDrp3-DvF?DOq{S0tZ;y-m)K-PKQ-_BNH))4r(EGM&>}TK2Vm z%9MLpT2K2AOUq=-MZ)O9z@9K4;_lHNdxz}hksfbtGx9t6LFMaC$lXiKdhy|&Dkm4j zUy=~EBPN%;ccDMi>}lr5*Mbe_M`?f2m^S8HfX_Th7s74yZjN?+IT5;dRJ|A#>)8}I0mi4O{;9qfm#p@I4lk9p{3Rsjo5}!_tW4?bj z*k;zO@~l_g$g}r@pVEHM6&}Cc3wIj0Cv^2KTS?hr-SfW1Uq|oIy7`r+`zZCaE-w9D z*uRKBul!wu4d?h}JO1E6{L%)WCWliOzKRF?QQG>kr#+q(-@lkdPgz>RR}V8b*M|3x zo|&pA`$790v>K9A@5>a~ozIi8y9=4R-1j?9_VtPN2Jhap_+2ZUj5)O_pau zMb8#v57p>dl6~ID;kYrgbS6Xsxe%wGbl<($J#X!%A7rH3bY~I0N#FHBS$&X4rTff| z!5eLBpS&ASW)L4EgOAY%CyY_!R++}F@sDKe)#SA(p36D4|K)qB_nraHfLA_6@x9cX zBN*iU{HR6vSDs9CD`&k2^Zrh7aWZ#AOyz!vDVzgd(@~y{az})@GuD0mIMcFdl-E*U z9>TOFPCsstcrSOjBv%Ad5{$xSAL_6DoR{L!QA(!Ai@!`9%9UKjNf9yR6W17kXn#~lIM^zrssK;G@;$gKjN#y}e zW{S$ksyv`U?@zsc&}fwx(cEgDWu%MuUex`#osGTjlzDS@nWB&lHvKRCzGR9%rw)`p>C6n9~g9uD_=!FP_5}PGSy`JFM_P!1>snEoxe?sHmu5ZTsV|_Co-HjsnWjhOA zoi8W&e#^JywS2ArK-Xipy{J2fOwzo#Mc--jBDn=srpdgxiSJVV@z6i~@putm4@d_* zKe=zc3f|S2ALyrU8+Fxxak~}tsrqaBssC7i^<#eh+xw~iNPqQ{e*Go=)c;C<^&@`$ zh5giDSE&DkmQL)D6nk8xqokjf@mq2AjvXfJ-wDj!@geM_YX5E$<)fg}yLV5*ABI0V zwwUZlzVrBHN$N#d_e&ej1Vz6dUHOz<=X+c>m&I4_Jv){xP~- z_q`{EwNDI_9~1wDH)$E`;;H4SF5)WtJZuk3i|mC`eyZCK6-9T*H;G~w%D&4JKUoGm zX+9}FT9b`Y7D2y188S|d1z4+n@@_z8wcBQHRF6yu0ec81`upO`Gb8ES29b$$w z>a2~FDQ|rFnVHZu->N?9_W=v(4nWFL`FdS*iD!*-rsm!*IoT$2HcME3Y9vul|{u#`;jGF=5Q} zzn$#2^>>Yd=W1&SZ3(Vz!MmI9B6q1R^1R+nvZpPv|E`z7X(RH$!lAm7Uq1bAYpHj< z!u~w*N&|V^$}L~Sob18g=PsjN=tN>z>0NWk;Y!jUG$+1jj540RYfScTe1&`k>lpA3 z_2y0CdDMg(e}rCvFKRn@MjOPi2iB``Y<<03$@$0m^Ej)F-_enkW9FO?TdF?iL}E)j zIO|M2+({mSctB^j!C&^inxiY9_t)U*OLHCt-b=v!KF0b2@HzR0|FmujZA(5RB9pM; zB{O~hWf%3uYfbbgSe%ZRy)PcEp)8MYb=4%BQ$NNS)2GhSl|N`v+aYqmt^5gax}Upl z1-D@43_WmxA6-H225o8m=n?Rg2TxJ#Zsj8=FY8MBD3(EO$ROqJSsG`IP8NNEvHef` zy|15s1$$ESStS1k>if0=UcXLuNS9EE?yw{O8Og|&9(DYFX3XYi>d%Rv(moM-KbtmQ?D$8|EbU=MM*VFKaM^n_f_}XKe@*?|s|F?PX=*q_nd~pl!y#-%ro-%Hp=JcJXq@R~e z4roR8+sGpN>b7^c5Q|C&Jkc~+^MwCl_f9|K^TTO<=V>qVwC6PXvAkfGP9=`KhIvw6 ztN8K@jC~Ds`*3Koy$@RP9L-m9d1`qz^EHf^N9g>zm!S1Ao8gT_EH*oKO54OLW5z_v z%-PU`?9o|nKaKLogcs<+$4dZ@o4<~J?(~XxGk?J7=I<47`*c6^S3HhaX-D(d&b)}G zP1~gGb>K1d>+mW<_mHLzWUiZf%<3p^@-IE=sS9}Re&KqDLX5G-NEheza4L3 zeiWE9Zk$b%va&}Uyv48{g`W$s{?qe1#^>%Sf;CPa=0JX~&bG-jKaWErV}V1myPBNo zcSB3k|C?FUHc!pe9h=Lm*Rtn@HQXzaHI9D50sVx-;l_q^Y<3d*X$zUNMQ5^y{r1K# z`3CEX8_A)Tt=mq_@>#~M-*Na&@=(7M{Puw1T*g{VbE1uU=z)C#wW_zB_s^itw&CVP z!TvhW4uuRlHdMQZ-@=olw>s!8t1{-zm0duYmB-W*oUZ&F%C%;py2a&ZQm%6m#Z%fB zCO-O4`j`Ekf*-W5;N-OI$Z!}L4F5|$+{*Ll4=(E7{GPepmCT9!2l*u_7c)ZkEgWDi zvJKrKxr0oxHm|dH^}cjiGi`n!*!rs@_{FQHlDx7&wtL33%EpoZC%7%lflUABZ>doC~DBbPjcNBz8?nKlQvj zP(Q1Ax40iCBa`e^4|G>yO{V=&5BDXRo%W2`Q|ESH#GJ|wP+9a8>~#JYx0ONv6zcQ* zT%HdGXJ=49O53MVS8)i>((jLay%o>^YafN!bQJgp!V7C!g?ss$2eb8QBMGg#d8rvB zuT1APIGPfT;=9>6w|H|N@*!F7%iZQ9z44t7sJyuSqds0-I~wm>6Ri)`EuUF+yDYu_ zh9MUtY@B!>1aE?%0Tnm3H>IE&Kr+YcOZPFsOweEO3SKg2P`1OC_lPp6|ua|DG$XyC-%Jb0Nwey+D zJ2obQP3rF>7@K?CJ7HEUbZu=YaxsL12!2Hywr&JD=Hdm#=SSXCg)9bkw~pIA#b~ee zlHu4?-5*mvX?h;6?ceXtdXEegq#yve}(k@k{D% zV}BOBWb-*xHx4a0I*AkC;923!iWKs`8=v4g;L^M~z4UqXhRPK;&^qEl@S-*=8ME+W z;f)ZhINUR%W<(Hw+T%^SttkGK#-`fMScZ_-p_o;adC*+9A&+W^EzJpsmc)rIu?E(< zidc!_J@Qck7z_8OK0}#s^>=XN?%8(oo_*T6WH9gOy`3I?=$s6Xx&H2N=XSRNYa$$( zfp5mGOd=IQ zQ(-OMkbjUyu8Ed3Cp*>GRPFVx%-tEZ_bbY7XZ$HXE+470mVBi1=XO^>ccNpp9|1=C z5H1QdQK}m<&_omPjWfo^ll41KTN?8ZL>KfW+Vpkf!nHNfgt3=#fw4oqtY@2q&Id;c3|JKrwN{$kh>||yFXdupT#@kIfL!z<9=Lj+c;t);KJcS>*kJ^ z#7EDO>ntAn6*Q{xuIA%0hAP%&L&Hk=hPs;9>hY#^Jh*PAZd(Z1BRK~AI-7WNN&7i? z?ckL@pZCE#e8h+5dK!|@L-*nJ%a4UcVWyr@2tYV=W)Ji{Vh2TDex?s z6&*pZ3sUHFgN{*rCB#SZ6@w3|=Lfh)r}Qq<$SC>4V+-d+IoTkeT`>RTuxub?8u5yH zWbW-X+_6I(@z9vn4~fs{+wPUW0bB{T6QO&->C=2g?mvOC%gTA^sEFprQW60&jE&^f(dR<>&kRPZ;Y?@ZP-x`jTGw4SHkyKhYcG$mw~xL~kJb z3NoTpZ(z$bEJp7tm3 zzG&ic@Fm-2Rj6`b8XDaKjcTt!J+`Rm>^kW5LOuECP1q*E;m!RDTF?O4w9_@BO}hxyal#w@7^=k{B_pZ70`q9q+$&f{6Ab7 z*p`>|+Llf4vhlfsJo0VJ=X}2v{AgaCZm41W8nY56o@gsU19Z)S*YgmCk-0XnvW?8Ny8mF-u!_keF1QcC>#|IQSlR zgW`zFkJo?oUoLq`|4sDYT-1L%d_GYBd3^8r^e4 zc@@xlg{k;@1iof{wskkUT|6Xt7f0VFI1fO&@m}a6d1L)K?EkKfp%c}AhBEc>ad3*P ztrg7r{{#9F+|s4u2{+$gV~q0Ob@!Vq6J0e?ZsYz%bv1^ETzzoq=muKZwSjWsP3=2< zBirge=KaVT-}YH0nBZHVFU;1uFzYVhP`c&_x~6k@Ut8-GapI^Fygoeph?#t#?i^cZ{Nq|-O`iee8E! z#-~fS(NFi(k>2zg{E8TIOFjeiw)A57R`HTpkn8B~gHf5@R}9D)cma7@$9r|q)T%%? z1n*oP3+|hYK~s7!LVZh9#NDnP=j-qPRXfCYE-7w@@n8?NCV*vO085Z>db#uK5)KyC zB|a8MR@^(5J!imn5A$&{zICTDkDW|AYw%66S1zd|=Bj5;!*3eLI}6XsFRAJ2*_Isk z^u*QJRzvO|GAT9^`)2udU7K{43%rcJGkzragKI`m)clZQOIk57h4ah#|aU zhFCvCeK|W%Hr`v%0Q!PCtJoKVUuwX;?@ySV<^cRJeFHyz3a~CBe({8 zPkNc0hv@R#y7X=m9a#wv`|x-Tx>5cu zeb_NUy9<)M3%ztJhRIk9cRWe{mD)+o%!lf^d91>I5!|vn_n?&yTM3*Y!0Il66`~5npJ&WP^4Q zU#vi9g4d;=1&89fW#CQg9A)cF_I;-lYXOFa;ak3Ju#G+DMZ79|Sa=TI9e+G(LL*io z4}_mo`PfO){rkfewdmk^%t3bBU8~klfQ~m}r)(&%Y^*?!ZFuujF9|oSC0HB3!i;Sk z3VzxxPRP>$j+$_&adw0DG=&enR&Tvv&k*TsH&XKy`%^S2uOn*h!11>&Hj^c&+no zgjdJ8Lx6nG2cfd8t?ynfpqs9p{4BU6tV^CKEFFyk8R8gSet&xO}Q-l&!1oKHbc z7CgaRq<4Lac}<2GJM&;@chzvyni@qcZMa*z+-`j%#fT-ZzJzTfTUxU1rr=%u)_b21 zo~fPw+L+3i%9zKDWGlK}vRz}c`9*^L%{`!fm$HP}f z^C_8!4V@fktlV{XYq|Hrz@fDx-Oa3blgK0UR83=&oVgEbEluwQ>!gCm#=T9~*^Iwd z|NnyQ6<+(B|7RGF=2b9g-m6dGJSggy(o|_3#S|QTzP@EIcF{al6wQ;JqknkQrSp9K zvHcP4b#`-fdeIyWDVd|`!LwA!`|^AGo1e9eC&b)n9LlFIog4S8g=g3;-Po+%%|-Lu z`@YXx&B5F@A1Pn^HT1UszW=Ov{BB;43+7d0b@S@RthIfgzI)fz&^cROKA+B)X@Dkn z2W!}$LoVjo|0($!VV$YTC>G%7>^U9W0)8aVMC%ta9%3!63GAW_G!jK8WsKLj^F-IL zpFbr1dmH^KSJutl{&2WfXXi@~d$gmwP~ov%>RYi#;xx6CTl;41D65-W!|-5g6#fi% zBQ%G+5lL{LAP2S%xjF|s?i1+nE^=X{d!@^s0M_KF_;lSpk)quLtj9Dgub!`*xq5Pm z6yHie6=~cS3ZqL+W7CgkH=bM}{?(bjRk@wSeINVrj7G&1gx_zSz_T?MwWL;?7Ufb1 zzSJN&C@IB?=u`C+H-evSe5O?AsSSIN0DiLcm2huLatGad#L<<0&J^`*+*kN|gZQ8a zKl$UX&^fS6qfMJ8N`^E8v+8)Kw$0AKqaJy?(d2|Py;IHXhVuCIDsZvNOjuH7sv0}M zXASu201x>ePia&>aVzf&=fbyShVZZ-oLqxmmMoDhco8}iKH|)u^!8VvBRB38=hZu1 z{zb}vOkLNW_A9U++d4=0W4zt7;OA-wd};l1G;`t^PL>S z`cjb$QCr3Q+6rTwyfh5!N_`~rFWlKL)O+h4;VIKZyqn38GNa|`WsW{gqBB_ zGhf+PLf#Viw%67v%BdGVG{>@W)V^>++qF^3B&UfjSU*89&oJf=CkKv!+s2|pIfqyI z10iDVwoUN5hTpg1XEldnv(aTUgg?mv^*w?;2(Sj-{9f4S&bLrCp=s>q_OR!$d~WNzr5R& zH^z`9npfu^Yzq8?4I_M5=znP^Ye9l#p8`B`6{ zZz|9e6}gj(Xn6~?+zc&C=J|5t2X;L~w2D8n?jZixW^|x0+wip+o9tS*x2GMQA>P!O zo8Tw$_3^;0```1`eq1@Ce2{YKX>z^`dvKnzHfhwD5q4g8;Kw4bmG_&0chk_zBH$Oz z$d{EIY(fwEa?r3RB%EsqXy-cWFQR@kb<@z1>P7<^Yla?@(9~7b&C_<0F|~(GA-7n1 z&qH2I?N<2sMV?w1L$7x%!Y&kK;+5>#> zrL)y(vlLl&d1U;BEAC}|RXL|`pnIz;Lrcp}xOzngJURk9#I7yzdp+&FJlwokMSE&X z@des>vA7-Lz$NWmxk53nZsf7riR(ACn+c7*RAU}HRJwe;)mgIWF@3QsBbv3(0RG_uzq0%TU=-&}fopqS2;4wxmy=<4WdNeBih3 z?>CXXUC;ZS*r&|zt|{27#dhXe)<*uBy?PWlisN*}?MF-6$9J^*B4i6J{V%lrn&t>O zul*}E&|hEt_+!Y=H+Kjp*3M>M19r%+kErc`f}5Wg!94^xoqlTq#%lD(2ca9*P7C%$ z1??&x{xJCR>r^Z`9@=SwZ?7+}Xk1OXXs>)^dE*|=hiFDmeFodOY4P=4C*$XpQ}1Fv zPd{}s`3gfChdw=xwcw$R8RS`#{KE+2>yJj|o$l}xqT!hPST;rVjEF?nG76p`$(9)Q+|`X%!e@<{qAFbkKZ3WhMdp2ywCf*@8_KN*=Fq$#Syh$_W-L` zw_?ffn1ks1cH)HM%>j&aIx)1{(JkNnN_^^a?5Py|%w4=1@l_jn9JPn6I3xSSHJZl& z+IP|R(WlN}9Y;6sMW*IBTN+$HS9cQ|d~fB`uAb0bY*}GFy>H%n{<5_Zp11TZTNAnK z%0%fgofk?BG~M?g4>XQq=r{3i7x$!?13n#RmWCMgcVQ3XKK7#;X{Y*)}vcFrzOAeCdr2 z8ooR=rfI zdFhNZXD!_RO4Z|AFczde_&95>xs4M)`D1lJgSNav<$m9BWQ*FXzRi6yY&d8CXea%n zdOF+e&9@N8(0&W+;?khv8H(>uyft=Mx>~aKCitL%yKdK**oqsGXAL34nUJzAU7WS5 zO~$uRLI$gz)~=%pTws%4GjVz|^o-0p`Yv-&f7o#~j&#!`##}Hsw&L7HvB!SIn2I%P ztb=^N7a6O(m9=KT7LA$g9TQ(s_a7!sYrfha?`Zk?%hp=|9)o11<|esu8D+KH1@~m? z;A{?l`y(*0M%+6z{3f@>kUK5htqnOw`-e;ZiM^-{xC~*diZ33*Mv0oLb(=y?^An1z zBXdqc4haw9LDKV_a6tDbU0OEqT7hS$AiFJ%x0IlXDCZlNHJG&e=lnaqY&qv4MrFUu z{FA_I=(Cd8=3?>}9%dZu?%uu4``r5Q)3BbkV+=g#!oi#Z=i_fYu4k?7KgvGGTe63d z4LSOjY=|9Gh+DZbo3m|J)B& z7t9}wEzDkr=wLoFdI|7K$JS96KR$ky-u78Etc-JiWqUfX?aIM%G{PNz&hM=^&Ym1H zP4rjI92@Aj7GHNEee*{6f8)$`ruNnge!6-V%5hQ0SL^7yphXFp7|AzDak zKEQbnd}qs6QuYY)&f}{>oK9!dEUk&YL~9%9$BW5%dz>~_Ct529=851#_a_}_%*0OL zqWfseZiueaoi>AiQ1SY|Y+v)Fd0zUCb$cHBa!-HOA!*5%koqN6jRld-OCq3ulmR%@rN=%>Aie%@R8AFeGW_;aFD*Dv^=(GMF>F8%1d zrq)X|lM2v`&bUlsk?Q_?Gvk*aIhGxp|Ik==V(cANKh8@#s0>IKfA_QscwAcJk& zIP-V)0`cUWzh?N=1)g{9`e)oV3(ESv^sx`wn1aU?GyVi;6kQwLpWhVD8azyYb$r9t zbl*OP=eAu!JL0-~BHTL>zek9(8{+H{;_S-hcR4g+?^9cy9s|$v1HQ3OSS8?4)@Q*1bR5Lku(j%zEynJ9Ey3*7f&SClJS^ z4)yZ>tw#oiBkrC+^D^WgIl8A!nCQh^$gjsbm1kFl&4uEQS=Dz}C5?ew#`YPyb z6=x>gy&`;#;`Qnp+Ot-DwTDc1QuZfuQ*fJb)w&;q6$6`)J#9!JEk zUW=PJaYON$&NKFe#w=maXMteT+Ep>f+7fv;+qqBuMba!N^9^D1to0zZn);TyR z#`m|tw{We!LkHQd`0|)R)$7KXG5D#4H6zaJzJ2a6NAZ{XVXoW6W7pD$+N+;B_+tQl zC?4+Z*Nw_H9zSWF?0{+TY;X-i2k<@MvFLARci#)2ZUlC8Mw{jx@^LVo`9H~g)TixF zW2k@I?}_xQn7KoF5OF9hg`?kAb-{kI(wa9A8*FvvyE)NfbypBI9GNZ!mc_%J_LKGYYlSj40xd_ zT(Rc`{vRh+S|1*_rQy;$wnf?7sJ(R4o3)qz@y+PFb=zw%&A+L!qt}PlMPHA7Ph-Oi z+;w!CHy!7YS311pV)RlywqFHsHULX>@U(UHu0Q3tIFnsBlJQhWazegr)D-GC+TYXs z&=C#fP>Pa6sfrv*4dhS~?@N|o6T0{EE5TW0eC)bI)K9__f22>@+3ocC2W-wwq3F6l zGXM3Vk$dV{FZmh$tyK%Mh&c7081<4j#}8|5TiENge$L0?FMY>8T_F2KdkXr-c-v0m zn_$yg-vn%0gOz1RufgC0JquQXSIyshlWcr;F6}mj#y4xe!ll-5FK1*&v9_8=u(W9^oODz;oh|Dv^Ph?PDTDsMi(Iu9v9Zw@gebn~7_KlJuJ@TBjeH+PI9 zb+_-_@BHFPeivPO$D5MFjb+I;(bLs8l&xzt6_b_=xF6B@Zh_OBOz9F-l%dMDH#T`iqaMH|UKVsN6K2#1iEnHWDY@`P~7nZ#55Wy#oDgV+y@~ z9`;x}eMS5@f!6G3p7GA$2!<<|vyBhhxPWxLVa)%{bFlw$jATdO=U|5ow(-$lx7LB< z^Z0iz|70skW+=|C^L#hrV?|o8vMXp!~84b z-vIs%i1J#`NwJ zD!0SqgS@_~feBmB%}w_PWtG@L^{iorw%R8X&1fxRWs2(+*241eA!7HkQ59p9tiAPb z>UUK3T7PXF z5#=A$+;wM}b2HfnVAs3!%x|0GxRPaG^7l1T$l4@yX5d|P@^m+E3_P6hX)iIZ;ICSn zO1`CJd>-11ARol%;A`pN(d?^*c~==cy84&(IRV`(+K%vkXNq=tV#G(a;hnPs)wYed zUoyaNvx|6Yl5=CbS!z)&VV(e&@M#r`w{k~*8Vw2Lz^*sDt$k6%BomYiH+&cQvkykYrl z=CqODCR4T~fgc~`9K&ccXz1ZO?dvGN;FxSVaOO`2rdoLVWYgVIYg~C5FLAdHyJ8w= z`D#QX>LXEDH${=@VizrN{q)_+8Z{tYyv{zHrtUL1A% zpG*J6ar^W7DC2Ch>zhRxSGvB1{`LRg@_+Hn+oElkZwvVzJ^kdJ?~r~H-0RZqLICeF6GG+YHw974Xhx`(~| z4E&jbmSx98`8Ugimb9}b_|)CCPeq%};)dmw@J|Z96A$?NeIP_Yu(`Z58t> zt}A*K+;{b{4w9bi zI$P^A zY1i7tecd0!Co9(dEu8IBJ;?_vH@^Z7>>Lc=qZY=*iRrhXa~DQnb72J5PGFiN7REy-p5(iL#xUA(CDNUhwgOW&K{eU{g>iQ>OV6Ne1)3W;KOG`!?Esh z$nFSpT^Ke?vdGwKc;#yJP~%h6$lJ(C{tPj1>^SmCLi=7nQFKRwej)=8pHX4T=jdF_ zsv%~sXB&>qUW)GV>^bEK7@6IHFJ{m3M@#T_6kWfHu{l40U03do_ki$;y;cE?#3Y7_ z#+b89ixKt-ifJsxS$NgAYqSkmig9qNj{|eKXT})ChR`}gm7bEue zo-_~J_b=&N@r7a;tXRXDtf{953+F0FUF7X8N*{J`9-@69F)QdTitWe0w8MUO7#mLe z1Ly+cOu&WjpnaNzA1}IQ6tn<-Hn9))Umqm~%l{qd?!_|j1RviC&&U_tL2Qw@(}Fuc z!@gRrNw*V+%mngRv>)-_zG^?>y?xbA{)*&<&=WotGN!GTy4W4A-Q~}(H`fJnVOsKh zkKa3Jhdt7#eU|6FkRVwB0`gr_70FQa(x$XBk)${RIf2dIYjLLmHB-M+X28G!ca77iuRsp{Zc`PbmL}%Av_%`Hv`v*GG${gXI~@ z6>k;(l&eIzdQ`u)v2Nww60K-oM05I`ewl;r3<+k%nBH6YaPPTz;!;`RUApElvZ5W> zq-Ub-v%95RTpMGdas6rSFE}PXRsKtgoG1rYhtdb@Lh0?$=oiVa@dCQ>i{Z-jXmrj5 z^h-A~C^G2F#J%HFkz-4eI?u#DfozX3viu#!b9K2Zt#ek)*7y0QI}tdm zq<0s|-AkK_>?M4+Inn++9(gz4qtliW{E>pzb8~!pE^$9Bk6zNa3F=h(@+{%M518oj z-pFJw)9RRqcizFv!C!@5?W%TGj1g?WlY9uB;gDq(B1V3Q!n(r@vPyCZv| zhuF83e}k?|z}M)a2gkA|pF;h*1HM_HU4D>3C*$()$J75>fDnQqaN zbV&G1@vEviAAH8J(ak*}Vv5)$Y)EF`CO4F5Xn%Dm{caa_3bZsHx*1s_#_igd{#xft z;8Q-t%xd!fa!)_u`ee$rQ;~hA%bEGe>pG*V^FS5Ze@L*$eaSK0Ll9k!%bq+GJv!T` zk#`C*X0vpvKW=@%miraub{uRx=yP1Qz1F0size&5e&W83(*E-MZItNC;GTLxJl{v3 zCky|;Vb^4%#;42)bH=OMNnaW2S*tT;g2~pon>va+SiB0Kf93Bn*a7Nac4aMeRPZSr z_U(Fo@du;W7>W@`p*fv{W1C^CX-(qXJBS-+PKDNjGSN`^BJW(w%wc1jXNAl6 zC?0ljwUZuNidKjTiKkRQf(<46)7x7gon82fJg z?++fp7gBC)Fa9}Lc~5%pGAZ^cpFHg5KGQj#QT#nJ+l0SleUkbT@UzyQZ;d}HT`qZ( z;QqI4%!01>&f^FDpl;UjO<-RM%`QCX74197ubQ-7uQ`#aEQxkjJfO2kR!J815LQ@E`qzSj^k=ITr!8V zGaIqjr6W$K?o9bRUoYsB#93P3@*2CoR;RqT^7o!T0gfQO-oRRSvDQuCBgMJr+`MF) zc*<1ZqaSCQ50GC;c1P0~_SD(0Ynot|!ZW@Omp?cr`!6qlFmA8)aogU>yT{C}Ree_A zi(*e`ymowrYVi38<;DJ&);a&Ww60}S?Af%AK%CKejlIR>3>hj{^A>FU$#!*du_6keVhr<`N-^F{5tLU{Sn3$ zE#>B!Hm!Fr{rP$<_qX@!u{inMR4+&$UGT+0^i&so@kM`5lKUaei@mRTniuv})b(!} z&!=;zgkH}BgXmW=YQfk8j4!HhXitA@4K=R8hZYUahXysid)K`PzTX3z`Z`DL>&RK? z;pV~GOl3L(ey}fG+^q3&BRKVDc*fRiuS`2Wo}2wTN$rIKhsEE|>04vWr;PjJqho<@ zS&cgf`DFXc9X;-~exEJ$DZ9_$W3YA))|yD~xL>v7VjF8reEpeg=(n2xe~;gKW*)uY>NJAAHfwYf+-?|(zD^nUA5 zyGS(sXFDD`Q0?5YL~pTZdb{m2&@Qfa$!PjH{~P{QZ=X(xZ<%QNY1_`t+3L-qUhPa( z`q#D{GDmi-3x~#wR2_w{hy8jj=#bnc+_6x57p|l0W&L`Ym&oOW-Iqsp4nW3;FaA^c z4_VtdYpb{RHu3KT&b@5npQY6xZU5J7f8C)AL%Rt1fk!4And!`WyX#DYJIfd{VndT!sFd}_GBaQ9CFXypWe+{be@E+pucLy z>6xv)KKjHaKNp)Iw|YQpZuP)c-Y>(p;+){>LHs?qHFt^W?%HVDj)e~olsjqF_wt;T zXSrL2JRF_<4)?`((l@fOEs3ojWz0PN*ZOu^Gq$p|vv+X^>1yO zlRF>cn>EDlX7K;@p)u|`t*fbH%TIC3iRE~-hc}1PvX9?JUgICT zHtoAzp>&7Vmvs-W@8XlhYvgQZZR6_*QPe*x{bvJvOU&ghwOevfFfi#2+s@t^e_y(+jq6%PIZWOMQZR=u7c+ zzNY_r9aE_XY|XG9XG{@?~ZF-|`9W$4>Un-+zQUn?mM7 z%>_QG5$$drWY%b}Dh$6$cQnzj0S~g(5*d^30?*$(a%626W6K9fi~--+;fV{nRO5$1nG>j)^ZE zy^aGuU>!Ry?|&VmYYX%oFIDeO>OrHT=N9%ypus-(SW>*9E_|t8v_!r966zrv-Tq4W zlqu}+9R9`&=&(xURmWerW;Q6olbsyzi?>>s!rs>nYmmoQY3^LHT#?t-GbHTOl zr(DDRg!y}@=hpFL&?Wqxf+yrJ>b{D;$#$rtzTQc(@;LvOf=#^&SUP>&Yn=q2bm#S2 zC-ohWqxnB7UdG)6&KN{0jko|X4!zDNCU!rR0_ax3{I)?>$`zDwV^?S1%|+wn1% z74-F&8TSssQ{>lup7Ny1fk$x0eK_X{PRk#!6y|ji<)4Jk)VHNSwK<>iMfAh@&fDVr zE7QKkVBYm)k7^eam%+j%V_0lWMcXbLlY@~r5xFWJ01S&+@=Qm0rpzct|fIySTLXxaS3cT4M7r=bLlHvuCy zM6q5`r5+02lg>)>`0)Bo?onpFJ z+6R7O^N+^Qm=C~DO9}k6TvNbLDLl4ZO+DWRZ7G4rmSx}(nN&k?to^f~7?;>_ z;{&+7t{+_9@}&YUm-K32%=Sj#8-r=j>)0=O(uuVb;9*3)R(`ohz{ z72zoquoEvOZ+Fm698!#@#KiRT$ZyM&r}TrTDfma$-jDWyr$6wvpmcB<`c|@Rz5Ix% zho>a={!Vo9O7OH3T`PM(>D&9sfW6;Q-9MgEi2|PDCF(f~-Fj`A2YRM2VPDMJ;f)t^Bn8|Pe*w5k`sGv_EFUP@_WQl{QIf{k@xa>JiT6#{T_Ib z-y_Vz8DBV$wGb=mi~ohA59$9bps!+G zGoCU_*Vlo!zI5z;V+wS=ggV8t^hAF>gMKWzB#-iApMJYE|2v$u>JL}WMFqOHc3@{mIeTO;AUTWss!TH4LYmdGYr29Pe*g+?h*fZ`p z$xKtdh~mnA{(R2Pxp8Iev~3-Xt?#-+Cx77>XyiAHXYEqed5k)bzf&j^%nwmku6nGs zoyP-|{gARU>~AN+-C*|Uvew9 z?Zzf^>x|1v-h7_F#V1|FN@d&Yn{0de0zun8_16NQGy_X7pS)Dylba~tgZ9kPgry~0z}NQ!_zL3jc9mOuG=RtdxDeqU6NyfMA= z{&t|=Q$_VY8>siCK)uI{>YX2`H{&4qi_T1dxzxclo`row|srrZgzj<5Njh%|eR377R z_naB~PTI3P7FlKO)+%Cm-o8M=2N(%F; zmfZ2~*Nx0}vgi31@;r68dxq}*OSX}=FTyK!zmPrYe(5#EnReN5qlS31eA zerjBQ$i4eVZhvr+tM+0d^(F40vKKb3`0e)RA8Qd^N z9z5ijm*Ov}57_JMsMXy@C;eyTq@pkRw-(Mk_ou8puK=%q)_OCBEgMdmZU0-^Yv0p; zkFzd=_^zBF>Q8sDzXgo;eF*PU3_Tga57w12H zuKS3a|MYF<7TPp+7kN`~JG?!~U7O$5I8aIqX#^r&1%f zf1`1RI?xodd+Dp31JgI{w~20aUpSgs?Y>{K^sOWAd4dSmN;Q=4uma*1h#<#A~z< zGKqROFRog?hP}P08PLoF9_rjiB1Fz7<}ri6k9!Z>a|)e3y(XoNfi{F`M4xOoy+Gw*5x&x6BSt+{B6vmXgR=WD{x`5J6j z&-YGX5S%Xn`ypW0y*bV2Yv@<4lg-;#kTYIB6V2T)raddHGcr!(|KEHjr~3LZ$Gyn^ zNj{TPnX~elM46jnP*#VEmzA?k^HrXc>3okqcz*YW&e`2-4joymdxMS8Vm<3u1^%Bz zPJ(}TU)Z-{dhh79v)|O$TX||P-fN-Hv*ESA>Uir>B)8A>VUHZbmp!{se0hzDE)m_K`-t^msncr2;2w!Z0S6`?L|?SE&D-%6(es2$Vmga)0gPuNUgKtK47fjzIb2D)-km{zjqx z!z%aJvLjIbfXe-~i*pa$!hfI2{k7@{ly6YEwT1Q<>iX?;3%qv#}^1x^r?)wyAv!5T>nP`}Oq-akL+*$MNIagwV~a34B@&2lv` zF6ilbI2BS35HmLwGHt{t@CnMZvjcTI&{=|8cE)9YRbEZ#*_XSkO32-$< z?KF2=7R}A>eu*{k<^SsuzDfS;ESlzfdi#)c^z(7|TpE43c~9;ATEQs)tTs2%J%4y7 zwyy5kr+_LD|Wz+Z8%}q*QVe3BW*EN&V3Ab!iHqq&qMJA^&r!2ua&=wD_t+lbq z>Cd_KMrL&$lySD!CMKun+Wu<&vdrZ4MYgQLFKeHi*7_t?;bQ{7+cq~jU8}MUPP)UE zMNUe8N@Y)a?X-TiiIcz=_jEZQAzV(!rk==sr10Rf)*Hcl*JG`_d;k>2y3 zr|Z5Iw6h4CL+3rw#}68Eapc z9JPZtmBVJb&dfOIpN&N}+ot|m6VdO1$IPtr;n)Z{@?_ig` zwb|huU95WywygZ_LtRcfi9BFmsYZUQa>johet4^EaI5k?bXMb!ZX`!CV}#+kgLS6c zNyx_-$JxkHoF5KhD<|hA!n0n;)p>J|m++_K@P2NVSFfc+{HBHYO^#TN|+{8I;R+7Hq-tQz%z%9^v@s z(5LucaU*N{DR82q zAU3aUIl`~-sCl|FG#Q(z9DL$i@$5~s-8I^KA14Cu zB_ik}-ov|sa`|uf%KuayZ0&oXqZ{#O*25o*i2-rer>$!>c{2=j3$8^!(Gv6%wLCkz zz_ab#o%C?#=>qj*>E|c#$^qtZ4}O!zz31IQw{RbAdM9O5UHT}zt1h`yc*Bl-#uL4_ zIpe^td~*)6C&xWq^tbD)C%Jba|1-&5#|(JLpX9{{Tsbl_cn0%nS9Ww*84#J09*6zl zy~&e2p@RHDu3n`~dy?c0;7$?m{B&_%pL?ib?rn!y`<<1XJ7hji%%dOi{oT5AN*qae zb4uB~*sr-tuU@K@ElVHv4r@u9mp+;%n>LG!4(>np&M!G7-6_7s?*wNjT+RDNn%_&5 zX@1IWrTepe&2QLI^Q*`<1m>sv>HXR2apHq^ex>4w^^$wU35BnEWLCfZL4!>-rtLe> ziC}+$wITLVb0>ed*mcZJN#7Tk=YhjL53BALzwTzWWvrll8qECE@74OodRRIQQSQbO zS<7JkcMlv{`&-qeJP!S-{#%sUxdv_CH&pJ=O*XIXdk^JWt2_KL#Sf}?nBT>UrMUd< z#bx|>%!46lf3>fVl}D~#JU|>qJRm!dIP<(tc%cDaSO_oY;04K&1otEro7R2vC~dfF z{@c{3ATM~amwNnfS0BcvrY{o?iOq^u{s=D|s`GLq&h_aozk}Yn(5^6M69jJnPaziF)$0ERU&8e>5Zhi+~s5+Tw*A zCgkU~x_@eVLSU{3UB2M`B<4G+cdjG*oa_0$bLH+BbNxYIb1lIi9pVpvuK5ynL5KLm zmnSVH_#*{>xO0V;ZxlVD6OWZlLq_G#jLFKG2ItpYh8rznm=G*|`UN%1J8s=UR{NX!)g*a;>4p z|5tx(#RpYa_a4^)_j@a!aP8O76p#Mn=v(R4k0Y-P@ADdC=DoMF!9g?u%~PIT%lfU~DZh?&!LK{< zO;2xH|Iv#Vdhg`PkCaT1T@^wWUioJ)*BRqa_u{a;uS|J8<(0%v7o8BhE)Q(-5v+eE zn?>?ec>vf8SsQFOKhQ4W-goor1>4K77GBK+_Su-La>l7o8()@>a3=cvEnp0D7vIZ= z;JD`q>jSdP#-LN+R(^zC3*q+-aueJGZj%vYA+i4qd$7VM|I!KWESBL9H%7=Q!_i40ADnaZ>4@e zbkBG#szAy@5|M%%!DqE-f{`0iTI~7}N6D z`HZE!ru(sHH}U%n^3UUw;JbLPt&r6n5z+NXaGLOO`Yd;I8wSIB?-fw8+_I@G_U%JA*S%=bId#+-x_V!eW>WP z*vt;%4%$Q3{m@z1;i-`6M*pp_V?4`!TRTPzdqKrxEDwxf+gt^2*f#M#+AImQNq`H@ z)c_Zfp5}(diNjTnd3ZH2C9v_6z++5k3pBaHyc@sW3^sG&4;MF4H`W6WVjs(wX*xmg zlOBzO&j!Y_@GT}^l6;+CvQ8RX_!XXo@7G6|HI3j|-xaIU?>&9gd)cq2IF|YduGNk} z9~<~y>DnWOytdj4Js^M0&uMZg@>Tpj8hR)LAIjsB^w)SQxuaM4bdljrj+8srX!fi; zn4e;-cHosAYUd~X+cqtdrPtmZoinr23&3By{n<@T! zivBG;uX3i-_T9n%i~BwRZ`i&qJkMf7*)}cs7R7B|3A70TkE1mKmKD%|rPb-Dn3=Y} z3pg)j`-{Ri#r_A8(y((>ap#_Cu-+le;Rn@8QN%EjW4`t4uV1Bk6j!+Z2P&2 zwzi*+K6vioKE@8NhtmhVKMu4>F@6#_(2dVWwnkV7!#bFm32S zly|tekIFzFnLfUc4t)1)t+K3gbf`b&=2L%y$I|%F0F5u{<9j&JpWY8DhUcB*y*Vi_ zb@8_Y{Jc!rol@^~}-_FD4hBeP{El%pq$@j3&%YHftGuyD%y)o!hB4#7up z_*Z+biw4K9+e?09Ya6V<=1qcYOIupw3*cS)@ZQE^AGmm&3H~_2HdmqFY#TR!Ztt3w zyKDMO=BU2^i}^c6-yY_hVE7x~Y@hlYTP7>Iwf!c!uUp*qUfQNyT5;d0^K8CAn+Uq2 zxXoSuc*2wW&F3+zkMh{-CEjgOi&bmbhR)$I6k`s5D_Ch3g?Aof`k7y^Ht4$Qy zFZ=m7yv6efb!|N>tJYCh^`7sepEZGg^j=tRypBBr*-45Q&UWon*C$DLh;}8So4I{a?oOVv{BtH`w~n zUCOnmy>U6?ytb(}bhxa^5KG58(g4!Fo_0W*wo3bs=;7+REi}d^+C;KDT;} zY);8t2N|h+?}GDBtclZ0}|$%5i<#H(CeSa~hu(2++kC#RSB^ddTO*Dk@n z1N72EewhyX5-rT~<7@St0l1F#S{Lo>6_2s|IJ2h1*Lk=1@jb_P;qWfLNq<-xt%iO? zLr1{z6u#ZWxC4<<7WX^D1KfNZs&g}YHaCLPC)l@nlll{=e;WQ}rRzsL>c(m0+s}lj z#n0kF$y((`6fcV(&x5~r&Gz;%#Q)1fu@!GIr^=H!Uk0By92Z-mGrVzlH43k;*|Vk;k85IvKo)yCS+^=PJ9f08wan&tHpy8vMV%aogW^`J~I1<30IDA2VT4v z`Dl8+&wDxe#KSXrgMf9@5c9n3N0lv74xvG@6_t^}OLO=hJ9w8ccBWYnB@d2EAIXoV z_rU*>C!Op$s$Ep{QAget+U7+cbp`ss=I=LND*h7Qe$9G)zR{b5^25yb=aIo4TBbEe z$D$vIEqU>eMsg%sc+U*L>#$DO42`YO{4;tFCf2k)vN*B4@{`1+xsS)Xr|Zen;?3jZ z={8?aC~rSKPT&P&xRZtU-|ru!GZbZWG_@avKl*bKXY*T=G^JzsPXh%9=>$BK^#8|e_7q_ zMBb0%$M=@TaXohv>zD)c=#9tylr|Q}`sV5u-#-wZ!#;i=;GRYCng{i82k;d4eW%~I z@pZR+H_gY+fjgA%Yx8ZtjbHC5e@yTg*1i42!80VU+BpLm^<>R$CUQ#phsgd9TD#x5 zI4Z22-bJ@O-BRMdX9xE^t(?ho?^ORcqOHvyZS6;&i?(vaUrOPr{v+?7^F<$kri#wU?cm4IqWBf-JygY=CTZVpH#v6k= zJ9G{-o1%Q#4NcnxXT+2@8|ERMH)HYX%U9@LA^O_ti?{!FWbGjOmJPZRJy1NS<9ygh z^)atOeESIH@r@o0?xVe{YqG>7$FY|%03Vk5-lX~BN8sB}7rhwpR*7FW3_r$|X*yTO zen0z3)7|-FBRaB;eOn>M8%IH#t6ldpJUN;yKG}va7^zyLl%z4?e`V zv+ymJb9P^NIRiWh_o;!R|I^at%%dKfl)Zb^9@hrG&CyxThc?4QW=^Cpjh$j0T{#drJ$)uHNiJL2asWBwyuT`f^TKD)!@a@%>OX^Rcxe2T$F zdct7SCAU2dU9j&sqVo`Ed%y9K`BCn{bgn1AnbluYki)E3uB$99xRq;EcX8@7DfjNt zyw`wnJ@z4Yk5b${a(J&hnizYU^HDQXHjK^==ILAUSN7Z5ItNv>HnF#s0FRB__gTc= zQI7A%AFuPrvF@o8kzX3KdVo9TEK|l^ryJezW-y-W#QeKXF~&2D7h}9S#@j9%jkRWO z^YqTW&O7d(W4iyWcL^xhy8TK2XUyFFrd#qH+RERyYUI0SY>7|9BP(6LHsR$xnLQP&O@_`*Xms2IA9&mzrnHOykqeVKgs{Q z!pCOE@Y}P@wDqjy-^mVl_{Q#ujWY{IQLcSoXZc4?n8jQU@OEfr(kxwb!uoLsPUjs6 zY+&sX=dh^~q1cLD;A7bk@&ryXOLfPm!5q7#mc6|{6K4l+(;Fk^`O8PZoA-M>o0wpx zFC2v5b}naffIBsvevmD>jix)t*sSS_Wf9H?Fn7&Ubd?!m-1)lmWWJhflvvQ-jj`@m z$lE8FEBN=*5r#eMfX*d&Vrs- zsoPH38_rVmEWdl5rMVaRJ?JdW{EgqAVm~kCcN63O2fwY1rQdF6>2~1Fe1rcV1UL8t z$}??o+A!L2@%c8gSGL2h%3-`;GA{et@MBwlicV3POC#o!=_YiM{?_+95AuI-xvTP` zR>{oQhF{!j1~?1UzLK?gZ3*-ec3fRyBA-e-_+Vqv0f#paKo$}+OTe?gWPRxGUixZr zW#JfC7Eb3Ll=^*b2{5r9qOo#(q_cdyYhAS-qQ8$irlON`1C_o?^YO{A)iaeHhu6dsMk&dC!8iAlGS4ZllM~ProX;#$8~(3BD!^ z*DnkHeM^ZPur0~~`>5xW;urQC2guJb(>kHu7_ub784JmWTpe&SKgotKA`2ft7F-C= zG#+oB*S=IaGv8kR6 z$nz`O$unog_B){&2RX2RN6)6bPwR(R>tVz=kPEJFqnPYZPb2ozOD`t@dJKH$d&1GE93xN9E6MktPXv~a#UM_p^*JPB@*#qPLjw;z})@t3`LUwQUAcun7B zlO#C5n{S@qx}S6V(iPRrKMI|2*3iAXDt#>+C{}LZm1Vp68+<7?@_{f>PcUI4Z2LFq zOmx_XYDYagm+z{bbnS;3k9xLUigt#3DsDd4Czz{r8Fv!ecfUU_C_Cf5l`p!n!3_Hf zto70aYy9AK;YkuD|ZTz|9lSbYfcsxO!3D7|Vxqk@VltK1? z0a|^9c42rd=EPRKLc7Xakz3G8J333cCqbT?S>Yjju;CX-CscmJS*o?wI!S*S^jX=M z3$9C1Ki9r+`749X^Qse7ef~dyzFl45%l7*J>{;-H8MKFe2p7%;&-P5ynZ8bR>dx?( z8cUbohbCQKk#0_!^zG2vXMs15%u{(6Hrj6V>mQM*k8OQznd#oQ$Fy~=G~KPN$L=k$wtah$>y5GQ zy_Fa9omk0Ae50^=_+D_ckAAd=-V`3bBzXop1s#>&-?M;sK3!dv;{O?ZTdj%g1XtHi zHf?7DSCIZgj5U;gf@SX^f0b(@#hMv6XT$V4 zT;=jak)4uT#(SEN_e|^6S;nO2FjuE&t}pH{%+<~*_t^XA6o2gKIo$*7zml&7d{#DA z0*954kMK=&=*nx^qCRfFM@-D(mh*Dc6*H|NPU_N{?84l_^Z8BuW!$rlL$Vhhqj$B2?D;f&p?+m|&EWT)@bDVZQ7bS>zPIFh7Fate zvJBe821?)u$Og*E_K|(F10QcCew-P#Cpv?zZ^IJWam3cr+{ zz@3Ty);iDJBx59VHk^nqN4{{EaEXIHm5j-7?q(LShoHxW(7-X+@HXDo!T*cnvOh&` z%VuvMf?ZA<>7!s=FGZW6jyA~b3~f|jK8=H}s3l%l%b6t3#Nl5 zGyO^IP1(@zN++=wq&ZZ9^AI*xJ#h}{y9df*D|UPp+md`@JHT_~WBg*5#;|3tN5?z_ z-JXXn(IYv6ADZV~3S!?CU&DUi@sTnA9LE>l(PoxDH6ngp4w>1$V%!O)%$aLG3H>kf z=GER|yuT-UZ(!w+Jv)A7ma;#8UGfG`&OR{7ESE3P!#UF^_u`KO&Rv^L+xqaadnEt! zo7wM2_C(B>)6};)J~a;8x$QpAJ|RPD#tgzor(9(jXTp{zM0fZSIqcIM^8JZZ%+e6w zTZozGR>xN)H&-rS)m6Ft-EiH^^=BZ{u;~StY<^->)AawpBQGn>aZ6(OgPp6f+pN6G zO-pwoTdiFE2k0t)dwhlL%-zV-5Pjr+#<-0G(lxJep9?u_W$Xi=!^YW&pA|l?Mz&n% zm8Sb&sMGoAMXj&ge{t(>XjnOVbPm(Tzjr@6jBm%bu9Ut3Eo19dts@Xp(3P1G$W>Ovp?_20)|j$fKTn zS-X>1JLGB4&slS5s^!<@lfakM`p1@-Y1UuJ;49?NqkG{6>(@zd<YSb ze;m2v$^)IiboSUG&Y|4YwU8b?6n+~9>HuWr!ZeOW+z1`#bQurNd;YT)wd@9XR z?_S9td6V{?+hc7%dy+ja>4@;~?7pu<_us^>*l*fiIeA3u->aRrU7v8;^5_r6mmYr^ zURW6G-uHF>VRNX>G1#r>!+*~G#kB3-4_{Ts+xDIDnbucMp4)2VOMfEXmV@qFh9uiM z!vkt|Q}30Nk8AxqV_XQ$?}b+P(f2DSf2MW!UuaJsnE}`@Bh2%Hz2z8qZh7VW-N2s% z{uamKjfdgcTHf>%--wUL(9b^bE?yMv>&~J0?bxzp+ui}kat@k%X4bAprWNx`3ul(p z&g#6q>#?c8gJ9fM?zH^|9^SkDVqnImDv!6l_D#khCuZly_=-F@bY;$J;A0-aww<)C z-_ARLv{BpSM(@6)l~3wZed%oh$;UH-bTize8_8$UjbhYM#nWX|MW&_qBKv~46r87_ ztEK;f_S6$^6m-7Duja6K2{96I-fMUCkzZdpiIQW#7cYH`nF2R`^{f12mIosElGj6v zH~Di>{`xDJujH|O(*1Sv%gVERk-NLFXXWe4rj5d1FG26t50Wn3i~QW>+qA9dB&&;~ z=(eCwi{&f_WTwL{-lhzjGT|53zP~J%n<%Lt`(t?f^oeP)3(^NaRvo-%=t?t}dKZp2Q@!XQk zP3;t~5x(}G4X&UM^{2ji@l@1TD)#=qzS@hYi&||zw}7Xez!+TP)!=ItxWiVNE1cJY zyT-v&)~&)PUcI>Su`cFbH!-&2@(l?R-q_0M^}T9Whe4tQ#wR&S!PY-ukJb$lvydV4HX&meKeHA@n$Dc_* zJLMPB-uje>&k*eoYgOz`vAILU^fg}_XS@3bGyT|VlNOy#6&=k-wmy1XtCP?^!|?2G zWW+1?BiE7X>#%EFx!r}_mV81^?|T%vegE9n4EV|r%M)$m`?&h35<2+wdEgkCAblX( zh}U`gzr7C|q`sE>JD0ibyQ_vJ+j3W!hv7Zmt%`LIpx-Eb*YcEXT&MZ^JaoJab+Z z4!JSrxn{u7Xvi6Q1NP&-6|wEF4={6Qnd6s8&s_XF{0U+(ym`~Mo4LPoKe`CKbdCp) zx05H99M^k^Z)ksD*X^W<>PhP4h@I^2LSKP9;i+?cyiIe- zu_us2kG*pLXIhj0Y^Lu6wtY`mHEV8~%Z1QWuRLWfwT`F2Gkg75sr;nk?^6Qbf6~YI zNZ|WwzFT{ieWNTiyVVrgwD%#iWz*XIqo7TD%}&AM#SHf2>&c!~AN9x?lboLJ^zGHP z^r0B6wQVox1FpAs73Rg6j)EFSv#UbXg~cy=*%vFyDsleg02m1A3-q|YnZ6|bx`-LExc z2S6MDsd&L2bO*N1-&yN@k6zsR%1Y#9GwWK157Q)C*Ij45$2mSN-R!;vx_Lm_+B+v= zr#uB7dga4Oo)5Pl8nLk>Z_mz);fpo~DMpW9fc>&H8aX5FAU87Xe}1(JS>e-6q}sEy z#rK&S(^kWL>=>cryfGBdXh$dPS`W=(=Z)dsuYBsf=l2l5t4UXBP2p_|gZz-6?Xz)x z-una2E_Aefkxy}Vu4^=Y4YE>lFM^DZvpsRP&H-7zV z!`W}}V_o>L=^AiyJFs8O??e2~3E-%9;0NRA*NPcg9L3H|KgKvg94X&{^jVT~8!0>A z%$e!EJ}!2`KeCxLUmH7EYUd5#YQB-fG`Uve*s-=%eUwwrg>`PF(n!~ZQV ziuJ%G;l$TF7AIE!=)7`&?M9bqcN6WjwkhrJi~kcd()TmR;%%zGQ(9|tHoD5vL~bU$;?>>HoUJ~6 znfTBX+;hmFYpktfVSN?ba{nA+rszRyW9E6YB4|4)c5^E7GQmE#+NmFRY-mMr$Zk>} zcI;qZ#Wtq;Ix*0f`Vsxe@5^hy6Wu9WOlLEIr~6~@jf?NV*)sHy@>Y#-2Aj6%B;wOY z(ZE^hU1tz$z}}HQ5v{v?4!;XeoBm$lbJ6>G(14})QP8iAzq$03Im6{0#fAl=-t0J zNb4_J(z;n1wci&J$MNL%+1Oztv-6peO+yO*zSN7on2(1@j}pbYh8fkU20==sz>Whl8F z?HL8_8((`^?__&CCBCc$4vSm)H685T6vtubFfZk;w)`A1hP&_u|F0Bpril3$^Z6M? zcoOZ4H%0&AN%5!4li=I(EkmAqi&vfR&f{muzu1c$h_-X)F<@7_@d0>ifb1r0W!c5p`V00FLy|A?efk&v zw3pFfoH-b1TkZkZ`a9gz^gWGZ@Cikm`_C5D!+`T^QYkU@8!@<{h*)I?JA(!wi?>IyA ze3O6w6!5koCtbN6i0N5czL0T6qZYT?TkBW<^UiF4GE?!S;xSGS%t>eRgnR2}*zu18 z?{@5n>agBcM{|b1cg@Kc;_8E5+c7pXeLJ$)+80g?-ayVpfKl@Q7n~D+qQ=0po*kf^ z_5IB&&%3Mr&5OOJe%oy?(e5npUW^Yr=e^|Y58`|TV;qGW_)|PCz4=UMK_||o&)&EI z_gQ7T1!Jx9HS8YZ@1S?ZcSGmKmM=ty8up9-{jeBz&Q5*7#lJ69=$fwZs2c7zsGPm(itO9258~z3RxT9pe&ADq`=H)^C5>C5`%3gx z$ioz>G2}&TKt6VIRuEs)&4u_Dunc?_`uMtOBW7N6O_`Z-ExANzl$nRyu@`jK5Z#jo zPp&QH<&Gim3hz>NKO~u_yN2!?!Q0paTcfsP7~@IW*3vFk)b4)TsozR7W{*L>j~t4B zImpX5qdQBkYz>*VQS5(}F^5B&-FMD~i>&fMijGGD`_8jX_YJ(mC_gZTKcQTHvK?Ev zr`Ml8OA$A6-vOBIovTF`{kFYA`Nc-MxylPT_3lln&&t5Piz(2Z^6}>>@8XXDa?+w{khQhYaVp%!mZ<+y0I|cQUT-QwhJqv&Hev;8%FI_+<~r z#jhd1j`bVR84H|SVpC;**K&qy7VrOVATE}{Plx8`t>1BEZHD}{j(3)IhI^KEj5$GP zSuTxpy=?c{FgVjeobLT*0Jri%8RJ%;is?2)E$Xi1#C`YK|oO~A@N_=2A^w)jps z9gfaaZX(fPcyN3wcS*YqIuB4A9(Wb` zQ4Orz=VrZQ-FFV&Jnn_>g~z{eL+tkG@@o&PZH9eNou%;Lak6VTXFCx(KxT4Y%DCr{ zpr3As{#j!;UVjVxCfY2g53gOh+RYtBJIUrXoJG@`oWS4m&D`Aazz_kJD%#Z#ZXXxD z)qo$TISxI!a6rq_VWK1b-2x8kHLfx6OnFu?{62SN?FN3OGu)iI@SE$P8>YTTg06PBcH0S#`I@Q7)z(@M#8^ccP=Y&zy}hx1?c@gf&)F*EBLFZU>X6YUuhkENnI8!c3 z^@D72{SWznlCcYsDPMsu!BI^Zxvx9js%P_IIMi3pSFJ@A>o42uD)1orC%>^CKGC{a zUr+1zD`b;+i+S{(txbS~3uqVe+vWMcZDZH{XS7or>hzxBkt|bQ5cv&y^F}g4vRyW{ z_)osn#pn|Cue!mus&Ct>9rD3#r#3O#i1(CF-J5Sg?ueJg6XJ(!1No%MB}<_7tDv2Yq3U(IS66Mq%?+Vrny;d~$qd;OK|bnCr@`*hJU>l4 z^I6^}sfe|fjWn%0kB_wtGDDinMi_M1Aoq-CqZzU#^ZoB{Yh*t$`Te#xli&ZZHyhur z-Jbk@{>{eg&AP?|vF|mK3$Jl-xVe^pf`bbk+yzd~Yn>cfcNOC=G{@`_zbri0d2HeJ z=SCOi}WIpiX za`@1lkJd(adp``HE%NIH<*?|@%Hm4&Lpy6X3OW43zs~MH78?ZKb7MWlbe6;xl)iAn z$!!n3gr7*>{v5bJ0DUb3mnr5IMdpcrJKyV>p5$-k5iCbW*mfDeT|IhOHnmemzSn>p zzw4zVNAZc5u+$I$n~pNU^pF|4Awd{}w2=I_4u3Qp~Nuhw3L@7{lQg!iMwzt&#; z6l?Cj2kXP)!cp$!u3Hs=gZcKp?NR|xSeOia@(^pcCKO%waHyiWDOB-+Z`8)|JzSW< zTjJ*UVdP$0i~mM#9NrmYVFG^PN3aSm;oRoc8@_|LA69+&u${+H+9-c$P`+3g<>&Q> zH?7_O3*N|IM|n7AW=b!nu;Wxe_3nUM z?!}Jlt9~oI829VN-z~f49_rcl-unHz)j_XX+L8`k|KG&vD1**wlK61z_&Ze_BS)XP zP&A58ztHM*Yj21z)K<2C9X`8a1TEMn=r1=Pl&xP+f9mrrzm9<~6ZEbA)i-w1lHBO^ z<81v(@Y0ump|ONJ(f=Lrx$^8caBpoPIWBUHyBj>o=IP-7NzMYVCXSRm&8(1Jc@W-S zIKjAgVMW)CMRdK8Tn>_VwXDlR&euv8-=h9F_uh_dtet4))^g@{A$zO0;2W(bC)AA* zlU`jGZdU)w&$DnE=e@~Wx61!!<4HG`g_;*J-gEGNBeGNVpQk>4*@_M4S2gP`IF<3; zmp{L7^Wi9Vg>ZX4L=GA+#$~eRR^53bE<)@X+i08o*SDz?Ccakt*7>eZ(tXv=OUTPH z+)S^p;*E~RQ2IbPoIcnUO6&e)1llst5Vp|LTgbmTmOcEps>A7-b)odzbz$mwK8<`? z*%IjZrLXl)$PfO)n}W5%DX}c-Hq_!n0^=0r zV4%)q_jhD=qWe24Tg9)N+m}59L;NE`{47~xHbs2Cvhg#;iM%^TzjtkI!#j=L@M33p zNbB}-#2PQ7J^0Q<%=Cm$2l_jQ&nS9$cXcTJ&c={S7dh6j%pHG1HpH*!rkGZq<^G9p z&-Cs!F4g>**J|*nm=eB_8><`-O=%3_;b3(*y>hjae*4cy){ewB^70~&%zn*X(~;SG zkd<1~R1_YjFX`-ZttoV4aVB~e&i5TZCjE36MuX4Jle7J`X|13v0gg+B$}NN zpxGa|G&`Y}X3uqLc7ji{}734FBA&=}B5s-j1^HHO%nfd&q^aBKvcG zBdlHSa?>5eb2qs7C)P&mk>3nI5?{?hvsY3UgMS_6fnraI9OT_O;>U)+xfhYn)tT;( zfjhgV3GNkJJnm$!!e4$A{6b7mFpj3oXjv@m=WLLdzbi? zakMs}z}nOvMxJn{%*vCO(9K=QedS9@AyafFq5*lik#cVz&WB;X2g7_|@YW7Gmi+S9 zE{ZJgBp$6cvlRbAN4RtJ+nq-{JEvY)k$dM^Sefq!!TJjeYaQoP1F$X^tngwlPA~Id zoze^Ico)_~n>{)ctdiwwa}=z8yK%xZ^AIm5xi9RkIq%&7tLPv@{7q}0K<0h~o)f-YU5uP3vg{p`K=+H0@1_F8MN?P;BUoB7iJn&;`vvE)X> z83S-Pp5fpQc!%W49?p5`o8^4#=%dO@(q{wr*9GHk{IL7(AGU6#ZU21L`Dk99kGeX& z)cLD?QnY!gg{R2-ihUM)FX?;6@MoUSQODwl^?d-2d_M7v?W@@PJl`tUoN$z^38(+` zCwH&!jg*JDe>Vya(Ki@By|VB8_a(P zzjtuv+o{KT?44cz$cO)L`p>(E2%E9BNin9u_YlREqhw_~cZ4ZA;fu$`ul{m0URQu_ z)5m;tb0+E={QoF*dimDoP($zA8W~mlzrBknsXY1P-5ynQn?WB8$-bIYyF%2 zy{10V4OXBptwCST;z{mpl0w&bR|hDh#jF+|zf6zC$DU+Ta!LezZ~-yA?%O$QXAPIoPvl zeAVPi5VXnQA>S<2k)#|q$JP+F*FeTyp z7QT_{2|jcuTMxVq*Ir0>z;Z9At;apD!`9Q?uH4&c>y2=SZcKN!qIcPPBi-%FJ*~;Z zT)p-Y?rcqnUMi>;&>&|m7Ysv#@x1$N$$dHOSnwP5-)Eb}UOn8H6O1|Eb|gL~z7g;- z$=6cWyXZbjj;nmSZu~pB85CDkH8;1PdiiwRllN}7-uZOfKwiS2?KtJYW(C@bv@BkE2{MIpQ1n^=m2LgIw1fsXT$~Ra;Xj@1GCucl`(b<>S4P zT$*+~-v45sB-RT)!aAi;x1V14r*JrfDpE8@yc<3#wG|I1@MpN z8{H=$8q9~&P%fLWXgI(89dwKNDrb%6!@raA{`J^9aBuA!DzA4i#kLNVZ&P`&-tx+O zR35CaR|e+eCCZOczwoYa>j_`d-A3GD_1w%w&#WSN`wCg^rw3}kmdG{ zSDRal|EKobHbS0c{B^YZ3OXFI9yX3=kB#3M>+R*9QJaS=WOB+S(F1=;hN0~$b9WKn z$Zsjzx$;S^#Gd@ML*kvt=y^J?s#rAcyj}bs)@Be+>mJX{bm?L2r`T8MEOv9@jTJhd zC;xi_8e!cnmS4F5ADGIdYp=iu7b1U}a%eqn_YdsV`DWn@*f?k>PMtaE454e6-YEM@ zyny&x>?B(w6}55fBj;WeyQ-SA>JNu27Ttl(qlJ1mQ}0G{6fX@|E{by>!P1|S!vmdS zEHZlAC^UlHY=PFszUBK1Ba9;QKT4we+&k;%?dGsjL2i`cd*l zwcxKj65CUPvjKg#ki%(G;+j{nzx9y^f6jP6ub<@5HAiXjgFwv(R~T zug@32-6{Bx)xPj^7kyrfE~4CRip>!{RAI+)Hn37}?~SH&!yg@;ysop~d}sOa2>dk$ zpR7~F!QH*tYi*FPYakw-8-hTTi))Qv15AN`e`)ozp>}c zANLA>+_!7o(7fOaL+jeNbm<%)-@L>5te$U<=~zddt&8HFRrGNM?W=F)=LqIjabTjk z*9s4w_LvX%YlWYjCfK#VI6V(`?V-QTdF*LsZ0pU8o%2n#nycE2j=E~rD0B5`w5xdt z_}vpPzPAcnvCF;W_al0`Jr6(k^ZvK!l6$ce+x54Vd`gT-`;{X4*V@BJZ}Flre2o83 zX8-H>$H4uC_P!v#lMhWXSG(9J=}y8XVuqNzT;^D8AMk)ZmE6M{QEXbN&IH}Sd5q1m zR5-n3VnN3f@ICE!yI2$5-{X85b-K`-V!%-YKBn+g&Y{cU*IC|aAABWqlbPaZuVnOcF>J>U#IUgz4~$`})E?q7YkO0h0Uoack8d;XEx^=OA5M284`RfIs7=vDm~nl# z)~tm7a~pUU>|bMTaOXSTXQf_1-zzFubEvcLK6jvjC- zyS~M%@C$Cc!K(px^=58v3VrrsuaWK8bcNF)>=f3vyqvwfcr9x;h>4ONM*C&aOpJPJ zgV^MB4>qg28QW(6U0~-H{(H{le&^@F@wpDip`(V^VM0eb{;_vmC@meT=%U2MGw@4( zctS<%f=JPJ@w(i`P+ITDgAe%8kn!^^=-2E=()y+gzS)9Y$O4CWfMFer4_pLqRs5vZ zn&h{Gxy)-Nw`4!8_*;WH9K#$2ZMIk%7{S7XYEm*kzD&O>Cd)bLx*f-|W@Zln_ z3D{Tz^F9o(VttPP3bflhTXLkrlYK79x}C~j6Y&r7DXf0vl-3wNg+DW9*>Q}iXw}-T z!EUJaEt}!9dPm#3+oD7`qdhBM{>AEYjgQ?|?($PB^Ud@mJN4 zzw#mRck?4327eW;-|oj>`TOv0{pd)u%~HZ9~0nDGJQBa zw{S*N^H;=pGT3?B&l6sV!G#8G-hb$6Jm*q-pb^oQ-mg6myCG}+6`l8k|Mcn!jcBeC z#Pkf;7jn(^^#b*`=x&$=ijQ?VMr!;Jx!jHxuYbz%dioFe#C~MsZrLmI%Jx!LF8a?a z+dTAPgf1(@e119M){@so;i@*49a^b*ZxB7syt}trMz}x>}`VjTPVMM zh;}wn{sWbRr~LMxp!`AGK6lg!}u*vZ!Kh(zO^>kj5Vc-2SOOZG(4hV|9(b@nO9JjHM(v5O}-OOr9!SFv@~SQw4Bce(PG zD+avS+k>q6+FIhM(Lc^&-R-bAAWsprbq2q6hP z-kw4aU44KC$@4iJ4JNqbS9C5~?H=L2U-Z@)ndv@Fk5I2$_Xne|*!DQ1F;c&S5jq!x$p|J}955av!D^$^+UwmhzXG`_ie3?GJg` zBTeqx8^|3)yg~NJcuu;tzLPCh?fyeg`rXR&R_T3~Uhe5YKCNd@xn4StaxzMni9r)f zZkY>hpzGi>5sma1ol~!~^eo+IC;V8nWOW_<RKipgs*E-O4d{p|(!EM)s(kp|uYyGxA zG|+bT=;7P$uBC0@6u!aJruweq(iYxjp>(zIxz?@`y?d)ZIVxSDvJIgC$8XiAMx~2Y z)`P#(weePc+o<#;%4~bdBhtxmIGsS}A5U37j=Dyr%LBaEgwqQH_|Nd+KPwOZ5%kY) z>7N1K&FFN&VB9PVr#~6AjhwM_df`CZiNlA-O$_^Rf7|iV>Hp~-G^ah`^oc>+jegr- z8fd$H1Y3FwwsgPkO|j%*=`DlXu8*XvgSIdA+rDg|?UoU2z2t45A>OO@Whb$8o*JD# zmp+!*yjkwfvmMxA{`iC&lWZUQ9fDS)DfF@w|3mZHk#sNK_1geOs3&-WHPkSY?Hn8b zZ>N)|1lSF5kc+~`QO1CSwPEk#HknBJ9CFC0PD?m7`2~D2d+I-$R{b5U{XLE3RHhCL zgZ(ISLHR^zNBf{w_?5*)S3x?84lLh$YMn_xx*57ePN&$XN}i>vd4dm(e!*I^ra>6o~7Z^?Q!H+!?Fc?H8-DOJ{r(hBy-N=|L9WmV)iG7eyjcQieDBp z-gac5zWFe`_IT=wuUr^0XGqsV=dL~RNV9Su<87nfwUKykO~h;uVJ9kJZxD)d9)P&> zaqLe*>;dmVPkfiUYU7L4RiD>!>gyR@_+tZL}}|<>S;d~?>Y%*PX5kw(_r0Uw2XD zxBH)zt&ko>eaW$)?Mo<^?PPHILdt(WMEx&N{;eU(FQ9zy5ashI|H}~NpP>A=DrY>! zjN^|!49cDNxb_9&2mHlYC$jUfrkq}m4OF(>!}!+8;|Dro#K_IM$lcK>SsY@&S(J>V zm2ca(NvvNvx#ecbUahYr_MS)3>xy~5jCOm_yU^!|C)Yl&xHW0ala1kcCx#2Vp6i*z zVsaKVhE1pT_x-fad@cj_o^y3Kgmk$guL`=}1FfcLE63j~=ofbXsVq z1-&WB@9FqqMW0E^W=`imP|Emyj((@G-PnBsI!PC8MMZO?2hrS=JbRGzX4z|ahB%Wt z9X_EmjlnrSo#ps8F^0MG-5I`@#b1DXFYxwg?5wLW>!qA^b?d|UJAlRF#D`DosStRJ zfm1N=4P)@F9QCo=VNnBbFbCrsGAx1WoJQQYMb{XTzwPs(c(8_~?ecJ^7jr247k z|2M$ZGtk}s=KF2Dsn)CZT(ZsD^~wJO=bDXIx&~d8Im{sI4fA~-v9i*iGsw}Fr1H8q7kwC_tO*CPB2K5)F~O7=p%--f-b{+ahysh(_Gx^GB(J8NH+%%<%3 z#3So>JNoL`;6d=mp;eW=!7Md_E?piFER7( zI?Ht4^~dS0OKP}l0T?=<5j{2LL%@M>9~JKD_Zz^XbHfkWv6p+x_^oMj%9yl51@VfCet=LEgSQ6%57XA{k8M%dF*as?Aw~7-qvv)NBiID&J5vE zysqb~Cihi*F;4cEd#~H^6nW=aIw)TA*m;+}sE%IjaowMT=dB^a7hL}6W+UY!p)~xIax>IL5^Wfs= zTpohw{~$PJ_b>Ole7qlH;Vkqj?6b%#^WiShleH$?$H0N&cFpPV68KE$EONarXH4j@ znO>gQ?p4>!aqqKdbEcTzyCRWvEWoiLX7_B9JNG->ug*R~_}^x6C|qLyXg0YISf5sr zw?}c|O|*r)D*i;`;+YeQTc>lbXsKd@-~~}=an^8l`?+SuJ@5|I?Sf9*;b&RehG$rr zCfR<$}Vol`icK%@Wp2TYxuS)M%{<+ zb?v!)_`Ws_d~d^32E+Hxx^UVYexbF#Ys`Xvd-HVP-kgHJDSlVB zt}JsP`JHdC|KTGuu{RgDzQI^!&o|tgHrQVO8t)vgO1)a)ioO<#mgv9HKFhqMo&he} z25?a^2p7;ek)vM*>i4TEEZ4v z-gag@_k;cX_SZ%`c8KBDUps!YU0-(cq>b~cX+$>39?RP9)|y{*;F=%H9hbVVN%$&eO=W3YYv9^T6P6ZZ z@U@0WX#C&A54gkWOVD4qKxa}$+cPPjRt!1(Du^Nf1oNQ1L<4;_E?s(~XmQ~qr$VCz ztsm3;ebw<7=@V*O`^4W#zJS9F?-usA@6Nn@&bB{reqaV|NEY>=iv+Tz@uE|&x?FSa z&qH=QF8&%F2lV&E}i9 z)(pHx?Iu|>VeoWd|AmVtSe?GyYi54+4qk~*dv30V@2AgCT$}-i=OVv8jU3b2+;(KS ze4a}fn|{ZEUovq2ui?iw-?7UXr1yLAZ>Ua)y@=tg>3e^4`6sGZnEQqM&mpHJYi*ym z5#K$IbAA`NJy9F&Cf{3dz0v;eJ(r{5)4V(P0+V~5So1db?wmINU7K)Dn@8%b#o6R% z*YgzemFS7i5VOw;Z=#;ar-Sv>SkcRrL(CvAkj+nIss`<0txo=o^K&&HXrHJDpNjh% zEH{d!A40zYeV$Ccc`9Sw24&MJJ6C128I*m9vW)Bwem@E5MemMMxleZu|LB|f7P;as z8lvBiQzjiJ%-j_-_Zqk6Z7O&dU5F+UlM)v3us&qC%Co|Tpy#y>6g9Xsn-_e5`iR;$yMnA4Q|2xs6XfcHR5sz(y?nCa}$`ste5Kyg@O8V- z^Y>f5{u)og1Dr3e)Vqed4z@D8SGvNlV|gXt3HLD{_es|EVBB|e=TAOe{s8={@AZD) zarSNc{S@@Jj`6f1{|q#<=`wc?CxhH;gGSEM7$i?v)7`*SW9QQ3-kG4eaARM@*oA{V zzrtroY?S^#j_;P}t_5V*2W=ar-mmD3^P{C%)}s20ojf;JMcJ!r)8f9&dzD;7JNPX* z|98e52D-b;=fhn-A5LOBT-g&&FX1m( zH(kJV5A(9(ki@iXHTkmPEm3&wveEU^79J9s);KymEjea@?*=w0;jF4-gd`z~dJ;eR2#NQ;Fz&p_S&ydmG$SmyY2-9BIpsFXVMne$ z&Ro5h^Wa6F3U^epS1A5eq@yTmaz&d)brfwH&EJ>~lL@CU-#oVCi#HwA@x}MXb)5b{ z->R>jcW_4+{a!wAVu!h5F8K})>9~B}q>jrkIke;I#>pL5-i&<%e?GaVa!YGWo$M$} z4>6tD<)Jk3xVfdLh3a;pC*P<&IyzZ^zYXlqhNE}-v$o`=(6snlmroyU*zpG2$7E;O z-~2l#k7?iHmN@r`;8wBQS@=#6HzB;qr?u6`r}#qzJ8dEOXc6wTw*YtIBi7aeu9NTr zdtMy88hAoAJVLR}+j?b3cl^GWy+?D6S^42BIZIBN?s->S>V;$CxzEf-?;1~zHh8{x zXVAvQv>|!?N#&om^2EhF{V--$#_-uFcVUeBQNPcieuDZ@-Yaim3qGeta1+mK*TQCP z5hYee^#I#^U`tYuzHa>`o5iD`}K@73s@_1wQ8`*KJ&eQRG1w8H>{nK3q2|6=C1Z{yf!_BR`AqYX2kn!7)1t*~Yi#7A4Xz+Bqc z&eBj$@eT=aCVGg0-&xG*=~KzgJ%u~~*do_ri-^<5I z9}bUW(>we7D)IJ5Im^dbYdFU%y!0}s@*^X6t)8hE+7BHjdupL~gbzH}+T zc{sh@{$K7jAulwhE_)`!r$uv6T0DI%u%0qT{KvuT0qyQ7&7|NCF$x05+2-IUkPSAupXDwjl|_s@Q~ zO1-c8?GR9B=lO^E?Ig#g&&g}&LfTP`eXOv+w!@v9{`xKPKJT}4o&WuEo{~Azxh{ma zv`I#lCT#xHC_0S6&%VDo-Z5BC!K2dh1z-0+j_ww#J)cHGZ}!bxO8I}mI|CkMkV&Ff z+2?zZOTEDOXYB<8n$3(!NAt!ojrRxejug28`^Qo49qo_fTz?$r@YGtGM4z#Ycv+$K zqg=^1r}VFtGH)NUyUKp+(_nIJI-J+2{k?nU;huGUxo8fNW$3lFmx|3~;=^t}_>tM>IF=|X`%FFr1PNcl^p*NcB&i4NQdzhpt(3 zhfS;%c(`Ee0md7EIjL{p=QH53IuG%FSzF|N$)795Y+(D^d}p;;slJNn>tFS8C4H#w z1@vLi0pyEPAA!92IP(82+BcL}8yA-^`Iq`${ocXyPkvDOKxd)5r`VrJca~n%=F8SL z>pOJ*%^>$lpc8GRPIsP+PaxwDhNqwppVMe%e5f@#C)%pJMFSaMjf~G=du&9;*AMa` z3x`iIwg$$g|7S41$NaG>KWYNp%wjwlbQ7zag7+NwpNRg@11#^u8!|GmWom9j$I=-z zpKI`iC)vlY#D5;lK?=G13FbliQ46~CEcBqbwON&VxALv#Rdtb1cCXq*UEx({$|I#t zw~)Rgf41^u$SyBC=Pb?|Mctk`w-lR47ji7L4u30q2>D^xL7RGNP7U*jOt8A(->n`; zo*kdYG={xCod}M&4@ZJB!H}jM!6=`R{L5M!lF3VuZ91E%KCZXVf{qjV*TFp<8bh!z z&>C3VufxK((3hT3N5Vhnxn|ufV^Hd-pmwUWy9^pzkVuxHh^ob z{S(2%mPV64(Ui2FNclp_N8v}+81#O*zNb#(*uz&Z18)x_+f%eHzl?N&F7Cv%bb0;& zU3N_#(AW1jyEvoWv)KotS4qys+Qy}?yU)gM_*`QQB;@<0|nI)QQnE#obR*emM5h|lK4P_% z5B)dHq1t_sxzd>TYz(Diaq*xM?=adcvwBY{v|r-wMc3H~K6|il=>JS_C@okT!zQOZ zH~**<4=wdxw@+fI{XW|0L-^?>|K{WP(j(|SI)A2h2fv#4&hmKYkC~&L%hhj@r*lHF zW8M6V&+hxC^AkLW{v`gbcS-PBhp!;;SJjaVNM}s6MtWGQ3G^ys%3EbSlds@Uj8Fc8 zYy2}5byn|^UQ+69z1Qho(rx6cl`i!yjT7Fb|1;pEf1Hf_*LHkm-gG=dc~F(puI&AmGWr zXAhvbXYJn{f8*RCxZ4Nb_5$lktmWUmfo?#|Gx|d99=iu9_F`S*(jQ??gIrvvv3?W- zsN7qkuSfKa{lC)7SzTOuX{Gm1bi5$O=*Qq{_d52?j92%D&XVqYrqdxqBjjWVDNYo+ zVO@8&j1VW9B}ak!%u1JMu2o;(4yT*8ldHhmG}Z11zL|DnWo`TbIEiJ(rS~z801pM= z;T?+yr-v`IcyO_2pFd!{6<#E7yd#aH0a*ISJA!YfLB1K;WCi~SI(viF*@-C-{L%60 zC-dNc4xW$?|7Cgbe{c}|&%%>}`RX3w%&yJ>1@I?Es29_jDj)vt5zYj;J%wfDO~0M@9z7Mj?F|KhyhdHhFQU z!dZf~DZj=)ABQ%5TJ9##Lf{AahsvSd6Y^*`K@4f|-B!I1aFrKZ`Zvly1HVuo(kFxZ zFXr_V!k-wFZ_aC{CC|_J6y@SO>O<`Wa6Lx(VwD4Pet8$=4SD?r^&g=8oFUq|oAO@| zQU6ZL*HRwvB-wk#9~5iSbEe7tjrrBxF6!Ur*<)SX^GU$@u-c>ialb#EH#|(T%Iej> z>W^o`rnpIZg0qo?`_B-`zIZaW9jjMd>GXTLAKUS!lP?h=e6|HR`z z%N-BNns{9b8)zH#tZo2p?INb9otUr;>&&Far^h2FtWQ{egGMLUdl#8pCpxiWCdE^Z z;v9=ThjS%%XxfTGPw|h)j#N5CoZQpa1}Ir{A$qG~6z}GqEzy5dbz4Os37jx3HNH%x$SoB26`bQZbxV3t~?sBsd{a};eS&NRlYy80- zYQK!ww(yj2x~B=b1?~2H+2q!MFX!l_K$YK_k`!$pbOzeGVxtxpKuc3rx!W2HBw$H{Ja9aZ7$5i4f=5p zu+0JA33P+#1Z?@>FEg=n+R_PT@dxo2BraOG*ANpE=utJ;5y=~NRrOP$#qo(|zGA8p z0V#O zB05@nhuBfQSN^vq=`z&Unhk*~<$h1y^*v%kwy zU-5d|Sl7z`s62i9&>f@TR_jWz!6#Z<>0jS)m$v<88}dVSOv=4q&if0v3sp91^x9gx zx7Y}b=-bUbk@S~;htAn6pOwS?U&#?9UEob{e*v;h`}Q(oN{iUnI@&4s&XCQHHJTvi zO)*R2eNpyB;U>;YT3cj=x0qNY+3GUr-f^98Wc|KDJh^Pe|FF*rZwr0I=_A6v1{;Ol z7vc_=IA$6;1doOMGjW);U$Tu?^_}KSv(wdVW{6f~t;gpFcufor(dHat? zW!o3uDf52dIldxXJV|*2d6@!SG@jsaaW1%!&vwg8eO4|E_Sc%1xckIk<93fQQmj3G zx~>}h|C%RYke{=gexGJeBu~Q3hu~Lxg1;8{bzXIU^IaWVvEk`mVBdR||G$Agp}c?6 zF|^j=*eWu}K7)R!_HL0}M?Ss!&9NPE)?Wr&%ex6+;9KcPb?^%5F}tS_FN&{n7wcL3 zy;7f#D7IJpL2IR$b-QLRvc1;K{}y*utT`K=Q^WIIvv@W2+p%9ICURd=r2Gul%i=8a zQD;V7l0g1$h2Bo4j^Gtcufm5OBJO_8`DSsHJtTKu&tLOp+T;I&%4zefF3(PUO1w_( z{mhu!hv5@l$H6Di*<9>!iFXFJ*IwGrMyhHRpC`Im2>-m6d*XWOJ-AnI;x0bQgDdSuVc-#s;OgvOejE?Ss zuO%VSWnD*sPwxB*@Dbh7HzTnQeQ&nO#nB-%#B9IY1Z_YA=N5oR*1Sm~LuZ&1p`rB` zP)BqVN8gprw&W6DE>EXjc;jUMck%>&qZ?h1Z115B=oouel@}vstsa`44ex+Xa=q}M zrG6bd$Lk~mtLz<8vPm3*j;Q#hI50jbJptYo0=63Vtp6(ZM0M3~3}5$rc$xLJLNEAX zSM7u6sLdp8CNyrr23*3!XDL(cT{Y`8I~#iw>r8Sn#CXkk;S^a#z472jGA`5>uM5?h z6D@zF9kr{xqt_AVKf2I#{0!ULiv=e4BV#BtouAh@gjd>m>|%$%HuPxOi$tU1)druk zWcO*J$pz@uudrU4%;Fa0<}&8Gg*k?gUOk(6mQOw&H?>vQE`4?#IxBX%nR8f|%jma$ zB6){t&d(|nlAA*LeXhYvU6tAkCLmkd*E?!s`zor}gN_bkXerJ>G9vwFB zukmWuxN_{5vBqg*{^1cb?|ysc#Mx)ZvmV4pwB|%#VbSMjp-=RBm1%4$mpp3b|F?PS z?jq?{TF25awyqx2A-cVMqCHb-_ZU(9?ORtPpJ}6-_`B3!$z#U47z<8yRwaF#{zyvVnSe!dg3>m;N6)_HtKupf>6s)z5K zzEk1NnG?T%1#!Hmq1$f+r*ke^c-2eb)$$?m`X7_s7{rTyODwqHuYwo7%lI|{L%omJ z1$lT);djWwd!pzx$hyTD?7D)@;vTsdGlu&oLbcMN+O&T_SLjkMip(MDrO-{V_zh8I!@F&0!;JTdgbiUTNDh(W*51b*oh~g_2olJpF0vfrNahwXw8ozj_ zU{~BwFEBO&;{sqb@F($YjYD$Z03&eL)&XPmO>)HrFh&napEnGQd!=6jr{EI4E(6v} z=tsO${PuEmq+qVe6Fa>k(bdqYaHKp!4V$&ImB>de)WZM{7&%80ILz>q2m< z^xc9w+~{73pk3jp1suibKXp;^e*LcDcRjzS@_SA}fgNuN{RiVX zfpLtZE$Iw`<80n(P5qhuqvW0VXVf2KkMPI4XL+Y`!Lfw0T%@#Cxi|;U!>=ZpmAVg2 zJn*4c`c`eAj(B04RfX42i-@pdnj4mi0umCx! z*fixJy@t5YYHOdmzvETj<>`jx(BQl*ItF<=-8r zz2aLHfF{N_4_p!*EXYCR2 zBF;iTvw?e!7^luZ1^4K^&i~qn*4umPO06tAoPIPWo%IjqR5%#LyzXxv$YUscqT8=G z#OyRMe};C@Z~D)%M}zr0c3}Q8M! zKT(K$oqOP~hCS>qU}G};>~6Mnsx2IW!fgDU!y(A7sn%rxtJYF z_a03=IdhjeS~@NM5@>+=8#0zBq`f@&R=t)HauBu9M&?Mry^-qiPVgt$W3jevpBX>4 zHsi8cHz#)zDDzPBuP`LFIa1#=8L4?z|jDlU44+raOXzwkA;i zSK9Dxh=;cM{c~QsRPE^8zP{7mE}(_w$in^219oQFn7g0@w-&>-1zmG<*n2yZ*syF) zTXb&awB@`dXQ+`AtU*%)uE_PN`DXtzIewHmDOsPW+d12vh1H%gPL7?0H=)O(TVJ!5 zSj~;nu?s!j%iIE;z0)+dLvdC6uwU-!4X00KJv{;c-wNODjTG1Rz^B>U!(R)$RAknc zY?HpP@NR^@b+%Y-?WDeott#?<`AhDNW*l1M#q34Jvv;A(pG!=PctN-F9%Uw`>(=WV z7h`wU9{mshIUjp0b>+*@8kmC4+=C8tIW}i~`xTv!q`m*Nzl*$!?C)~#OFT<#?zckk z(Y?gv;?r$oZ?CnKI@-)^tYB`V*puMBG30-Y_03;8#JLW|bQt7iH8$utYoLmL7nrK; zN#ZrXhVA+g-bdR^dRZyo63aKR9~|vfaM!B#4&PyIG|-29nzOJS8TPy1b z`c8efW49W^-Xp|#`;>QuJ^L%q@|(5%$`aH0Dso}I);4V^rt6g@=qtcECW7xaY;uo| zGo5A3;j74tF6bp28ST}8?*`f@ma(%WV!$gpkaDmv_D?c)ja%ba{|V(+fhKOkMQrik^y)r!yVB+mQcao8NVQ_9ZaiEa)Jx~a8m@=XnM7|4y2 z=|ehxoc)OQpJx2nlTJidBugCs7%WRdv#(ew%N++Wa}+pTdf+UGe6p4v{*!M-4{`Q7mL5Lp=plZn zK`s}1zb`N=HUAB7x%>&|F`gFjT6mUtpaCBlXe?@k69-!->!{SU#JlJocJ!I2~CTue%cbG!f4q@IvJyEvCQB zS;YP|k}H>W7DN77K7pQdI(*`mBjc>2BJXJG-9k(W}S`*2STJ=Gz{4iTJqVV(T!hxnD-m_e`6^9y#A-nWN#?Fng12Xl8l7YPpiBpoK8*& zr=POrZR8X0IIY#gE3^{QwvWeEz)#{(T010`cx%@bM~Z zMldNKZUML`gC3;k%YW7YjWocUuO;74ay)#s51#Q=a5NEpf&Y68=XPYlQD*tVfM ziI*=e#g}7DKR>tQhd(A6zv7SY>}9RcrpiSJqJhtd2B1&3o+`axK?9m&;eQMDq>E^6 zXkDCvY^b*T6_=M&x(%^;#0Eub*P-hMajV*Myd1vvpLXBEeoFfmwL{)+&)K@ewXzMY z<*&YAO~r99!&66_yRWKuXHS}!nY8q1=R>OWJmf02pxS{vU4`Dym}eGs?Lu_&6l)Nj z(cW!M%$~j5nl^0sQKe_`R-FXfBjc}n6E^Y3p5OxK{2SnMU$Pc6#LD`Dhp#|BA6P)wH zH`ULJ1OC0g*_{aqI&ZH=(i={o6xkz{wnHJi5!UrUNy1vhesL6m#Cn4nhO1srGIlEOK2 zoMo*=p%LjhqLbUu3*8>R#4CrUt0X^IgYe}}=5g3PPmCDShq!-AXS>x0c}C{f!KeSU zukW7TNTpAo@?(^FC$W#xo-UBp!h_BT>Abi2LP9=+vF4s&&l2q21xK(Cd7X9p5P98W z_(iRZPEAf<4L=kO_xrThh0+oBfzai=ttXjW2j95(66NwE<`nx%Ft%>sM~516Ki7`m zOFEZukQkv}mU^16{INa3d`JB=hf)8Wv)zL+7L7Z>Txv}B0$b2WevX#)ly4Ue(f>Z4 zn%jWqd=MJi-+XI-{Eqfj${j9R;=JGuUEox_WiK=#-aZO?dgWzDKjJr{(}wRM-`OAQ zTwvo5*o$MU*)bdb9}aQ9RVaPIZ_yd?5$;+on{@H8XFr2Fnbkv|!B4Sg>5Y`J#5k?&X2Aeko#y+0P!x7pCZ}-%Hkq)N|i~`wyMZ+9q2i z~Z9njj`w(NIzPHI1}rpc{$L%e}m7q2@RIgm2uB>BlN%JPnR^7nI= zw>)~gix(CDN$`CPx`1{T|DHG!7yDDnz4WYI-aUpM8v_QdXUU=XL1tz*b0gnn7N4bJ zr<9{gewXRkD(fOOtLuoVofUEU6b?m?UDAjy#P2p>cRb3S@qQWoT6`+R{w*k1-Mg^i z*zY-WpT)MX^_^kuf@e$LK0WrIUuNyk(Dd6X9* z*X4JXO)*$28(Aw_TapiP@jQHqN&3rt8Jz(*#3z|&yPkseCwpi<@4Ox#?=EzpXOTfm zmK7s&v`;AXR`LB5=v{cd9hsmxOUBS^Sy!2uxqBbFl4xIXDeo)~ryoQY*|AY&y8p4l zTeG%rm1KhQU#ZR2{C4wQ;Z?!&)LsnQ=-~{o?DoP@o83#3f9gz=%YN46ETqCeS( ztJr(1EFbpMtD!Bv9ej`N>ExF&=)8eGk6xj;2kWb?v-&(|`jDM}W?YN<#hfOerd(5P~O>%4TJuMOJSHW+tu|*64P;Fv^Ae1yg>uOsy(^~tY1W5u zACyf}UE2CRZRO)Q#M&1v>7C@O^pn^{=Fmr=$334#h6i%l6sCJve|y4V&J4g8Y0rMQ z8Q*q6%HBJpJ1OM1`k>#}kv>hg+Zapo?)l?b1^tHUXG0Y>5^U>=GiT}3o+$tyLpVs%ePax=)3qylN+Npzm(OR z2Cbiq$&WEu&i@Tuw(EQje|Eozf3O}tB%T`k$K2c<4-r?!9EYI&D7PT4dOK3VqZD9??S#CT=u6S>P4t`z#a_y(pJ`z z-G>2Z&r;L5mDq)4><@Z@OZ)krrSZ;k)~fK?z}js=UUHtOGX=cnU2qQlAn$Yg_woO^ z{9j1^5B?7`PVJ}FP6AmNL+8AfJy`<#+HuUWUOo?K9;vWm*Ow~E_*zKWxhGOL+7tD=xN)= zoJQPanKv5Wz4j^C*E~>EC zE<#cG8a|-VRA*z`&0ITqEW1~!_agGvFlMa_+5b7CH(z#M@x%PPCd6OzVF*QbP=s>D+8dDV}VQU%kR|U<7*>6zW3uReEnMR)rEepxaMbR%Mhm#|EKfS zYJK+jeZ6`x@n`5#(%`4hkl|MqSYd>J@6jnjJ41EG?yJA{xyql z_TkZHvEss#@H%@|1>1QFdxmJct6a2w;8~UZ&F&7yeB9oR3<=uFVnb>8kh8z%X3M^1 z^TIe?>lkzB#f-i8Xk;*TlB|Q1(Wj>JY=Tde9dK5&%-ce{8e^j=Yi$5e7AI}R>02mw zx-xPS*z5Uw`W^HT=pl|hw!y_TAG4Ldrx@(<$?8R1&xo_>6_<38w*{5i4ATA5N z(eCSty^}?!*j!H36PU$!vLl=`i*e_J>_ghuUD=5Lo&CSoCi>m_cJ$TywvzN`=quR! z)!t(Z+M5tG!HpaO37S zTAJ2+4CvbNMf{M=Q3ASVZGg`bZ!_O4g>Lr{?-tTaU{&4w;CIfKSnN#|?9i*m*#nPk{E)GBko9)##onFvS>l}l z?25mWyk5fG>~C(j`*p3?`jl07EJcsJ|Ya5RL^7?^=8 zVOnk$9Cx8x?qq*0zlZp>Y-3Y7S0o=sioey2qoJPv`TG(7Cr|Y0g80(phyDML;(y;q zmn8R%>~qo^w9nL86npP%)H-rG@!rD6`XJ+S z`TQ!pSFtfk2hbcfoCTin^E5~1a!z|p$JZ>5%Dg4^S>b)zrhe3 zr{8BBUuKd%*TP)vH6T~DzM8;=-3KE#ndk8#=(ZOb zp!ed>GxGTJ%6Ib5(ieG`vPSG$)%Z+N>oph6xQf_{_wA7vN~eLAZu<#27l^Bre^$Jd zGd%rkb_#U@IJ4;Z*Q}{+(fc=JW3h@IR@)BHsUwGkL22 zC{Nkubq4C>9E-A(z_w%{&w3}vFG`H-p2D-_<{V7 zTC)-MQ1!qxHRoiN+3IvJ!Fi*Fx!haLH#Ya!9%###!=_SlxX9|51OBC#tsObw=d15` zIBw;8N3Z4HZps|rg6D#B%eT-YmQg3*4;g#|!jF{!ZDr|G7~AD*#Sh(Cz_aa{adI!f zuY}VzKi8UYK9SRi5mMQuw5K+{!ZYyW2V?y^@jjBvZRolhXD|o$en9q352KS7v1f{6 z4-WWLa%>)-BIif4-mTdr=iP$u?RjCw6!82Tp@IJtu7N!t*N^-Cckdi%Kl4K0jB4~R z`H1uD{?@OHESY~Vd*T!_S!)#eeFOJR-Bq6{O_$P##@enpspw(pH!VImD+$kZcn4ST z&K<;g1iWI#F!U0&cq{e}^YK@ur`8f{GWuNK%+L9K*|{<0>F4O{Dqs%gW*hupK2@DH z(mb5pKQ|Sg?CdHVD43Hw1K~7z(?0M*e*R-YdG;-+ZT) z+mgk?E&GK{va>7ZFFSEuhtA?yzvlOyU&>$?s{T%B@dwd86HfOeW~V&8%;%5JW>dj_ zsvzCF+~kh?8*{VV@xa6gcl#x{+m9UE^VGlHcYa@d*6HTvqv8pLmM2Kp+26d`@`HBj zIvYaRtHCbt8P2#_T%$V^V z4{-iN{TO7#6nsLxUp4q;l>5K5R(&hKM{+cIdC*-yjjwKu z<-d|kmEIQU@}YW@4*W?$doEV6%1iwjJ&ASh+O6__kG{gb-0t1=&6GCZry&2vHJ9&A zGUv0{&!>PVi~gy6HofdM0~!kL9+0Ewp!)?lYso7=lk%Z(G=aQ3vhCMH)51dl>ri9A zjIoRM?_%y7xQ7)!m5X)p4c~P#2H75ZzM2<<2@P}tXBd15PdgcN96FI5D2^TNf%^h^ zQQ{p0&e~{KI0-8*bvZP$#N_V4S1=T8+VgLNo~%y3)a1_eVe5wr9a?bR=;Kyx_b!2! z_V&)yKetJ#Jb(+e=B;a=NlVu0S)0R$h`#Zq*$+-lacbswBXlM%$DkL-hbG+OH3Mw zYfqy`h%N#>X(U!>ilC7Yx6eBBFK?Yl(;YA%DxTndS$zd3+_S3mHzfhE4DM> z{RDDF`f$%|$3tFebuq&7o61(S9J*fK#=VW$&3X&Rc1*YxImuXisW0A9auDaWT2JrD zEQOY+yB404@O>Kz{2Cgc>OxC)jG4c@Klgg#FWB?=Uh-1=^S&2djU^o|hE^w&Z+uL!G86h1Zxvs?T{=$lz+8-`O?#FI*n+-8l&z<|AZ}|O-^fN=ud^f2 zuxR-v@fz05EVb7Q4??D8*6~E9-O0Balk(LFUd7xA7xw}0!}_1Tx_C+^OHNBJUc`I~ zkNcbN?YNaO=h*`{t$<&t?yja#`Z_;0hCC78I_7!6CaAMP#opFqP3{@=X{~+ng6y&A z3)n}srjpnJDH}4DBE>wXXgdpy8QP6v`!nP{3iOKr&;7PP+Fj0gb})`8xH0?t>|J-* zn>N*$aI5ma%w~<*7#(zBgWh26$=@H-@h9d-dR~_E={G2{BKhk8%@^`gqd zI0ZVV?jPyz#SP;5@+FsfmqFJJfxeZhNFSlOXRceAqb}v?iC3n3kmriCtM=b1&PIB% z>_76e?ON{QMaH9}?|ph~M_}{mnZmuqv~$qCYO~z;yP=o<1zP6!@thBlFYQ?SEcHHU zpT*wMJf*V*Fv}j)3+&ki>KERF99(Gao&H&~8^?5{BnKBH>SDRR8Et#H4}))nyB_v5 zjxGl7@009R-tUF}z3KPz8+)&xQ$6gh@bgC#Kjl5joNAApf~WkPd3_aqY&5*rjB)p< zOBN^|Xc>IDoc9gHB+fGB+f(?5enz{p^$1VB$ROp_5#FZK-dg@%f#%-}p+hb(ouap; z#DPbVJzJR<)mJ=mY#n+R-yC<0S*bdzE8DpEK>G-GY1X~FixZu#E9~2%$xAz!9-ZFn z?iWC)jz@AkEd$=hrCPIai=kOs`2BtHP*R<#@fw!2;rfvF_H#Xo5zfExM;r= zTtq_K+pvq{quaF8$A!k#WaK+2CeGLB{scQV`BXB<9pc6M$E~{m({Hyd@=hLWRw}kG zu*r6@h7!tA@skwe7Cef%lKf86wsJ*=Szp(UPFyS>-OhEPbOAO6 z=~H%}RWPbmYvb~J91W^}xA!Ra3N76hdCD83IxRev6YS&ASP)kvex&#fNPA-KjjmxGi+|da%^z#9uR2_J zD!C?*(`k57?sMk!6IGU~Cja_T*azTKzena(N5La{hjOxalcP#|YUjT$_kPV9eV`2c zQFVQtA)cZJI&`)OV)ALL2K%7qd-h2|U)&pF?{jo9mx@uq?xH<9 z@X6wxtZA#Gcyy+4Kbjtu?g7I zjxaODf8{@(0*qD4R{(!kohIyTT0@d+E!^$ci$6@?^nk(oZbCUe~kal9CH42ag_L>>KWKCS-bh;`Xb|EEw5MXfM`==bZfcP z=EUL-9Pv!+69hjlw+4N-5vRI>ab5$yHO>NJQ5wK`{y4LTxp97SU!NT(c}x!&=i$LP z8Q+Q|x*AW#wA!)4i(X;8_xR)Oy)xdp^(2R5jdweKB#rm4#NTT_|Gds{F{kpMRUgCo zaQq|r<2;&iI$Cja_<3~P3UU{TR+89|2Ghza8qKZBPkmOFnvYyyS{UUThS z{cpB&Bki0-JHkf;>q2YY+7^fb3uJ$0l6dWKK5dr+MX-O4J_Wbd`sUec0a|Kf5w20E7bi*S23xD9wf zjX$S4*YH>AVZbt-{;mTyoy%9Bx6+ULZKJPV>_E`vGs3%>bi``-aO;2hea`axJjw6# z8Na=)tk=V6b1Q38?b){R8|aL2%+fpj3c8+_2iLoP{KHh*vN|Ae9ZVa`fy?57^)4Jp zFGvEHU^<-t1yjt7YK8Z=9u8aq4`R*QvAqi~_`lOeQC=I9?Ofrzz&~B!kM&(*vwQGS zE523q;po@#)`{Xf^!poR)B*P)TNx37uY}3fgPscCyvd&5ig+*4t~=+UJR&X@jNAbB zOkfgSLF*aY)79~-swOWu*_|UpH(acHw_@<_s86FoF8bi^<15fN^D zyYmR>qU$BgLgdnIqMxaT*eZCB&aNcsv-(uCxQp{$l4ERm)+l$clmfF%eAoQB7AD5XyUWZ(qK>uA~=)5!Zo$euN z0(QmoJG{6v={fZG4}c$RoGyPO{+c%Gvo7tv_BQyelTqknz#y33{x!NI^cynlL4Zf? z2QU#MV_|CXV?Yh<4Th!MFuu8Tga0!$f>NtU$bBz!wqZX;{vNVxD17+mzty+y#|q%c_}^bbdqJ7v64j>QTj=)_yc1v4ylF0r zEqxBq1N!p%e0^E;iO#&<={jLAb(LLv^At-RLQl45oP0Ww4&FXGPgiE|aUV2L1}!yQ zBVD;{J9$T>E3cQ&*y+mU@7I;lf2Q~O`?7vLQSmkCXTFYnC3%Oej%?@h-_(zpmoD%f zRjkHP`Z4>a{`sNZ67VMdxNNHQWAaiH^CtZ`9O%c4Z|R`%u_tl*alO-zISXa?3hkF1 zpdYW-9Wxq#54^QsKW5zJ?;H2PzCd+n_v^>RCqdKs`te@i&7$A7Nk5KOrSC+qwfZsb zrWm()=CJxP@a$v_52YX9&YIC4Enh!A6J1<5d5}1J>42R)r60GjH&Z@{t(-@ZJ`?QC z<%bCJaR7(S+ol-pbDzPs#@T=7k~^)@nK)Ax<1AT2*mO2n88~nzB*+hO6SgPOIat}T^=(u({XSm>H{b%JG{j+l2tn=BxBK(B#VHx5nPr1wa zYKeQxB{?f6SrhWlQm=@+yd0@b(bk2?nyW5K+Poacpp#2J3#QKii|lq$KQG6R#<{#4 z8ebQ=IW$JuVl>Vydsyv_zry}p&k*Ozw*N|aegD`4JIia}x&hfIJ4?{#sr0EaYJF?n z?{B`N|NMj2@GfM}Bdput3`B}`nOSAvm;U)g?52X<`f;=$VXvY*3%#uG9()h+HP+_5 zUi($$O^{w*#ePfkDOz}U$GDDUh+4=nOtDg3~+-tmV`+cmo&z4~Qn{(Qy(Z|_-z zE#!Q1zAZAjY^%vtaGqNFP#Mp6d!_$Ku9l;dR(ap$yFT_1ieon5F|IpjrvN9mIq7)H zH#D6;qkTMf!VIt|mK1xSyN@0HJIOtLnK#MFfWBNt3~TkeiHo1AVa)Iu^|fKUj|=kS z8qPSMj%`MGiD_@bcfxHKK3dt{-Xw2;aP%5+@v>oWx4+B0zuPA?PPsiZwQ7KeC8rL( z$9HEBp0)lh@pIuxb~NEy{3{nZv?GaMv>yIoeTeKIozK14+ws#tKP>k)uSdW6rs8M7 z?U+d&UBIANt!nbMh7?x}4uqr3p1zp|-L7(JbhYZxw;oK!PrJ#P=$IPs0vkT0$WHEav<|WF!aGqIp&O)# zn@_#oH$yZe-mHDzR=%0S|5`WBukU=txx#}xe!*N5dm=mM@m<^cd^>@u-649W|6cUk z579@UcgpVebLmQv*>%{?YkSdiL+qWjpK2Iu4%IiT`^j7R?j>8#>8twgNBp+3nQxP? z^v&4Hw=KY%3>PfYiKyxuLW_G|SW~gUgf7XF&knvyd*q&`txDFa>aQ_{i^j2M#YcJQ z-YJoEHT=xk7YgjWtL|>m5O9dbKE|BuJI7C|ycM*mI)7)MtM`X$zNkBqr|K;QmKNwL z0^UN2G(u?hPi6aFWxtp}S#yQX%YC$wOSd7XF_#jK5QD zB(dj7ch@(=*5lod@zO6HTuJ0~0=XMT-&ha-3i`s<)_;b}Q@P)EVt?OEeXsgzL*K6b z)qAD-ZYOl8dV6^eIbQPo2)^zSY{=d0JA%GK|DxUj<6s^>v6;O10i5r>AI_@;C-2|+ ze+j;ETk9`i5YoH`tw)&_07BIdir*P~m|FZt`A+m;db zmo4lsgY~Gp>ojMgHe_Ra`0e-<*c24Cuwv8;FK zuAViy02|?0>Bke0Q4YmCRW@)BdBOF> zVp$*1;4PO%It8rZq{6mdvEcWbhDqd%pkD8PrM~TS| z1^ZCnejFWf@OXx5YsmGUa_hZegm{StVwaO&ryl$nn^Nameltfy`a6@Hk4>_F59-#8 zP&Yn>y69Fx-L=Hi4%cq9hPszHSYsoc*Nnxe_XSt4eT2J-V#JCh=lSi8RIi$Pr}^!T zaHmj=^TWw{zrB&_5qqLI@ep`y=M2@XVPr*&bGu1m%T+HnQoTy*o#?kW!d*r&;&75D z`0b5UkNBG85q^8b?+yze^pdRb+Z(B#Xvde0?Zj>j+a7Zo$OZfY!`EZ3lI+_p{>ULQ zYGVH@QQax~cfDBj`&#l{pjLN5X&Xl&JYsXg1&85(X4xn%DBC_wEIxTm#-sRJC z-n+~6E}-AM`-vWcANJF0UirN$59o9IfSg~i@_-)m$~~0_^!JZ}`nRe)ptrp8pQ=2d zFYXBm`43bc(9atK^}nO?fL`*-zoGJgKDfg)Xg{U$fFAP7uTl9pf4<%t zsDHW2gZa)Yzf|SH{BlqGV14IHzeJB<4u1e&*L?Cm4iii zRUC`(xL!~2cDJ73>3%)Ii_WwXQn&AE>AMg0+h=5(QU1ah?|9#tp>8$p zv}2p<4jb(3^~9_`Vs+Z+Fl5um_;sNlvv!jOwxp4H&)8GpGkA>IT*npY5gg<`14TX&ja?`nfZHyLh9WZ zHg{WE_2uC*bf#b)qi#I?xl~fpm1|7;JDioX_BzHA@SFX&zgMa`4u0!@wTs^MmhzK) z5gLE{e?d1t$)g+DCtTct@xF*3eOIB${g8fiSLhtxZLN0xn?~@idn4n}(hA;>=G_Wn zGF~ zDu4a#36a)r^t3|kJtm5uhCh{Opd)lY)`Vf7V{N*J&Qkl(WCDD~(D4%Qj=KJ5=x+Jn z$(N;Ht-V z75E7S<5d2Z8gjodPY&LYcN1_lV58;{bIXoj~YF00SPlz8xnP2$BGw_9H z0)Fw^ANQ>i4-ubG`vHHr0{)wD%}D2j*Of#L;`7pd$D^VDdxTT|*7CQ2 z`13As6bHxT09_OVCv#XkvOh0JhRCK;&t7P2>!^;M-yhvE1zke6SkZ=J;aE3CyW|7b zTENbz@n|iyg=F&`z=eDWLGH9*EblT88q4#!zPm;5g0T{s&&QYJr|urWn|ypZA0ND6 z2z&__yYN}pkM;5KT&Qvh_-L~Dh-^1Q;6pT(;!kU1sgDoQ+`~RT7W()=7Fm2W2_N4d z)v>2_bjQoA)qH$d+c$XVqAlSeW327FpC4K=0km zi}-)AKCO?+w+#hr^}*YviV1F9x7zzGpBEnq(~s6yJ?(3~Vb@%A349~) z)qI`0s`q~q{+IqQ@xK)O9~k%G)@S~@)EZ+hX2K)qJ&} z{>Izng9z#-{JPj9?-{D@I_mzPg!kD067RA91>WEI(Z7%PUcc_hcpr!>!0FMSt3iC3 z-2C?Z&TQ6BFaAZ=kIj)9gD1pTv(gnl&Khd@gZxpP!CInt;6ji4P36laSBlFidQzKz z_J=iX^D*FiOZhy4xLkN;=^!5QJF7#7{j&q&67s$Q9ICx{fmt;KBN!@t;rF@83g?UrXl$?uUO^J;y&I&@h7StpR;sd|!I9 z@`~(viu|tpRxDVMXX8%vhaTW_F)fAO8F{#PhI$%HJMiYOJICj_3m822$ddK-M68C2KCXuoc-DFjPab!);E+q>|E?EtsREnvq6t*xYf^Jr)^ zQe<@w?e&hK{e7An+HXNmQy&fE{x9aq3(Oq88^2 zi^}o>W|UT(F;o=I2u%cYXAG}e21kZdXR(17?VXk-Xv>NXG&MExBFnNmXB&v2rE#?Z zy6^q*dcEJ>BU=r$o{+0fi2M~ zYu+6(Ig_`>V`sR~*uo)W1b8o4b?$oy^jqTOY!84_V9wF|nO^(Fz@*>e=zYO70+{yx zCoo}a`YgBNfarzI-_rcDwHi)Ii zHvHvxmpnGG&*sZjokx-XH<7V~-}H&Z0T@sHsHSTR{JaL-#h?$#B+1v2%E9w!rqqjD zIIQ(he>cwk9O`Eo^KF5KuMRqW@qjbQ$lDVKv>C)$mpvoRz{ql|g4x3+*-MApSrP^RDOr^-k!w0eCq+$}xM7 zGaaWdI}6++^F!ccP<~omL^9U@7U=zHaH?Eb59b3hlQi!>#@vU>AN>2xyF(-0wZI`6 zB>65J$WK$fUxN$sBDMCFjHUI~THg%c?Eu~c|4N+f?Fr^LXLz(teqIcCOMzRszlPsS z`NpS*=t1bAMfAXTb-<#tW+(q>Fb?voA8PNP^AhwE1~1_AJoY`>RR>&Wl7z!Bbo~-E zlrp|d-hhr#EXmLD>Z0AYY0ym>>jo{aeve!lPbNu6zK$*&s^_eH4wNgNQ;mG6B+ion z#vbS{8-326zlm=pm|qL@J|H{1_)cPf;O17<;zAb_4>JC%^;tI=I!*wS_QL|?hfPD? zsm&f>0T=BW!@>5)H=JKOGPd{@@Lj2zakO`6e?RmldJ(-1!^ggue^(E8+J}*^%pF?o z)z0vSS2j=NJjyV4wUaylTTWiXdT^tf{~2m0;i*<-dNFag{s1g?)R%aX zxTfiAK79!;$uoVs=KAQ$%8u|e`Zi9!TjiVAy%Z-NFx~rBZC%=0I{l@&=T-icXwvFP z_%RV3FWGbgawi0iJ`Mi=22EZH4Q_Bk^MkyT|I}C&ZJX!W9k;-zbAT-do}$ci=d4g$ zgz?3n(>NEQI8fERNU!)Sy)b%hHD_U?#2Nh9koxKh4KuNYH-S&%aK@?pGx*`^Q}qZk z`%^!kqCfRp$2r`g{n`2bYC!J(KV?^~hpzv7yXru={@>yQ)y=Gf4*ne3*rr*-8k`?TIjUp~Db?py!8d^l0 zS2nEGGr{sZsG}qwgZk0!Y5cKY6C=@?fsOEKdbX_#F_Uk~oa~0|G1(1;#D-O)yo34d z!rr#IS=sKwQYYEj*Wa|Br+$yYlLzbLeB#XuJ1QCqjt-dqa>wMdN0Dn{kK#w0`(^uk zkp2qFoTPqNe<|dDf*8Ip<5NTKQj`3b{!amSsdO@MO9tr9b;E!=E#M4HjqyKN>X`@-||1b6JCGgq5lQWt6_L=+t z13B{_=-WDvzyHT_CPUvY2B!Z?&SdD@`QEpgawY(c$yS?FiD;ebJu&x2=-H7wEIw- ze%N_Gd(un4*6V-sE56B$cgRL&zrmb?h)|Pyuyb~W@MTHNUQk;3;-KazJ9!rN7%I|HA!tM$F=Y_!tn2jI7Z zwejijpzw_t0^iNRcii;8v;Vi@qt9L7?Z1X^_g$Spo# zeY1e?#ZULWchCRw`qJmUhlOu2u0iaCb7&*o8tB`zXWhBbU=O~a_U{DmH2!H{DM1`# z_a^!HL2|+K+>N*OH?4uDY@9U5tvn{$-Zjl>zn%9fe9TPQ6o>bR%7sq+Teb6f`YHHL z^k-r-`2BJE4#R8kAH0?`;J?1R#PjiiD+TO}8Bem{416|Azdsqs_h8TMp^ePV+ExKq zbLqax#)!mo(78Et*EE(o+rilQYsNb5_Ux9kAlS0Hs5k@-GN-Dany=1rPE{-Wx(QP=;UmfM@YgEw+lBc1#jxLK!b`Ks1~B|u6d_S zd;_1E+L*$t&N=#P^;OpDkDRHw=16DaDt>>NvpHt(@TS4N!$f2-E@NZ8FBt#6nzM5T zPvH+I{|24jAv{g6bGyUfpui0b%!_)*M_`uWh4#2lwa`&Xsum{Rw;?ZE5T=-fvwSNWKmZw`>X|d%9IGc@T%XC#Mb^ z35TjrE&Sg&_EXRqm~ww~ojx2^GTITVl7Lo+#dQSRIs$_tTQ{S`mwUj#jx;ta$F z^88cv!Q`*>tl=ITJ*Nee?Ru64laKOj+^IA6!Q?u<1J0l63Cs`axi(zF7{UIE?=z0bEe_3^%ZV2yI!xmqL9`;`Z-k=;)1scCnaN|c@VV=3}eDln4FXj0~ z&5?fRdE+kS{{y_6`iRLjm;xNS15|So&H3}WZ~#{7$I#}^jQQMYXt&UvG0>*aJ&`s& zz%IMC8~A-*6izjdQ~Br9k$7)Q_EBx!%k&i(y?)gJzy>?7JDE$oAWEB~xn7~QNhdR^!i z*3z7frT%66_LZDFI!8KsoEcwwdmpvej_}4;zxQw+Ed?CiSK`Za4^%bh&eWIqhw;Y# zi9Wi$KJ>ljvG3@SZN1QFf;p&qXkD)GYQAm)ANoBmd~mK)XQ6a9I1`u4I&|-eSFZ}3yzIdvoCY}GGjWsT zhmD8an$(^94$luD%PW9kf;XmMna?;jk1OAu$KS2K40S%1 z9G5Ys#uKh3BQ)mD=SQ`T88YVM-k23;OzOPz6fS+d6O%G@D%zAiExr;>$>)erJIUHa z#J71TUc;||9~{xxi#m_nhiz4V6zdZZ-`o8S8gY)$zZ;~Gd&@)fBQ5ns5#nXhHSaZ* zhVYNbn@u>qlcQeVU6j1L8^Qfb?5j?2E?!ct+sNY+8dM9FeVp?;sO|74VnR!?pC+6+ zp&{0CQgR`FWClL_H2Be6)7S(ThN9Qj);bI3Q8yz_zIyG4soBBZ zOSO}o1qtr*sy&f9MAUoSwkeowo}^K??b^Z2~sW)aY#~@*1-6!$RDF zR7u-ELsR-qXNJ?@<7mmj3!?@ffy33)B*#C+*;C=I?l9*=S~=4YW}Fi2s|!Xeu2!_Y zj@kxc>NhDq=C9?te6z=5YvLfY9M&p~ZJaVSxVh&5KQB{e#kqH(b|v?zaQD?y$hz7F zaKhQ>+U5A7$i(zC`XD|bd{=gylvEvQ&GC8GPtEDs@5)r_U=}G&{?=R{JJC{JDasTbSR@H!z<>8{L^%=&qtom_C)alnJZu*8X>}-k1Ta&SAweV0D18 z3_mXj%))tam$l%eA?{q4^{K+F$iLXI(3i zje;S-I-MZC)D(1E1<-O1A2~{7!5U_cxyJDPPmM!^Zt}hwLW!*RPYgGn7@n)ERe* zWN;rmB05%X%0BF!rd!P2h2_))DOYWB{id2gN#4uux{+~u&^!9An>N2uK2@lod<(S= zwj$q-pq=8;aqI*6DUY&OYwlBmH#>>TO+7@+u9NtZ@#mnW4rI4v4Rs{shntv}k2lF2 z<**FY;6zsXKH|%)ll+DFZGt<$d>pmVIQBzyqW9n>}Ee2 zx&9fx@0+dKm%7^?eYiGYeAvC%wW7zs9MK25nco9XqJN=j(F6S*a$d;3iWGdPcCo{> zi>V#DK)!br(TilC?mNqN zzkLJ0mD%|P$T>a~9R$Epob?wTXmgl*!nivWc|ElSzXDj(#BUNF-C$n~=Br4j6^mYM z3`a2dc+h(5Ea`92$-}V*oe8Rkk}xxPhiM=NNUU4uzW-cuO%)LnDX5 zVKu+~H#q#q>i3$4=5b&PABaPfk8X3R1Z&KbwR_+TAEwN^=-%P|-$Z-BtGllkfaA=4 zCgOlAgx5LXFIDd(4G!}y4j=j#v_m{u>sc3c`cm1BnRDPUd=Y^!OdKzNFpigUtbgU# zOH0w`7WNZ$@6i_wJU0JNYi#L0Fi7{TeYNP@#H120YTaxuPSEXoLbNSineEogp7Yi& zjURHTwM$d;C=K5wRF9(NTb(bF$+jo=mwEJJ4wfB}eUrN9n6`SYqss?m%+a zG;DnNy!<9#Te5GD>b}x%7y99GQ;yd|?7f=IDemHj? zmVdIh-`sWBLA&d)eW)F|otjovS>%5O!m8y$efE6(GyL;6pi|v1tFbPkt@^ql_$HTzlAiD+6Q}|0-gDv1nxYrsAXE%T|e5!Wknq5afYOj1VABV&? z2ln$`K?nFatIK%5S-Qc)QGoj$G>#dAT6dz01Ly&{ej_+h8*IL@YFEU&-T^u&QXW{} zG>-;~p@I0O@rDMj5)C*XH+9cTp@TC9=wO_CO$HrAm{T}|4yq4B2i2VW_UDv73?0;Q z{#xs)c`8pDKWu;&MrF{#ADEl7(RpTtl=5Z!iPOtI365DhS0*zzi>KxPf*ecCg^(ywmyUTUn#bdZAYT1&|i^gdYw>~Z9X^i@~3J?n8ccXec* z^&l3vJ5+| z&OlJ_+`BKyv2_E{gSv;Y8(NqH4FOlV{Nod-y(}8|D({G?RqW&au1(aO) zADMV4eHS^3wL({2$~=5Osq-*4ON_fkRXeOUlx@yEdv&0JCC~i^bG7=HxRTj_F?D6j z<R?ODcE`yjTN;vA6z=h!!(iSB98D8I$=875>6-v7Grfcpir-7nIg?8G_b-*x&n zWc1_7alF^LQ}r!)$BB!ENmuO)OK{TqMV$Ul4E z?VwycxXjsszH1kqX8581;GJqQ2sYVqKHqHPz3SO2-lX@w|0R2TA$FF|{rY(V#Q6`% zfftS61O64OchEcIRfCcGg(?0n$svBP{Rw20Y@uEFv$EX_pi%PnlGawTcetDNfWL0g zdX&lEu|4F`6>Gz=vVo-I&kmK^VoHLTG!jZmZ zj+&d|f0oy+Pi1O{(6)|tPU`gJF8WuWo0y~GY&DGK%g^ih8#(9tGE}*shmxW4J@#hE z&IZ;Do3h-OpTDOzEVVEfyhR*XGE+4ljsD#fNbc?qB>xlHSwx@zmFx@_49HIGe^Yny z&btR?XTt376oa2^BRliJK^nR}wCv0?{-D*>Ru<_z&cS5ocGgDp_!ev7!<;EQyZPP7 zPGFSmlpUt_8b4Eh-U&?I$P>N)ck*+%M_(E8^C1WM`7C2)%FkcQ#-LBh&(_1p&sI-< zE+wabsQiq3@>BaL|E=u22Y80c&egp0Wv4GU@8X?wyXd6Y(C7Mr^9xr&n>(PLWyDpa z-{rq9MYpSdF8DL|l}L}9JAVf3Jnq3}e_Q^p_~~|NL4M&MStIdLnEjhQ?A2bQJ8OA= z3S%0(3A;Jr$ulQc@uK|q#cJPOV(n(F({B80>FI%WMDCubb;K_A*KuNo++FYKLB%y9 z?7KEHZ^>ONZzm#exy!?Y5xiIJs!{*Cn+&dA@LcE^^Y^89y}q(p8gX;=8J6njSr} zO?puK)K+GT7q~lAytBdbj`+>re_435J;xNqo|E4GXfd_MS+9Nd!;;%JWr2%gw#J< zqSw1?h!JouqXk~MF+VoG`FicaDu<)MZSDGaa{t5Rl+6#cZ@Dti{-8(OG3Z2Xe*rGj zr-Rp8eZ#qZzR7Ig>a};!V^PKutp0edobi=h-N!e3u{DvewgxnI&PSBrxQTOn=OJ6L zwaebJGB`+^MXZP3g}9GGIfF6qOWsUF5x7rHjkQOq^}=3Oau@jy*|gbF?~v=odeCo* z{7CaRYg0#zDM~-Hpl65mik_?b^P<3OaF>{r`~&k)KI0Z>S^GR%N9B3f zYfh}?R&Z2GyoCL=a^dbBb4D`9{U^^!;JFT5rNN=%U0*i1()|V&SHkNoUuMAb{otrs z^5n!=d+KChnyxz_v#?EafUU^wXFfWQFIzWU>Z})x)zAy+)=8~T0dr2>&zzSl$DzR7 zjgzAIvE!azEc8J}Y*G&&RxZ zwd)D)9yR~Zb=T|3c+!!9!^Dcy$kjOOXYSP5Fc@EHnQitJ-lr#QZSy~x7>k)thM(}) z=J%T30#D)v@t4mFDfmn|m7?>j`CYhQ44tNC!m}REwH9^YEy`L%!lxv)2D_0j`bM(5 z`gKd+YOg(HwT(ni8Q=wdr*Gci8>0(_L%wa@lJVVa@4NW>zx%WIU38|$7kqmhuxbtl z2Ie_|HA}Jf3pYB+3HW%5_gs1>d3~db!SlwnXGk;(?<2g zMbl2qsfZ#^6!(>^`9QthhmE|^U$q-LP+;W$7&rM#;0~+Tt zL-7)PCYmpHKWlzdE?Bnv_6kdn1>X00Jad_Ulz7c7|^F5*)_pPdcTmf%y^N&>aK&$nBQi z>N8L2Df~S6MK}}grgGLgjxHXGx9OR9OH~|zx9c+THu*4kn`H6!C&t#igu@7T(%+te zw;v7QO>(T*-FKsKYVo$l!`p8S?0N1}JRc{&LHcc`=ogyFn>Gb_%iZA z_Z>|}-`D=KzbS&uR~#RFRfNMP=Wk-X%t@{>@D;l)JZs7EoditP=#M==A{V4N+TI^@ z7GxDf7w?2G!yiSSBZowrYlMedtr@xtI@DdTY2@eg9* zu`+dBGR7J{B>sVS^N2Zipo8^Yf_4eyS||Egy7?tfC%=MD{u}3*h%Hv_zcSi)C--Vj z*;J9-LqGe`qg~LC%}>g8@1kEnk8kR3YCp`;zL3@?*KI}~2<{qSReurmtKP?;+qot9 zy6DL{+xwe7&w5dRqFnp@suL#pF!v6=UCrMs!TDOAO-|oJ+EpA6zUcEzaD6#;ji{A=Z(;;>QjhM?(o_7Y5O zGwqY8?@!RT?kPwJ-Y0F{6RoShY2mK=TyUs0{iwI5ALe=WJN@gnjhNont}(#Ree8Q{ zeTd~xOe3SaZ<5TB?h4sj9?bn3V#RvD3trq1q;4F!T-ES-i`Iv@YAd$O8rFt8_MjE? z5@+6>k;gY*hh4CqHR{(nQtGWp_sQ;2eyr?}4XS}jZdw;PfnDf?6f)hHIeVa;`q8oR z`_ZM{GXw2ivjXkwJ-Hpwe6cCQ+;bQOKFORE`!QxsnA6kfXXIdC7cdj|-R>x!NV`ul z4`Y8od!p&|HOv)z&FEW`i-pa}x{7YotZDjDo|i=X&OtsKJtO;tel!ooFV7=RJclz{ zT8r-cC5Pc%;4Nhh%CPmi=;yDTVUV1E%{&X;M~vNE=vF=1-xQr5n^Hn;z5sEtF#Htb zANVx&B3j6;_I#gX1N&`U?|ktb|DqQ`VCk57KW>Y8X6 zn4c8yLLa40@)rgU;xnA9`zW*i zp1xfXNlZLIZ~TL~`T92E-C@=Nu7>)n5#r|36`yBp^u`LKH=>2fZz3b6G1oM-w3PRi z(O~i(=DPzqt~h{bYBw^x2OZgkj_e+h*Y;I2SDWK-A9GFDYp&S+@MRZvspfi$nPawl zGS4*lH0y@EJq7$O)c=vqidjUEkyn6&vw$P>-8|l@=5FS@8s6>j`VwD-;Ws0{rMIw` zKmSO7m2W%k+~1%7VPvJ~VJCh2JlXT?fjn8)-Cy+qjf0%k{HuYdvZW$9$YB^~{H-p5U{luHRIR9ql0V&Qe-?|ByOupH2dmgYGxzG8b@>*oTv%s06?-X+0;aqJAdEF__ zxt_q^8ysi;qo`dpPfIpHDy#^af^*2w++vwZi z6MFazK6iqBYx%?u^Oj5>@g}mFzawj@3rs(?#EzoiVmbDVjhW`TuVAlaF$aCuQt2cY z9|w=n-+IoEOrTGPwN!tqQWl>EsU|pPZ9hpqoO0(l zb7S7i_MXVug#>2<;@sK2VGjEw+&7r2frnT(>15GD%cu(%x7_4Z-QA_N2^xD$v7B=H zYrY+O;_J?W{5bIz;8N|^7Ur|zGTue;sl)6c1QSKexX-97m~%=e@=W^UG&%6B*90rS01)hhTvYqIaj)kT7< zZ=7g@J#d~{`!6sb%|q~;-&vue z(fqX!)Q$f%n|g4&G$+N-ip-o8mwA^vp0sx^Tc?`XP#x>y>%Qo7_(b61PWVJP{4}sE z<#)|f@+hh`>u}7uqAQo#`=3Qu(ntP&_$vM`F!#~sx@Q9asjoN{>O-)__&x&e6-& zkubg|KKexFnKpiVzI&IaKNE^GvOZ&>1NC(-eTfc68hS5suk&b6>-HdeN%SZD2?wIP zyO_JpCs6D+$6dUMF|gYuyC*Z}{jXboF?Is-K|CYfa4)#4K|h$-E@yJjW_{R8DZc~! zIwO!x>`}M;%&GcYFn|6&&LN3bZav8Oi-(MVLB{xs%OJBC==(nbhx)kA>qG0Kd03vX zb2*D|cbA2d0rZ{bB)mL8jKK0yuKNTqY95{DoI`>81bvDg>X4m=o|sdVHT^Sjqy%>} zq*$*-FJmvQ2_=7t%~g-S(7uen=U0O~t%*9xuM!&-9jkBG^quF9>$UuHt_QDh%N>n< z;v3=p4CakZ$C)>8Em*r7p7QcoZgSS$&X`)qA26=;6g+D3_cjCr+sT_@>Fm-3N4- z8e1H2hVzcd+m z^4-PWm_f#>WbPY+!R;M4S~^}h08ie)U4*iWkzEfNy^7o_LnlkGCZ6P;XD2HuxF5xy z)Y=XMPPG-^ONJXh;2YT;rL1ED+LgcPhZ?Ad5t-h9~NR(OlCMhSFYg4zfzw?(IDO;Y#~x1-M`v+i6f|6j3hEs9?x8$~DI zV$Vi4$5!NyY>q?|XXe1I_%8wPY@P0iHruD-^S`wKn3t)^Zp!Ro}~AE5=5Pj69)PK8Et|N}S^DqP@k$u;To?VAE+yCv{q~e+h6Ci`zOq z+BX7QETQ$&T!3*2agSQ^G!r)vLm@Wa0p5n=gNScaq2ca6=p~(h95T7c{W$VKa#-;W zwKeaj5!2zD!hvrJ-L3e!`tAg;{Z{;##LpeGPn80eE_9&qDZ4IQL!LFb6OQ&^vkRwX zf{*cq*Dmb(Ezncl(by}C(esbu4*%bO_-;&tE3@2Lg zjA*s-`baZtZ)}Gy)~S$MZc{cnFGTsKh`voJAar{=|6wSYN66;R? zl9iHwW&CrFDQc(>h8m*CLEUd!3ZJJNj%@q2v3IOY{V;x_@Rnc?xSQ`~$6CFb>q8YAf zvW}8Q4`FHsWTnl%r(t~pNGn`RSUvJ|t*P@Fan8jStt7`Wv zo8w)s-?#C59kM_?B792Uh(~(yv2soN-xS2=7-@Ae3l{rpwOE9mY9sldg^_>5N z?#C>}R@r3tCp3oEu$+9))GWJpoXsVNfV($C1?BP^N}-3owZY^^U$*crK@OFW53^5W z@_V7qS0I1#y*UU^zt%h$OL*x(Hr#=)ApFRdG`bEP6)v^9&e|IV?g`(sHkbHN<6p_0 z+Ez~?pUoJ#uIl0l5BJf(WPsY;fvguU#Q(PuOOl?FF5QPdS%qFGfah9i`v~$kTk(VJUZcr-cyT$lNhkJap?s@@v|mO04t_6j3b&_|)}E0+^*6?IkTJeL zRgJA<>=yvo|CPa0uS5kkoATpirk+!DlTp5_WRJCkDH^g!SX!Y@K@mA zmCra8b&RF?i)VeGaR&3xw8c-sHkaJ{i~wGZ>j?Qmu^ zVIMaAZgRt(QtiEyU%a?2L+S&{qXZFbFt%MW?D6rJw|cW<3` zp!^blRU>3Fr|qKaqZmg# z;`4{_Tut7t=sj|crAu_#M9E6%b!-}cPQPhw%fN@$R(TW1!DI2e%JrMt?F2WPllR?> z7~kz+e$eB`Q{xKuy8*|)lzIF@iC+SWX-!yc?QZFP2!+#__*6WGBWyFZPM zLkyoe&Souv%ha5e-?R)q03RQKpkyO7_)SM*m1 zrmr8=fk< z1cyW8*lJT3bt)2^F%~}sgEj{Cjm7;sV^p5H$lk-PHI!|12QpPOlMk=4F_PQ@OfAS( zc+b$#Ek+g>x}U^;)o-fBye6z1KVqHPIsU&$`wA0=0*dOhwV9C$J=>1eBPui!J@`Pto8%usJ zdrkdyBBM3u9OgPq{J~r;jd5<4Ip4>eD+|h+xtFZr`!{ob#KT?UOxf+k$KZ8zv@86! zLg(@o_LNy0Qg+3%4(jV8d$O>rWJ8M1wAa~#Et!if?5@FnWt?%)@8g$c+KXpA(}z5$ ztF`u`gPlnH-3NqtSUe?%TEEH=8?jKC)eQrPl9<1y18Mcs4cq)3Oa-P@G2cD?G=k z5{^_)T)Zq9DSyJsLTevh%bAp|$TZ0c+P3SQf6o%tPmsKnEs^8CimdBH{=W84-=@lS ze?d80J#YRtlU3|nHCFk@XphtWB-!HRzRBfb3 z&!8XK!}4zh2RhK!J3=Slu%X|~Ut^v^yEwVsouly;5~nAx1Q%BzKevR!<&P25`y;=L zc9tUJ^$ZN77Cbm#fS&g6dcxkyPw0#s^PIHV>a`!_JB=0xMQ$7I_3ahR^QXvH8-wI7 zF#ODm`AzZ1*jM^1xLafXzWY}fDgM~{)&45;|39oQ`a3WP))737PX~VD-Th79XKfUt z^=yyLDcK$!ladFEJ9T)rQQ~9pc?NzPef)u6jp5_(%|5GB{wn%XYza7?V*KEFmNBlq*& zv*AD4?P{m`5WWl=SL8JM@8974GVpT;GB-}xWFf7~J9f0Ome zgkL`Y?~%2NSBVy}RX0bRu}KtH-%Zy`HsrdE=tuF&a%jT(GP!OCIoN71nt9{f{Y}02 zA0}=IO}qt81EXy&%-%A5A!I}aGDI=ooKQ|fLC9$sjXbO;Pfa!=vVpOQORpT$wo~!q z(c{L?0e;D^>Jsj9LWk(gavkzdV@mF|Fuvr%6zI&-em?oA{Y_I@6Uo0E_$kOfY7E-{ zGqk!_J~6W8HojZH-_n&+JDhjo$j7PSz&oOysd4-NJ-{?uvd`!}XMP-eNP5iwe~Q>! z=KtgRKi|yIH_$uw-VDL$K=apm_SkX2DZ5Vg&b##%<$ z7l7}@BGyPclQmxv>8MCvu*pdlhKd@>&@p|;!*=-ric_5A#i5Zw;%(vm@AM@fPIusG z?XY8Kl;FE4rk9T`Dg5jL&(gi}ouqp&K(4Xg2%2-}znr{{s6X zf*&dRk<8CSXLhqzQ)+8hrZ4+eulCrazfa&_4fk9=GB*ytpGqGxF!`5;gE$y&ck zXhVCfH~vEFs(W0t|5oI#fPSRsw7=rZ;6Fl3nLb;ZoR%AxvsT!~b+`3bwL?pp@;@&5 zpE|hwk9upID#H)=_*w8M*8WrCXUO<>q&KzhSFq;dt#ypQ7}>7%xqzI%y{yaQ$OhTd zl9Pg0aA!=OX$}BSU1z%7)cCDW0YnS|j(J zG`=@&W#je1<~88?s*E=}v-0Z~xxPNmcb@=<8uu2)_%w6OT!)V_7km)tQ2A@Y?Wqs-SBZWjEs~7|?h59h zIqEy@cPPhrSB=wmy@wYE-8ByQ>dJ@#!(VmeI+uDrM9}jgoUrc8TKC&2wyQmWX4V)V$d#;DyAI^@jYo5b(=_E6 zYTq;0$dMjwyq@Nx+8R8X=)sm%Y&VVm6%QW`4uZf{hmW!>UwSsU-N9bxAaj%I2njba z^eDbbUne<75$4CgWPtnH*uXh3i)A2e0Qlll0tUk)syeZfB! z`%`oDVfW$ggfD&A=Y7x0EFbnd=Gk-8k;F-fA2Wa1oXQtVFJa$9YtK9V_Veq|fy~Rw z*F5)+d6GZ5?r>;9c+2AX8gtY-9O2=AR}Hm@nQH?1vjJNofi7vObdrxf$6keG5g^yo4Yb=c*92UAb$2^9dp)MJ5~Jq0_SSf_IdNncV9~Bw>^Kap5AzJP;Y!? zk7QTAD>#-j_i|*V)=%?yoXN>$d~0Ii%wP6n6#7C~Qw@{cag zid}lJ+#2B_;o(6tO=Ail!v6gfu>jf38c*w{d#tWs!Fe^_Nw!FKl<D0}Ul*PyE^d-D?uy$rY7n|AFqXqQq<5GBZOzzth zgYQ?p@2cTJwGoc}b%{i6o6SS}esr*J>X#hQcZc*q+dHj1%k_7hb<9k$KAUYj~%su5_`ta)lmas+EG+>7STTF+lh z4heDsIpweM2cdPX@qzSCz6NVNVhF9@&t9c?SL>Z;Xgtr&;pyZ2vE6npRp;#8B@edE zW9%c~J)PAZ*=g$nO+r2e0?3IbNA{elZ_tTue+v5OLO%6STlS^kNOB9tH@{wdL>qT< znOtw3Db!qSz8duc$>oUif7Jy}lNW1pB4#>$VPr#UmSfIBLRT{a$j#0mInE}xGRHj^ z`BNvG8aRZDDE%rYT>0Ok2c3OUzviq=lWiB)8S&FH&xpTd^15=5C&og**DjGh&ZeG( z>hJw?V(ChCo^}6a#pdIphJ?o@-gvsHS?oeL8a8-k!l%^lq%j+(Sjn}$Iz(Xt)82wH3 zTjz} z#}KkuBiYZd@P)3jkUJpy0Pp0-^gvNQf`#a4aWNW z*WO$64_|$4sfSn!byI)z-fQ|TFJKp_is8I#Crb4Jd52wn`f^3s-E*LTy;5z zJ~LFE?Ns<@&fmay^?ml-kj>AMpI#Dzj*sPR1iaaI4z>?`XKkcHw*i}1w61l$jX0Rv zwotz}#@r>};PZ@FI=S`GnSPUOJ{_5KKKNE&OG1)IF$S%da44Hk`XWf2 zSTb@p=Q%Fpe1~!|q#w!8gN}$P!Vifc@)Y~3d6d)s18`*E;Jcl?$8I#Wz+OP+&*Dr| zjDK155u%QA*9`Inj%UB70Ubeq_4HK@{H4HOik&_iITK~=!rtAGVeh;{3;WJ$E51zA z#{%eL6=OXDE)Rg0+&sbiAn^7YcqN08*^Chkao&I$SKkC)NA~B1_(__7zSAdPGXf0Q z(B_WT52>I0XrJyN4Ibf4T1p?l^>&Oo{+w~uE=J9tTy#zgc-V5XwLJqbbI&2|g!jni zPxnm+&Su3b;9);Ta@(!%H606X#P9yQTX{EJ?~tYbyPJ8pPv0UxR$~YMi@$fm^IOmX zJx710Ej=xie7*Ry*xT4D@cM_|>u>t1`hXrwh{xT=H$Khjypms&XfH5n{Vi>=?~H9I zIuH%q!1ui~iNj^I|A%RxPkWt3$Z?nD4CJ3GJy~hj2Rd4qu&@ z3Wc4d__CD0ef=jLZtN@I_Uq;<|6FGeh4%_4`x&)WJtXO2wO<_^{*GujeU4-7hA&Xl z+qX6Q*oWI#?5x|5kGmMY*~586>hC76!{3#EE7?4U_{m<*l{a2Lu376fWrXv>(!I5} z<<&SZRAn_bTy<1V!&3H;cQy=f+a;eLyHekULbH22@G7$weP&wbi-JiV_83{a{NVO1LJE(1;-1gzvs6Ux;ru4=~GRE#3-KA ztX%1&rqZGGZvZ{G4}RW>jGAlmH}l<#e7KP9JDP^K?IZuFXY{e-ANS!yk1wflk~_hF zBm0mB7v495Zznbv_fCx8!JHoTzL7mzOI&axKB0eBu4Q0u#qPbpk3HWmnu^0?n$uG9 zbLSlGv@a#LHz%8C9?yKI{jS5zHG&M#TpiVA8e(fyQ^QF(@%ck@*zn!=nl_5on7?$A z^x0LR>}RNZ&An#B-?;*K`$jwKbRK^x?X>3pnrZE8z4@hd2FzQt%gnr#`M@j`1$ z4xSUR^_)a&KCNcLeI9TNUeTo9$M^PEonhd%XM$qDwh0;&tWU6}gS3|Cp2lzK(2Vij zqt6?^iMxzCgV3N)Yi|z1JrB4q1#TaPq4a+85Expv^;fB;>~7Y==Qr_M3LcD$@2+>6 z$pw6&a9{0h1^uy=Q$hs|*B({Kd1|d&zB|UiFMA-@J;MC0y=iKQ@PD1Tm%gqh@5+ZY zeVE#DY3NAy&mzvo6~NcXiT1$gL-SD@n{*0%{}DK`c!|dR0w*}#d;3Q2u$lw!ZOn7pALiSotcm`77CCvgI-ZB9^pi|nZmjY$ENN8IR6W=ZeJeo``9pb@u0u zooMt(3*$(xguQ#LhStn{cgfSnUzHCLVZOrg<-k8M9`~i_oqoHR_uI4<%rS;-rCKeQ zVw*_zRF8J5{&YJsnOM&z@?6v=Z_d&gG2~h;`%|xsU0O65{C`_(C0T_$4ukuB=wrnm zr|>s4UET*i4DPILs{BL4@2MbXaDdU^;4o(^#YaAkMh?@in%`7^Mzz(2vlHPTV$bEb z>i-7U>}B8^6gUMx3KJ9I98!BXvLpx|hQZS@?7m9kthG*XzI+g8{+v^}|E@gB{pI50 zI^M}&_&xYhzh<8NUw-NF;6Qu%8mCQ9<`wgBf245l$%IVa8^L?Qt9ze>Th?R7R^+$t zsZe{>%GP?wAAEPoeFvQ16Mf76?uHjc_p;w_U|lX|J+?k?@3j=q>?WqRmU(8L<7)v2 zEyMsb&+#2kKFMm%?tPxKd&esWg}=Akyoz?^7Gw9HSMqamD_P@+jXAAw9D9!MQpwSW z9h0+xef)3F@$KjyX3p{Ls@M7I?2Nc;F1abehKpFYF5dew*J0cxp!K}koT18f7xL8k zy=tA`TTx=Q;}#mj$X;f^Yp!PFS*_*!Vf@3(h^N za(rdWCz7qu#MYdt<9o(9wb!Du*<&gmLrO-Xq%(>W| z#8dIb-$4`Ny)T@?e(uIlQn=h(HjMp@VM*?(dohv`sn#(l$iUEHO`{|J}6Psi7*?l$olQvOgcM1N^XV(U*Vw{uE7}^(z zXrGCF+Sz75Up_#dyZdPEZ)Cf_14a}71x{?h&6WHUyxTT~k|F#^!CaZ(3@LqWo5kKd z`R85NIDOGa`kU&&Wf)jA<`rKIRGGcZnyjSG7F5RVe&#QMZJ-@_M9)JXX8xB-#?FlP-9vtlzRks-3}~&WlaPB1 zG0~;kCoFOoF%Pwq4ZKNhMbnMYlD-E%iB0#IqS=BkeVw=nWb=HkReIR|+>5S~y4l#qLcDC+br%l=NASSx42Bid2<0 ztFF}Gd6eAk)x@z+8R_IKIZ?7b*L|6B<`Dle{tLRNi*qQ+W9*)}_(JC`&uAfMk36!e z#DZFO$|n*Y#<|;%bs9xC4)KrZcB^X#Vy_`&1m6atHV&XYc;V`^#FEmZKilTVem=vT z^_%u?{g}@jFXqz$4u{5k;?$$r$3BJbk>l7|=V&fBp7!2G5I*#X~E8rr2UO zG;4YP8|zt5tAW{?&TC{;J*2R;>Z? z>JIoqbgXf{HDsJGGLF_K1sqyW)uoeO=tj0}3Oc5i$V>F6eqS)pJa_${Ev;zmR_KIa zWWz(edx`J0&T(i+dSkHeT&}4Ja#b+*nI3$Nx%e90#4csi`}94DF&%%J;{?yD zBnJDyKI;n~iQHHh91i@72ittSHuyq!cBjM>ybHh&!~~LG_TE<$E0xTe;?3XcJDvS0 zG&m?^&PH~FTgj#)kFfGY=XrD|YB6&-`Y^swY*e5^-{YeldjUE{b~EROMwg68%y)-F zSB!Hx_}E1o{Vg2*5}Xjnr=Cop;qFkuS$B_c&a0u#PTCmS2ER*bclYSio3FV__$IF^ zI1b*+ahKfa^BpyJ@rxCQxtBQ1TH-KQ@mo*X@MN76rJh$UzXitE$v1{}#Q*8OLEpF) z{rw`eXV2;6xeJhI!qY$5n_Lf_)S-9N$VAcPdiYZEO>kTZ5B375=u`OKg|2HP2WkBS zk~!Aj)!BhN=L5GlZeU#CkUnCdI8Y%OFFuu@p+1L`<1P7j3iE66mjANdi!~R%Jxb>fn0pvmqjgTbi+==s1~=FqPd$PEf(+MrR_S=jppLmi?2om> zl2ydQeY@j%WUcz%!?_F1d6#(>5cjh4Y&gU`k37UYpDPm0iN3SlbD8H-Z;m_BwL8(Z zQE;ZYMzM|eLtCo-ZqS(x z^<`<`1Z1UI59HjS{l)LhKPO}U%f0zOYUZ8euJ`8uuzBXXKjo?Q{w#QqO+P@FjJ?j} zUgfyI6_0o{xsx^D**z?IEWDt1vaty!Kz#-{dkc(Snr;)c06u`Bk#@Q|9kNcN$O8UeuGTm%lf7mZfVG`?Jsrcb2-! ziF^4Wt&3_fSpHmx-HVPhJQmG|_ONv({!wETx|4?J%dQEIu{$@>hncshGuP6`>&TyO z?JZ1@?9Fkv0AEkpFk}tm0#^|^pu%OxZ@A0wENWz;!_qaecJ$}`NLAehvSWs!=XUBh z-m7MrdwQTscJ`|4CpIfD)!4Co*N9KChPo26DWrd2B}Yi#DF6DIP1q5@V(ef3e;Kjp zLiEu+(6itYJc4r_=M1(0Uk|ud?D=2tiQsd7%l8vv?UmBaz#=;qJ88aCmAF85YXCT- zo^5%=nSm?j|IlYqu+-n+%7oHN}Ueg_n zlV%LnLa7aM&%{ZAs;jS$u9Oax-=H<98~|5oGDQM>v)RiKM!plj(?sNtuc?G{B!Z+ z2j`zRmq|wXG3O$;<$0%SJ@kAqe{3Z%d<0tSXFpkILB%79;{sKz!9%OijV;I?$vtys zZHd)0z1aQolkWqrUToV0dePEku1j3+p}X-fGG&D3yc#)ac)!-^I~BO~?kdJ^g!7iVSGinie0 zU{-8Ad~g;zaKUrzO<-3(B$*+)R9$4{`S&-BXgh&_u_#Da^4gRd8@7efLKWH z+F?nZgB9LHn-fQ9AHv2a*auAJj)6DfPpwbT;}h9s(l3u3ppz97DR58v+aN!>OC)Qv z-4_K5-)!dT$3n;}oc~??BEdG-gYC$B`9JF_{x&*c`tW3q7pH0dSlx^-$)`k43r@k1 zz|Io?JcSHU|2?Ce#vbgY1ivq3y_fOZZ039``{=UgKl|f4bDwY-zQ764cLF(6@{PcP zF#c{jG^||v{O~G&EBBNd0_c9NlP_zYU0~~~_%_!Z@_gkZ)G(g%e=CUvb~OarFZbr% zXy#q$-sa7JMaKLU3*v4wGymh6|L2I8%nk(RKgWLGCEWR(AQ$Jm^DbGmdO=`8^&P|! zsgJHb2F+b^Lo!1?nC5>s^UstKvRm5F(-zJg_XoiJvS*L%Hn> z-`AnfrLUy(q~o>{bIjD+zxgS04?QQpPdPlw5h*$#KcvXy6R$d3{(O;(y**>q*-rba z-%V^-Z_b$Iy0Z)nI`?JI2}pk}hJPjB5|RsiCp{ftt>kZ>%XtTr|<^M_v&<6}p$z z+qwRdxi+JJidXbcti=b=yf1&pF?#4!`h|aHNS36LN1Fd$_E3!M#7>TQxDdTc_G#X? z_gTIe11{ua_%WylflKil#V(vPs3CTNGx6Fq@j2vx@N4a4doT2(z$RU)wG^z-_lu)= zXYEqrR~c(q;H}}?oNv+i|1eMLZc64Lk5Xpd)IH^U$(#$e+Ie5kyodVwOIRzb7i>)7 zLe@$$`Eb7eAAfJ>`-*wyxu0ZDQ#5q3*S}!eRR-TPN5vE7m@%Xe>=>1dk%F%^u4r2{ zE5GS>Ghb`#ZTW_1NVv#zUqcS4&vj;=+3wHGGuM5D@ip#e823xSEPbr`6|A1#=WHcc znz8krWKYb%Aem-ihycTWU=U3vpvgLDa+ODucayWUnmiT<79L@2jS-bt+_MZU~Os-bzeaymWm)Lp$suP^!N*8|x``6||6u9S0Z=nYwe0v-^T=W(Bonk5l?$?bBC^qNZ zv<_PtLw1mGW_9K`x03j;zXpoKf6DZ&+VtXA8(V&D6Lt*n#9vjvh3;u)eT&>5AK^5r z-_7)E=oVVhH(J-{9z2k4A2hy#^=Zz8-=`ykGv(PXcvbRj7q*Ic`XrB^24z~VyBPXw z#_t#I3xQ!Nyu7PM_@Oqv?<03O#z&UkI)y%l%D(%7%gR1`M)fA(l8>x2$dU_(%2$nE z?Nq7Gn(sgDUptIDc{qE)x{FWjxz};-S$|RgcAmev*5=L$-nYCO4BO4Kz`VN1>HgK7v~Na^vqpFGx6U-E=BC#9X3@X|_B{6W+ZYDz z6vK$1+jK5lBh?l{sPL zBKeGKW(=Nb9D0_e<24JL;G_Ba1^0720`D4+u(a+eFSHYWM&wqMDF<<6ta&c&5 zV!!y;PuG#h$+yOjeS=B;}Q1GrJMyS}SVpAK4bGD@Vjh!kyM6uaq$M|;6ZLEz?Gq=D~ zns>FKlR|ekPkkRj#;BI<($A4osWn0lX#A+`s?Ry?>pj1N{gf4v6zA=r8-lYb)YvqmA9g?S#$&!VSwQ266{UZ01Mqd@WA7Z=> z!Q$-!=B#s8A0&n#I7Pd%VbDd!j&WGeZe)q*M(|@#nzJ*qHRj`ktIu2mdx3i-uuE3m z%NdBw_@nUS#~?olZ4U4q^YY`69UU2V19I@zZ}wMd4TOJvUqBse;lBbIta(efVE?=r z052hAp);?#*YG(sC;6iDEEjQ}MRG$h`e%C-i%>n?XQA1H`L^2o{4=@H12zmXi3gzj zZ^)hpF4eD4Y~l;oI91Eyj-iixiA`7AP#sSCmXP}*JV(Qb?4s_?Mt zusiU06g%Kc)Ldb|i+j#OZY1@IWWlCS7~e33zpZ<~QuU7NBE0Q5pG>BvbN*_j)84<9 zf7}Vs|FDC;l?^3Wigo^RY$x#aAn$rMNq;IgsL-5^-;tfy7Bu#TQ!ai;)BoMd3x~&* zf2X<{(Jwev`d%{dN_h98Y~?swdrf=(`feX|=Ij37(I(f_J}7i=p|9WXBd>2N=h*0D z_J8pGuW1wTzUPcX`y%?Fo^gBU=}yJaIsN1ib9xS*ME}@1fvZdC;~CG-C`1Q|riev1 z)HqX`+wpOxDmR;mk)bp2a}4;q=O}xoTJx)9O~gw*@L4x}_F#hk%RV(lHPEVWwDv{; ze7S*pgPcp9D%nW-PJX{=p%WRIX&=1-&N=Tl*{oqakks!5%-h!08O(d=X3XE*Cm!RT zdgdVB`W7;Gwpfq_EgGd}KYN(Rw57_jl4IZj?hz~39(rc%NPFg6fbCiG7PbJJeh;zMp%d)+iVegI z4t2glza8p)h5D>v{$1Ds>Q{5qIg3wm&f?Z9EDhfvd@=`}1D9;7)04HY$aK==!=w>%X; zJ3jpVxyzly`X_2^Y_b(P7odKk;QvF+Da)JFYUcDg>S<|C-)27AE0+(X*pbG%h%pzF zTV2UKj2*>y&4L3M!oYAld}IClYSrrCCmOE` znI`*@eSqx|+NkdYZEio_+Jc>9%-M+*vI8SeP4DYPMoEs{#Ti2J#(SLUFtm!T*ZR-* z%zkE{-c6(Kke;)si}C_vL9caQpDAoq%i?+`WpG|+LhC}v#=k!dUD>pQ*WAAw&+dh zt1C3BJcxWTb$QS$vI#kNHAQQ*_on=|zavY8OX={7%zhVjk-+aW*uApLg5-q;g4hVi zwU4pKApVr@{s!~^ce?+1+GOhPTj*E1yN*0q$*fF!qysy-2YD43VvqclanUhW24mMO zqL0_0{ZQDRE9n7F`H;oZf9N0mjnA^+xu@T=ai)3Ii>jeT_=`S@+{R(dSM88-#q%PI z&J?|!!&x@Qt>#RHY?lNzR@3NqY_83b@F%%v340oNpMhp{?~d9=sh^Ts>$JB6i)ivo zqDA=X_UkPjZ`&pQluV|6@r&Y#LiZfnh{ib^m|y7x&Z<6!nhLC09s0Bfy{!C=zhiT4 zA-}Sbe*TWGQ9O4GIV@Vo9{OKSTu1cyD)W_XC>v1r-%y)RbDh9=vRkv5tLk#e{?NMn zHi)gymER_REfMII{js@@b6ZZ!)Fkn;{O4<^vqPLgHiYaw*|1;d-B{#UJ^d;c*aeL0 zb1q}lFveVL-0qe6ZS#OXU32D?y0}vzS(t-uWb!8Qk;036C#Seqszh}|6X21*B62GvsY#O&zcTv zQD0*e)pY@9W@jC>q5ui<7?P$%9ZPaR+KB3hUbRn%RT(IrIRN% z_cu)vUD8%~QGUfS;Qb%K(*+M}kNY+F;vCjNzM0x+UX|g1*-u?hZbFdn*YH~_u!N0o za7MB+4(#CM=lDXqx`!oe!D|olEi}T~D}q~lApO|cJ=ku&_$n`FIgPy@|J@0$PQ9eR z;ttlnj<(Rt+fGX`8N?@5-vyyy!)1r))9M|( zV^%V6*@G=}`;C2W*N**3##Izis1aTSMexa-ZN_e2d4&Oda3Joq|!(HQ$Igj&%B~CO~iE zW&hhxd*5!U8T|GhzHQC?mOR(Ud+$|6pa;pFnaCH7JK6g-jz3aEJV4{5<_Fq2>(t)E z9bDZDxr^%>=ES@$O^N>sT;l#O-n_QIX$SIA#VBX`(ueV^RfD!qYR*e1Ox{+;IZ zUCXzVg-hP2`2PsjU$J;Y(nO&g2n{9#!?=%;ycO#(^?K=9(DY# z^j6 zbGD}c2tL2*!1(7lcyi#pJ~o2t$;8>SQ%<+~FzYc5Tp?FVu(zy@GS1zJZ}WTRCY{!e z?V|HN8izU(*fGWK{vSDgpAbJ_qZt06o#Li<9*C+_czVnDZPe_WRKj$zh)ul;Ds-my|Zk01>Z%HOO_YeXIP{9S>WX$xWB=S zRc!Kh(#W;d9uLKWiqmseop(z^PUBJZCp#~Vt#Y&UEwu6r`t4d`>$Fc}JfSv*tLn=CKTi@M0<~DK0@Wu0%iF`cJ!hDyh zfw{z$(77<=WBW$W+_L7ox}M|E{%QHx-nvcpb*De$K6t45QVovWsUsY*j~;x>$rv;+ zV<~%H6n-D!8WZEtTw{Rlz=(pA>=SeU2YsP-9BYZ_OO<=Mp^+WW`$0Q}o#sq|^yz;B zJCLiH=$7H|_FI(Q2LE*iMLMUY*zVa1#+P)-CjCx%_d7Yw_!V-BtzXYvN&Hfr^v9`f zvMKc5zws>lQG!19H)L61KQ}mVR@32fuPd*CHOh79PswCZ)~{3+y2AD+&Y2^F`qs0L zt~-T}>p*T~gNRPHo&k+>zpgs1?fEnMtn}!iUjdQJdiGgskM}pJ``?*spd-&jax&1t z?5hmn`mDqC5z^`GuykqzvOE&s@8{8h*ufu@?Wy&op6K?0#P0_O?W3T~6P}m7djm2f zKeud*Cz(?UC(%~8sy^8S?_gv6D8-yHA16HPsqd*zc+*se_7rQ~S#=b_t0;A7PowbI z2XB=I+oW+n@xLJcPTr*(G_I-7V;}BJ{}!~Me`=naG1{9Zc@YjqUeH^|kYTNH)FZ>{ zOQPXX_7@AkVrZ7ZSj0SboYn#E;2WrgPvqP>{)G;+&r9-fr{>e(?e=-C&_1uZ)SsJX z_juv(OYZS1f4;-)@lrYXex6rZAHm=LdHSXFfa)JaAG{eII}RIAcy2f4v$s)gVvM*B z9@((Yy#<-H^$=_D1?o50FwD8x?*XqVf613Rl6&xkugXz9_`O$X51z_wA#bq%?zt~^ znEiJugSCR^gL0OF=e6J^x}@m5m-;ewA@A^VNTc+x^sv6G@MC0B={^E4ZC@PHn1d`_ zh3?M49?y|{)N|(!=}sl;kK&gy>(P=G$$c8-&t)u@oJh9+M87^k{_XHr{3HC=;@?Q> z96@~>6|V9 zBVAvnk8V8vbVvTn=-t2u^w=+x!`TooZQ8@oZ0AvM`cLqYPxErxdAWDF zIk%Vs-CxBYjCJyQcH)RpaLcZ+-JD&^g+ls_{`g(|VroeB)7Uzd-*z zDIJ7d-23mGQK0<5UQ%B5TiHv)nJ39s(!RIAhA?|yu^U^kqm*BEjd+^LBYt+brMldG z#CZJ-KBy*aDfOrBc9gxt8NKRxUB7FE%-um>dk6dJ)Ld&@$;RrmuZEA)TG}t5VRzf= zTgESL$DK^|EpWcx$QAp41&v)b@Qn=FRh0duTRZk1pl_J@Dzp|Yc9=0lWpMZ03xf~- zBx2{Q)5)LDyh81GJ9WS>6~7-l=>X}c5H8xOe~5R#MfXSCI-TsF=t;Y;G^0UtivEo= z;DhEo&%g`W+uNb1>6^^yp1|IogI;+G9q|r)59@I8Sos6}eFM13Hdg=X2Y&Z+ACYu( zuy5#{)FI=f!~c$+lWncMfvtTvbCj^1U4&j??ReZc=%F^oXq(ztMf+NrqiOCbzQ2m? zrSZn#+thUq`fm>3?(^DU4tgc{20o>?QX}JD5^@(p1h9o>EvzDDo{w?kg@b-TYoT!yoq!ksNP#4o@+<5Pj7$7a#uTH&{9L3~d-w&~5nXCpXE?wEof+DQ&4t zN%`J9_DoZ1!Anu4b#wQpwsmm-+`pLf(Qox(O_@I7;onzX^85b;do3uh`#H2G>f(L{ zvqqBpK6g$;h=nc_o*Ysy(3ZDfPTMB{<3s@@%2NQlMcmiIg@$EOz)`9 zFb8W-KA`8G0^f^N&99MR!8`yX=d$r&4A4{5PEtC_k{ zd!wamq%VeS<@+hvQo-0d_gTK}3Jr4@UyHC4m97SVuy8>(wg&XeWlRgo$e`?=p>l*9 z?bdqdQSxlw;p~qB?z?W_w`_IwlML!PTh}pIaw@rhojU|P^3`)!nP3sJ-vjx5;@Qld zqj=Xo?HK!2pCtbi3g>;ztsR49`zSsI9TLRn5U=z>x@Dvjy})ZmE_7F&NuJUdv~JU% zdr}Iah2h!z$T;%MT!Hx-vKz?y6jIMULy!IYFN$Jvf}uy`kuTCkH>a{o3@G2;L?IH7ltOPvY7L-Gp?Ey~Um&&(I=NfxIwQ(9g0NJrmwd$Td*&*1^&@q*H z#CCJ8i9H|Wnd;K*A)V65-|#(?ZotkMWg8i#9NF`%&;K%`KKF%BpobJLnSQ6gbk$Fd zu1ZDTv@WZDqIMr=fBlbLKTGlnI(;a;E1JXq2x~+)A$#rHetb~v*PWlionE?kMeF^= zJ#!8glRg94maQnBfNP&iu#d&FyLgaE^#{)XSh^I`o`LvBttvjO#+Ng8L^5J;tBOflrzW;fg zZygaYJT>o0$rpso6&n?nTw?|uG@Q7zi;}3?*EuhdnErF*HeGGu;o+CdCm8` zIa80_`s=L`Z`u&TPqE(RF*ZlJGp6|?-c#5S9` zee-o0Yg*b539oeBpT2YOle5vuMS@G$y>b@D-^c?%j3 zf(LtuEAR%G`=m=Q#gC*h7I+uX?x*1Xg8Od^n@jT_ibD@X&!yfSY9epY{%J|fG`^M+c4|7>R&Yj-0Ref$L@5QYBY3>;4 zo{wUesXe*_2786E!yBOAGg7@9jO=n({4UC5+-1uMehJ4P(X|bHrDxl>{m96J_;wBQ z@T|KJMs~$^@IE!#qaFC&;g4ila-+MDis5&R_Q99Ui{bH0wC~maoHv5DnvXn-eJMGp z2S4%q4iBFR^8G5m3meFbeO2H^j2}gJn3dvJv~rQ!_!2n0I+}H3(&Cf(MWjdPUAmIH z_HF-a^im3KjzBNU*~la&|Lj8Tv#4J=jkXC7>3Z?k1J@#WU58!NTB7;l;Ko9D)Z-WS zz7F-ZT*~?$X)@?TxyXU;D%1MC-k+k6ze?Fb+g60yrhBOPm@oJ1dK))B$zop~DdTcH-l7b#9M2|_fJk}0-6DE3h*Hzu|5&zzDdUZSXWl*2S zJ2TGF#>3zuUKM`;F2R@+rC!-4+s&9`#|+Ax5i6Zm7alv>w;c(Mzrw+53}np%2P;pJ z#-GA-@zUs@*;fBV=!=|(8#gEY*igpamfRO3!xG$KUtFiXn(TMv4vVwg6@^o$rKLEs zyC?FW0RNyJlDQ`OO(FW7IsHcMD=9%1|rjXur0hlNnXiE5je!ah4F@Y z)v=+)J0inU`ljxT)D@rQysG&g0^d|J_J#1;PrZfY&G9^Q{~>KXDjQI`TJ}w+u1?wU z1og$Zr&W3}fv+;q*SYU=cUcCurrM=`HyqnYeOa=uyAsu3CG+&p73!~nts?p;Ja|_< zYQJ=BE_!Sx|4A-w@Bg}{X`;jTx`%&ak_2gU<+_gWjk#9fj zl^rPA(7f{wY{4e9(q>pqWN$K9AM#a-;Xp4fN24mLo0*>`V~=T745ozG`Ka+d?H(k+tpOzM^GkW2eM ztR43#I!Sn6$c~e)L(dMLI@0w*v+?=0P*jOLPlpww-&H(X&U-kWb@{ zXjc#Kt-aUP&sB6Mwkmhn!vCX@3(oDGoh?5XVmv$1$5mpbe+Aqo?=zW;=J39h_Z;3u;|HK|4t_xWX8*z78T_6>I^~;7 zx~NNc4{=eK?o0e0!td#K**a&q`htAZLiuhF<=f`+)or)=K0!Lqm0v@gN165Y-r>#s z)*6@E`5yfL0c~qWey$|F+B!Y8v#mqB)jcRwcR{G`b*}D{x7)mZLv^oqbvJ*`JG|1> zt$N>seyTST)C*li<97Cs2Y4@Vc}2gu{8qkeNvC{ok**nh6_-t1GjaMogWoUldkcII zM&Tk!UPEwa>vnE8F7YLIa1bq2lya)Uo0uBK`2l89|JW^}h_phW2 z+IJ~&;!9JFceoF~!+Z(obJW$%Jm1jgsZjn7?t@o;wVQk7yU^z!NEgtDb?hX4{?2bh zAKKOg9ru$?Z4K&sv|D`-hU$AYRNo)F)%Vpsqa^iyiQ^xPGd|XbtuzU>cR^Mfz`aT`1@1}0`^$OLO7pjjv23_Q% zopTYYFOZKGoy&22pA*VA+vN-7<8P!3$4A^G@#aX@ku)(2O>Z;d?< zxIXiu!*MYBK)UDz??!jJKKdN*0S^aKf6zxi#&7Z9Ca1%GLb{-BeTWke3gJ~czg55Z z5cHcLxH<#6d@q!@t-X_fE&Nu#fG!6~7trN@hzsb#J$8mJwM(E2ZFrn?YGY8}!`+84RNpJz>bo~o-%FwTUU2ojv&hzWm#Z(p^Nvuyx1kcr3>tzFV}R{A33zCyZ-njVPXAoE$@N;sHd%g{@^>9Mt{`NUX7nwqzhzk z3vrUYdhDzOzb~XeGF;t({?IwOAV1$>OUiCBzr*^YKj{M5T}PZ`S9<1C{FeNE-}RMC zNEg;0J-XEw3DtLfsJ?~W>U)p#96^0Kq55XI`mUibYJB-`(uMVh&RGWeriAiwkDb+X zZhZM8>6)BejUi6`q@MPS;0(MA1Ev|*2%(I-1eueJt!QCq*zt?oNRb^kI{_i6l&^qv4UYc@ zh-*;$;Qu%HEq!tr{ZQ|Cer)8$^~3%VR-Zf-l93OxKP4mK@#fCN`EqhC_=Wq?)x3x0 zWFqf@oYaAHASbo_4)@zpqzmMvg1A6VHuBqy8&_JI4|DYeaxyrSuP~JFW|waZ-{w;P z`v~a*Ik}NI$w~ci@9-*qUr0_Or0*^#ZS3_8_u;cT3*vZ_8>;&{SNDy42S|1Qm2^Sf zdZ%EhRaJhoGKu(77TXIqZ-vc@M zAigVInD1%u-OP(!ANrxg5r2nipZd^KgoSDIUETxQuvaR{&-eIk^zdai@3%-7(B=$r z;!9IM@9=-~JIt4$A3f~q4#sCbDw53K?DC5~ukbrepL`d{k=GpPxmJWcph~5 zrjky0eu{MUj^9rZR}bCP?#KBZ{hJT`mW z8oKfQ3xxk0d$Yuc=PwX{JMk~D&#$RlzBP+rjyD+84zyhHnX6`#1gwJQQA(>FrJhXT=v%2Wy94EAmoU z>a=^hnwX1R>DI;T-JY2Gw#W|rh0~hY6LTN!5Af2SfW5UHgH^8RIuse~9{*M1+qXS! z);UtBH}Db5S9fDnXJQ7u(TqR#y@JH^*@$jE%4X3$^Z_UC-(Zb`qj90RT`xo{pyb69nxntOKqrLgDUd&-h z+aDQ;nzX4KZ=g=CQESbN^{=N^p&JTG!=$CNJUj|bqNc%}7*Kl3}e2CH?!VcI+MtXUV#5dN+HQ@W=xSesH`590OR zmaXtx?bbfWRnS@AM%20~Yb}lH6Vlz-*t_0KjqH|x-mD?s71?ck^sJSt@4P!VlH5Nd zoN{jV_kIMrtF7{f&#Tm0bHNeh=xE^_;HAJjfqeg|@FxXFwCACIf_Jp!(%Z+@u#f*$ z_G)OIT+aS0VjDXWJ#K&FongO=B1I zDZBMRqi=p@bK%-xROR%1yJuLW9cZoLd zLbpG%UYP@}L_2-I30zuq7it7rH6d4$?H7x5$07GVA=53;^Sz(Cbq4Ov-^ShflApV{ zA4%)Q&!SJnE79rQ{(RGqb9H;j|5~4qj(rFHEt=$T*IjMOpkG?K7~J^Bb-E+T`}U)mO$hp2Zs&B)cqnvl= za(~ucDdm0umM`W$61@#dJf)s+oh>HmA0stzD1*Z4~IA2dTGke z&G6(4gLB9F(06<4oUscAN5*>4pUAN9?`f==0@FLde0k}vyxr00xYb^dH5{M?fs*|`^a(ziG1d-S0J zjh6~<+VcgzlXPd#Ib%f|(dD*FJ;S@lkkMA!-o_*Ssx}3(d@ZspT`OLxZt?4X*7|!H zf5|hv@hI@0MKdGUcKzSlBkW^Ae|W5O9$uDPHlXi-o&Edv->G!EdU-*@Rv+AQs z-wxwW_jRfL?DZ~0?+5tw2Os6np#KXOqX+xjeVA)1I`^|i*asNcN(0;;9__XA7;C=A zePDZ1G9EshGGON&jJGj#&c~3ii|L76qbIT_oOvWM&@2e-@HaY(|#z)p=^o2bJoJ>(_vwJtR0ZF|L9j* zr|pler?Gj#9_C%rx6~D^r9Qu@>z-*#M~k1u$L@_5J{I3}>W?%IzK1riWGvNQP-ylp zc%Qmca?RO4^!b&Hn`Zw2@+mto2F;?-*JJ+|cI4*M=sw|M^JPX=&E{VQI76_?2f}(!rHg}@SyggWpG9? zkguzeC+M-ei2Yllm)c(o&5gY@(b`KHO+9vhm%eW67}$9FHT*vh!wdF(so!pA@7cxr z%y!1z<}2-8=@Do>oBLz>a$ZdNazDV{NoY^mM07j!i0a#}eLeP+4ICHHynWlxjDA%4 zJs5wrhAy0f{RO>cYjvx8DfErd_jM=27VMj8@LKmJ|D5(^LKpF5PD5`a2iS@$k3xI; z`&8o2n7F9U^V)l4R}=qT;v3lW5zr#zee{0@`d|1Q15eeZa^`S8_#M96sJf1U&!Z}@ z-B*1Um%`5a?kCOtif^;=CVzbf<2rkt$@>)WUd5l|48G${K;z~M*gb|;v~3{ygxAB4 zet~ZKp~efs?pT_Tk-LU_9WQaX9_2|H8H0P+o8q(k!x@jh91SJ#mkptO| zlF!Kde4{o-Tr*>_(rxE^wHK5BW5gLbW?#5y5Jir$p^;;^4>f7K+r8n!S@z!y_OSdA zyoC1*?v~92=O=oBd!L^S9f2L@y`C~`3;WWF*q3JRU%;MjEw=kQU!tA{^8Ex@z`o9A zJLf#gc`>uEQ#P^gvYgA9p)0Xml~a%-e}IO**SjlY?jVQC_I@z>dgqhqXTyTY-3hdGpI(Eb#C?_zB@hR$zh zoHFUq@fS;Xin^P7Ej;KIA~(8g2Hf5Q=UmN&9IbNSXZ*<3_#s+>&;IurbN4gm8eakP zS1;A-oTgMWc6Qn;n#Uqz$$2dJ2YoObyYeOVLE0|51>x5a{xjD%gRsej>D+|y8Yy{o z@frO74Zr^pJrelIg8n4Etv+S?)0LtA^iz*>=>r?J)^Fp{1-mj1LcLVu#4oYB1m z^~i;I^9uXNjl3hzfnKG5bm~=tQ>YstU&2@c2dZC2=@H+iL=YIm*H<4n;qxXpWjMdcu@U9``5wU+~JlXy9&A8 zx!lUD${oW0tg znR9d%cZwDg-pu_=9=sL*-@>QTfL|-O@B7@{ImF)G`HZ`})A%f2z*mM|mA!HHHGF3H zXWB-j?3TarV)Q>X(DGLP4cS%r#ombW?V{KmjW>fDf4D|88{8Pfr#7sB-?QoG$diX` zU&-90%#4FW8p~NL)BW*wo-w4c@fmbJPg=Xv$&4)Lp z{}|aqhZGVY8%)1L)}?!LKYDT5iJ|LN)+y@IeDn$Iq=Ji#S@qaP%{MrG_FkvGFdUvq zwugq=pTRjw$&~yeZ-Kj^1v*pXvd&#gMjOCgb8$uQ^2>8RU(Uw}E)*dckqGiM}G;WPCa|b^3Igu{fm7{yWsgzS-pYu36~&SE(nEsoyakOQs}G&B_BmgR!A;W5?dF5T^dL2iRTL8d^C1 zWWME3`ypx6SJX$YWbPK|U-i>>(V+w3wff@a&`*6G|8*|q zXTZbR$b-&5{TqFwf%~_0j~n;NaJRPCa)f^I1o^BDFr+bG`Jq!i{cSsIR~gVJ_cCvq z_}n@-V(&L%pLjF!r~Z)fKK&ts{?KXP;AC~^q73*#`YQ5I1}5aH+n?lM&Q>RWX}89d(Vz?=VM^|Y=57zVmtC5j49ilUT(U; zToapiYIC232W1l|eb}yuA}5ObmEy2D0{IzAxW?mv&a$82;{|>A$Zyg%%6%VvbHR6p z^X*4dt!>n7Y$NV^!zXq@A3yvEaC(O^N&P|g&goWv?|(b|{={#|zkJgvj2X-67sy5n z_e^l6E#-~qU%68g*~w^-4jdTt4JTGueNVHbg&2KNi`;YlW|DD?U>;;{+u%Y z>kjU9q|9$%7prd&-*UPSXDt|;(-yNQnLAWpO-;kb7}j{TY>Z0A1Ijp6;I&+Z%`%EU zFtTrM*?+#^9ZjJO-MhEHZ`#gN#f*2HSv$3Xd0(a1qH}W1FJK=bv+8q0@OcdR=_LFU zOAe~Pz5P>s@bo{8NA>8|Q>5MA$D4|-!k5AMdh9dxyY)-mJ=B+Bi$FWcm%VF!aO36Z z6V)%d`$Z}Bfh%-6dW>=PV!S;@9nD6U*>5d-`;ecNwhqQ)_$gi-d&{m{ zNp>~XG{2Uzdmy}&40y!-TAE$w@r^y@ zk$lV{z2xI6(TF-vBM1FMa(Ik6?!{z#KXPyBI!JnBrDVI*(KQ#_OzTXNjZ^RAi#mpU zwWTgQ_{az=8*K?>1Nw+2+c~4D{c`UC&^9c8w-f$P3hySDb;8-` zpH3e6c~17}dsg-bHNMGxPXWJT@6+dF>C!#J8pp#EgGaae)=}T@*dNs0UV3+~bljlE zU*BMLOym{ryoYxCb^e(<>4c;10P~ttcNf9S?W`@&@dn<&Js>;ha3-}CdW)Z@@Q;S| z*P=nJ=@+N$R{zqySCWTZ`1ly)rL$(BMn$0~hP5Mo{th`>%eirYtmh~Nb z+wOwT&_#2WHs)!k347Cei47x95D%eqT*{Vz@t)b{k(SzCh)l;rell>&Gg?u5%7+d|Y*5YxhUj za87)14)wfB+lQLF5;fNw+W3eoZ$JKxk?1$|w^OpqY1`fER;f;Sqr@f9I%ut!gM?HG59(|~OE=@ZLvSWJEcJW2)VzRvyhtKAO<`hb!`G)*dIl$(Lp78}}o)Df> zyqg-HQ*g)WYkV8{O0VTf?D)<(#h}J~eD=@fYwl%q$gefG_!%}}3T11K(LBQXj^FO} zIIgA--V1cz+8DlG_5WaF&O8!>l$ zzYy=(>wk358-?_VJ?sh6ezqrXO`KJ~;hW#i*F0!kp*Q=bV9h;jzXyB9RKNWE?c1I) zJPz99h5jO5ig{a|h;4|^>>cH=ygFVUk0s*KwUt#hWtA0)o1<0Lm7k3l*JQ0PE013+ zO-;f?C!%Y!HgAe<-elfx?%g|CTv;Mo6|1g^R<4h(s2QDzE-9og%8D&<3R|;uNmdca zmPBi+V-<<=n87$IL=(}nifBz~Jet3%_xj4}O|cpR$f}^x#g&_?NK-O8x?*Y6#hMz6 zwnkUvyP_(qqm@N!cw$2D-peg9ERC{CQPA2L?NLxmAd{gC? zcv(rjqNc1kRvs>9@p@BdKz#8bUPrMB6P$=`io?Tj22tO&R4Kg35`o~|QYv{%Ajxo!MpoF4lZDhKkdSb_ol+E!x@gHm zvg0I$iJrP-rR|(S7mn4|RO)v!alE>^vU+B8)MT4tbyW#n0t`lvnml^KgbBS}#+72J z!UMvpqetVLYelolFo$S&D0BCE=&TZ>Vu)Qsx1tj2p3?g~d|bLjY)fYxV!M6FS%DQtD>??imYz38cNqG;sj=O7ptIr%UDl*DymGxlldKE zg7+jH0)~y6Y)Ct8!sICvrc=Iamr)dCnt%#bULY=N&s(uH_lEhviJLdYHd0HnJjXvr zFLD*r*8>G?I=1D8V`Toy+-0klX zinUI)cb04mzA1O*vc=05&Wx&1{*v#bCeIu-1s-4BEgj*8@iuuly-7HE`iiAAG%r@2 zh*vK{qRZpeN;-Lp2~UPho;-g`tUUSZMaN`hWQ_HIw&3Vm3r9_+wj>swS5Z}7TOBJ; z%$oeksQo!>(kDrp44;w=A5#&{m=&EoR?!!Uzb=UGjK4aRf7Ikjvt~_>UUgNpf}fKz zW=)gt&_Yf7k=Q2MDMJeB^o+oBb~ z$zxs4F}zI)LZ>KHs6vzLqEk$ZqS?_YlN3IM@T(1x!gLDZqoP9PUPrkiN%pL%L9wEM z{Y%mzl;039)F#pLqV4LIPuVU+f68!pj4{P$Po88NtFnnO4GVZ9%r1u42bZtnX{xaJ z5LI2Uo~~^V==cA!T(QUyz@i9~Cnd>d8QqOlNj$@HB;s|&HN+>!#jBHKo&`~y^d#Be zN#}66on>~z^STgsvu917G&aD}HiPI+?h`*FDXz&1lkJ$+r9Y@^ntp1lwxS;pK6}=* zpdiS04N0%23rsa-Cd<3#gUU1g%eFtj_Ua_fO=DcahH$1Sv!hd~XqqZY))SWZFrDCq z2&`^xeqPi0GivA%mTu2mrE#@9W^}FOJYysXWp<**tT2mhfnBOy&6$PZiG=o;n^C{yCnzdA`iElPAIx{<|Xl;(mV< zsC4NLe!;RaTqHsSF@gphz$kg8c zxIXSZEv1Ja5gR;3dL1Jpz5S7W#J`M)pP|I!mHvWw#p#dt0V!$i<9oY+%OdS#`-Fn* z`{iDc;xh0N@5z^zLZOjJN2Cwu~R99A2Y2r~*TUE|%l39bB?wR@eXq(&{-Fu7-)W8h4v*F6>I40}*%CX65 zj5m6wq!Sq($8>sTRB&!(g=TiV*zi?mwr5{GMQ_@K_3PfvJcucuwHb{k0GETEz1GC4 zH()%M$55e~g==2PyxL67H{lX6b6=)-OlK=f646_#D=RjPk5*MC5@ki@atbip>{bM+ zHC;+jEYthycyZ;1in3edB@%!0w<5n_33m7D#jElQ7B9=6zj9gbl7hL*`L!k=xOD0K zWfEy`Lan3}_*wfyjjoQBlogxF-ut)`%-puD#7-jULtXN1ir2)VOc-N^-ts_>-dsChtgJHg>XNdGjM>pKqb84a(Ou&0l)}cW zuGm;nc}s<15EE<6e_gely*drT^LLwvV-D{E`IR_x;4JhroD z7jEXQDo&0SVN_FH8y8&9oI^iK;_G9z$Jjim>5UIzz!&0+dI3 zBc`DU8;;|sdY(9x{IlWn>)?NvOg20}>B|ZtyM~*2zbVUvk5}dV^e8=R6ibvcK&;MRkfpKBO{0-h>#xIg z*u`aGkmbJ>OS2YOrn?FBrr3tE;;3f%V7jS{K3O3hJxWTQKgUkpD(C z(wAtDb@v%BB(vmB$H!Pt`4?}%cC)#=IM$rjP@5>qwmnxE z-quaOG78i5Ct4w*nCxDlgzcN>+4P;ERi;R6zKnUx=jMMZZ+>(|-lFSzXY;?wlGNg} zGVOcU0$eEhCz^a-wk_~FJGyv=ZrL`EOcBz#7neYE&9=z9c=r;HM@hIwnKZVXP#|fu zCw8rP#O={di4FLK4c6#e_j0?84gPMI@xfnJC7vefKc{G`WVG3 z-D*Y6X|k$HSu^jGt|q<-hbE)6&5*ZTPJDHysOxByTpg49zB*Q1gNYtRj`8N9mo;>p zpT4XD2M+^KJW(7&WsF|O$Ppc5b4(c<$ocvC@Xq4Guf+sl#$x=V$x6U~os*Mwj!^4t zv%GaO^c5d-u*g9fcKofg;^3{5A*lB{3o~7K)WJ;0NBCh9EM2^Ge%6iFm#cu)6*zub zDWF?{x@AQ$3-!X>3x+kPr3yYzM>|}jTurfR*f1RXY`GUV)mAg+EqDX z@I>Qz{-ogr7KY5`Fkay-0ftG`v~W^^;k~*dQCn42S*?Z`J!lLCYbRheWtrh*Jz_S$ zf@rH#RnPRIGDGnY9g_8F1PW(yMJgNZys@l$Ls>=EA|t(>a%@@|lIwzEJ0O}|@ToJN z)qr?)yaK&S+}iO`nVsJH+FNhc+-@_AR%TpKAb<3e_2sb*F1MXGDtyF*C<|D{R-;FQ zcf?eY{EfIx{jke>m+%5-K$<}Qqn^t0k^+;*#f8!r*U%l!d){(~pXpY}w%qsf%epQ! z6Jkw4Nm@M+s^92^vXTqb+oevMud}`pw|8xb-i1gqyjy`TJXrvRCx0tnK$LK$QNlah zb+HyHH*GsO+Mx=r%M`ZG2WHL75Y+Y9bFEkKp>x7GqP$JI~Su2<6(oaiRTIK&;-JFU(x{kA5yA(}mE@^!}04fU^( z%2hYEc5o=3?kWe(=&NX`i|tabTU(GS*D7RnEprESw_Q0BS%jdW4A7}XuS+XUyd{&; z>JMTW*wsvP>q$g0;bS3pgBSE>*W&{FIca*8#F-(m;8nWvbsw)FMpNI&}o~wcg%ZDz+7?A-`aA)yJxT>&!*iI$a^@}rKCw)1_-J%k*FPJg6AyEWV%iPbL|fWP+&4$xC4+fdt#S>B)yX9&l5z8 zN%q9^Bf8)nL_147T7$uXz$CW|1a^`Q55zSP6VIg&x=}Jbx&AF;@|S!U!r9$>VQ0G) zt@Ox+?Xl{qG)3Y1Q=I-f+Xdp#*hKPQHhTa}2eZv5T{6JXOzE;GI)Rd0w`utVzr)sVqhu@am!1Ohc5eGL|Sy%yemrXV#UM ztzREbDL?avNvDzeBc@d7R&FN_YnCitu;79-Dw{AMyd_?=s|2!;Sth11(nM!%Vfs#q~aA-L#(EtVqLvGN*Qo|VD( zORoam93R(hBIrt{p1Q1(+D)6bCgU$upUFS5YlTiv8N`Kwm8- za9jqPbL>VjXCE(jc1l6s@&funwKJQLSA?F5B?77+TY=e@S#r~>^S5Lr9Vu2|I^W}N zUhUe8-bivX3(lK8JHfO`<&jPuE~H;9zk>-^z`DRtt8jjbPWGu3*3FO`!9X1JTYpLF z*;njHHm>*c1Er=Jhl%-R!Z2_Gf#J(prmwm-9bqdS9J9n06G&EsGMFW}d zjbWrN7&{xplhZQf<$Sx#lyo8EOA*yo)XOAN-fpz*?_B7ZkQ2}*V=9!UWT z!mrCNsw~->M2m6Pyk}1oegtQm@#?&8Xnn%A2o6QU1EU+V=t$kyhpwqTd;Ux^b@n9CYzVV+(Lg@@mO^^m1h;jv9RJ^R%|`aJ>KODR6HGPec1-H|1MTs z8gysE?OWF78l~{E5S*IiY>IpdykyF+ZIYpgtU4Q!75E#41bfTqzp;EDkTeP(*mPjlk-U^M~nk{{cOB5ZA`_beqVnsvi)TNc|r7Wr~D|ho? ztt!q3i-HA>bLU&U!s{ksf5j>k z_}1vM<@r%rcQGW_`hRVB?y`9o3}3#oTX5K4d46G_5R(7%3+))mB*7P3KZVklhj6C$#b`>fd!1>CU9@Ayq)pv8yjV`K8H4rZWPZ2s zoqYcQ+AXy!j8TnMa^E6L*rRUJ4BZ$)%-pols2u}p zP!Y>apnTCNhj7sTL@^o7o(t5{rZT3yB`Ve&*l@PtdF?MQuQUr>rYcunux=ZU>fT-y zH1?A9y5%N+e~8MdBb;7S9R!HwG@Ayz^w?X1!j z+eKgMV}cl3t~<2r9iBuQligHUq(3!`&FK@Ef&E zP^4WblXcpovlD*OoHk+g-JS4p4K#at7BHL&8UC#O1^=8m zm*%nE-;KsW`xLZb`ARH7+unJLR~D?ApGUre1)MWryq~z90~*qH&dxRI7R=AhU%k?x z;&SA!%*|iU@bB@yM0=1ZB^f3>3LuQiv-Oz&73|)FRs`=W^3}f1c$q>IimPf_%wy+4 zxA00a#g=Q*nR#UBUHdKmj||xB0Asm zZkk8?maon);Fq_gOLx<Wq z#KegclReAvUHQ6%YedmWH1;H;yr(9INH^}y4U{nb-VA;3kCrD((+*Z2CN=XO?8|DF z`Hj`j<`B_Fe{3Y)#AcWht8DD7LAH-ujF7d?k(c#d(~+QCK4XP-$%p-FGMZY>ORrgo zk~=;pcgihZHh0PDdGqHP)~Z9zEvqip{%5)&2i+t;;$g6>js2ze0M0~QPSw>Y&Vp|J zB#7@89PA&$XlJ*M?OK%jKKOKYHPfO6$vn{*XLe#GI*enE;#q@TN-9Y_a#%Dc1Zt1{ zP_^aSRaApr#P%YiBpr7+2V)lU%v3UHDf1DtK`l!Y1C{6A4Kn;^>+2-C?LXb-lLc#% z0o{rV;bTZ*@ll~}qhWz7aCP4Nl?8Ki=Pr^G;m?IjO_+e-JxCGc%V(aV1S{!3<`gIU zK(&`9PWy{%jbez7LHlxWkHb5YCItgZ$S$=8q+RQ^JFJWeq_tBWU9uf7I@7K^n-x{J zTQjCQ+(u1)R%uGsy}w>Gy{RJ@HYa$C-R_t2%E~Hr>EwjNP~$@FGbzWIMoo$un_b)u zMoeja$+;zOo~D{}Rxe&+J7?E$W`H;Fp$zH2dGqtkXrpy{a)bhz+?vtQmd6_dfl#yW z+Jw4vxdy1t_qlm_F03=%(&h8Iq+7(B4RPfzSdzPNRl%x7xp^9-`0Em$38y)%iOSFmV)?!5U1yJR@XnBuymTeW=k%3%JAf-voKiXu7kC)(~Db+(uIa9lXw>Sc@9bcsy1%lchZt|c}81D*eK{+J+4n1{nndEv+l#k+=F2+wfT zdF9@iyK-^voF#!B5{?f?+3=O!=(NQ0uQS-;y++wx-K)-?&WktNBE3Qj`587W3Zyfn!P}%KeK_(>Z`8lrbNeJ z_*U1PpK|0#J8vwocEK1+W&D_K!`wQzfo`4KIJa&cn_)<_&iO;vjkGyNj@%ZwisU-_ zTu_el3*Ew*Albrp>@nv=ZHkFSWfc>#$g_-hO#3gi#3lcgbx8!}6x=ZXQ>#$)^UXde zW*=rIcKxd5%c9yk6C6IZ`z*>jH#o50%@h$UapPyL^#dDsbTmbd4g@C}3szmsjvowuS3o!WUjs5jBeQk4r613#!7G~O z;5xg5XfekaX4+%Zn$?==4I3mt#=3wxi}pa~&!YlSDrMr_j! zm?tb8cb2du>I!}>|0F6^yca zMCbL*K}Sco33i*4iZFjhGSJ7#cRNYVty!W`M*HWGoY2hG{miXdYVB~d4n%3FWj5Wi zKE>#yo>^OAHCo)2Z6{vH)~4DT`a?pppuaM=kC}MugtNi)n_PUtTjF!=GL4tm$~_~S zjNBUy9PY=;;`9J=rTb2(CsP_D^R`T@%bS*i=JTafGnS2>pRJQlnL_!GJAI%GGfEM*CfFU|KwHlRY>8#al>cF>|-fIY@amJCx zv!G^o1eY%QofZov%TBzz&beg3tsHrAr2TEMvzjSXsuPF!43FSj}fF3qn{xGBU9qT$JDq z?2Xie+c^U9vndVaK5VXymp^+>-m^~XK>vC*NQcWIBsJ-*oE zp2%~N%To5IUCa6OYrWs4UYmYv+Mj#Q>G?S4zaQ^8C?zN5)|97HUP}3M%EwZVroNFT zEY)AFy`CtpE|XluYPE__`)iBn8fKU|Ga|aAR@WJ^)>X-_kkMMP#reZKR(sSec1N{J zF19BsS>{5<(Y09f`Y*m!#!BxI?ye%Y3?y?KrN#MQEh|Oyb&(glr&wAuRtc*xvc;GN z*!?aE`6MaBxS&{|VJODzF{egl#tV-X?34?VQ@Dmbkhw(`GFM6Izp@&x6SD(uKGHu1 zJBQ^Zc_+8T=V-~Eb9~!+f6h(-dU$oy2Xg#;6WdQWvdQ$aKHuQ7%1SOUh_fZNn#1ZP z{3_s}dYlwB989m~SHc?*S$X>ce${amKsliaQYue@SHxQxVI_o3;4*}2bI4u9PT^q_ zi+S{J|AJ7#;ICgYZE6q%oNj)r1pXpbuT5<|5E+*G$*23HugG0r)Z%9k_Pl+-@t5P% zt#0w#*l$t_JdrJ((Bki4Ut%3_II9Nrz^SZh>;Nvgkvk56hd5Ai1~>_kn4W0yzrB-> zMFHFPa|b|8i~k~Z)d5cfj{w_%UTupX9l%`;z#8B#U>fOY#R7rJ^+wD^6wjG^gUTs{mO)7auS5Wepppbv1-w_E)5VbBdY z30VJ+v4Z*IWF1@ME0(8+Zf9 z{2lqj15)=+i~lt6%)5jG7oBVIwQ*yg51zofv{rwQ-Y;qOPXN>Vw)*Mg;BUWHe>yN{ zP^(`DJU*n=e;Rn;c1^GvDIIKe5Zf1)sHe8>X*~%-wiCC-|FvzFIB*= z1M7fCfc3!VfDOQAU=#3d;0fR^_|mX|^uRN~Bfv2WNe|owd>wuq2R;qmn-{hEZNMRm zTm6@zd*Su1eqZQ*U}>v=8?bIU`GNiNTK&_&eZb*S-dDEz>wsr~`+z4`wfZjtb8c+) zCqehRb*+B(2=GFYq*QD*QO~ zS;~VCXE%WdbV{#;p5xh5xS4#w!UT9hC$9#)Sg$+;Tmn1`ECe38i+X{jpKtX~0>^v- zx=#ZCFSh#QfpuSM_0It7zf69dSVw?4z?_}r11R;X{7@;1J~S-99#{39)MoJ1ACDZV00hpfMJONze%$Jh z0X96`>emCuJOW#yP)X2J`?v3tW_Y&d>QI z_0Kuy*9p!&=YLyp-Z}qu;0fRW>OVgJoPQdaw}A3yAy3zz^B)181r9+^pIC9uUk9AD zig4hWPfiY!PJL_sKs@C(0sB#o_s4VoNZ=vh+rZPnbjq!IjdlZ1zYZPdLjOO} z4&bE!Jm=pAoPPS8|1{A1^Ev+v@W5Z7AN8Mk9zTJi>WuQ&EEsuk>2K~T~E1}wD~!} z(|z0gXMyRLwfUttP|jH1fmP$${6&nL=_}j(9h9>Vco=wKRhvJa^!@VN{JVjRfct=l zfaA%x=*BkxZD2ESP|ZUx^}$m?y?1Dpgr35hX0ds(7-aw9kC;kQ>fs_6R`B@A8Z$T$uRco7n zhaK>A*F>9l&y6dRn{x z2yhYbkivn_0-J!Rf&F^4`~3>Y2OJ5k>(%ZT0{f*?KX3=|IPf^|P2jFe+WjGgl-HMX zf%PA0_n!lv2A&0a9|eyX^aYj!n}B%d8ufp-z%AEQ2?*B|_V`v$c8 z&A=T4!K;|?LG6AvunJfLY#!3?FDilVm$&nmI99eAECVZtJ?iE@->epo^+GOw)+Qw(Q)u$J@Nuf+dzN#M7uu* zIAlEZ1)i7)Uw~^S(;i^@)$RU(Qt+JyzQB{$wEK0y!cVsQF9X-iZuhIo$e#-xfX9Jv z0?%9zojwa*ODG3;XhXYy&qm(MsE7Jb0>2Kd|7^SeB5=&ccK@)-D~EpMuLn*AdKIK2 z{~6$E^6#o__lK9$t_1Z0&j9Zhtf78j)s}YuZQu@I`X<8bkUwD6)^>jf@DT9Zz>~lx z;PhLmAJ}gjdhlBJd2b4LIbhl)stxuaO^E`gpsaS55nW-tO-MHUOUk*8LLvXm366MbQno zgm(3Nj`G3h0Pr4QzhA*`@JQ!lo6i9!0h@u*=iB|hlvDKrcmekTcO_`oi_i&p0{A+x z{x{%X1D#(&erl;7_y{oPx9$E3U^8&)7WjP<`2;oq%YokSNC!*@9@6`NKqp}7Dfo8_ z^#P{>n}F+pIj>T`-hs~oPrOEds-xW3c?a(M3-ke=1eR_E{+lZ)j{xg#gWo9~{`k+q-_#Dj6j+tk;qL+->Dl4G4D2_c!#{8bmQ&srX~#H#+0*5METGV=Ag4u1)-@D9=g4{fJCJ1PIJ4!;C= z;7ha%zMck_19QII;ZKLZlXg-+@C0xlumzXCqMdSE^<`qd6U z4Zb$)>F`g(x0Ap&;Mw~rAAU`LfOd;-z%lTx-(K1eT(ggM0?z;BlZX;XCpu>3}(p!C&B{$204!`d`;02rxEd3t!0Mids&%M+G%myxcs>81W zo(7%-9{)b__7%!~n)-mv&mdP{rM>@3dw@HBPPx0_`*G+1Jo9hJH?aN}&;dBTg)^d6;r;1S@E z2FiULIs&IZNx8u2H+}ysu;Dwaz3=*d^a;r+^a1Lxp9Nmz z)|Fmb-4&5xI1&^#gr~d^|B;JV*m#~Dgyp!fC{Vu-6SfHGo8#Xt@y3FroSmje|;$Z7}AuI{&mu$4pn~6qv?lQWS=uXya^bM70y zXV~2Phdyxm+`U8g4W9Slpoa#gyhtU#`;XtA48#V!$)8Vt51I@K@neqou`-F%oCi|x zPb=#UDf;BT^c(YLb;&OBzD(YE@QJq9M;`6>*hlBwcj=ywh>@0$xexTYzwc*F=GEr^ zy5GEH!c;fUic?8-TNhwi(4 z?tNG6`S||J~l#N7q$dcRpRo_QMiJfB^vl9z=iuVPtSXBu@H1 zr6*aog%L&u6H6c)8H5qGNCYTEX_Z86-ByjBq$lawqcm=+tZGd(t(&T?*F;Up)XnmA zIgO1zld7xCRMYXanvUC1z_sT0+vl8n@4J$0lg|7xYx%DA-o1C9z0c=9`|PvNKIf)x zvi2bE=^1zJ#6i}CIO@y6Th~b(gKJ_(nvWg|cQ^ICUfOjq@kV*?f#hp_js34i23|?+ z&-_fr+MoP1Jr>a6D!}gq;#&CHrDb3-@IC-u8`dV8-0jhw)srcmq`cR%*cHd^M%)CN zJhc4@tYeJe8SP-?x)y_>>@92!N($-4d{E~rSAp&v@-&7tLDy*=x##HJW!-o7+_9xA zkU(waCvOiAxy<1Iq=^3|nZ`h5;J{zHYA>Y_dA#(m0@bj>ibS3!A&l%flal|SIsP$e z4udAp_3_df1I^)&Npl`F3!vc`j&_pqLgbT--ZC~ty0czf3*i(yi}9KDSBO4;4E5)( ztIdGTC7iK{byWq}`fjXq;@Rc_wrw00rN_N)YnvGYq^{aQ(*T;oUK;SjHm82Kn)(d@ zZxl4|fkxWX8Kdl>pExtXnFr2pJR>jCha+89B4dZj3S~VHf6qd>w5+~@@$0p;@hb@W z8CSiK_p1S$K)OD5w$QrSj3fC+Z;$ofoV+R0x4!?zc-L)-TetMw(w$ca(chidb4yw3 z`hgp)=cZ@&4BU{qz6uGM%yfvO0AdX>wjh$8 zK|ZSbu*Qn9FRh)?x;>etkb6HIkJ_DWU64Z*LwYT0XG+HkK(1{<`QqO3UHYmOb9dG_ zlBoiICHha)o96QR07k)3G6OmJE($FQ77L6kfL{u6Ri~0erj6bF8q+X+@UZbqn?V=r*uAQ0;XYZYwlp)j}j9jHq->Yaqe9iq>7gyt= z8qYYCi2^PS^@X&0Kpt3sgP^O&`u_y@5#`tVPZ((qw#!0)G^~ z1oc1%zLSQ-^XScrv75b>NqJVQvgS@kvHU!$j6`_Ty*s+UUGFtO zIS2e8d==hPX-U0@S=&SodCVV{e#&qdhh%KO*!`J)4?Tfq*~7@6(HK^G+w zv5Cj7Ik1|Om^w&`&~zw!&*ao}Sb@v%7-B@B0oG#?ew|3WIdbJPc?|e7z+a+otRRyE zNaH+cN{(w=f}a5zar8~$-89Y zGK@dq-buTLvRTd+(jng`z45;ST-9yEMrLq6G6ejr@1p;zHgU#*DETKwX0o0JsgsK8 zU$db50RBl@2k6W=a>LQ037*=nSje(w*_U_I)aoX#$vD( zQTB-~eU|mj$uzb~>o0v4drT~S$dhl|oJQQK6Yx8ukg#PMHKqvFrzrI(>g;wO$qpxP z>b?0j)%hsrGQXU`7wA*j)(@sXs*z6Xg^by?zl| z_DBIe9a(Au&3X8PRjNL%>y#C>mDTxNGP^e?nz*hjH=J67dhCJ_mCS%SnPrOH&J5FX zY(`bbsWoiZiXWe0MxS9g!v_4RHM>j>+)1Gic}kGknQZS)B2Q)410`;VL_Mz7hC&e+6H`&~trnZt}WF-@5*52Xa!`*4Gh* z*N(zC_`#{HfB4zrjLP%rj3c)ly|uC{FL6uv%{@2uu20_BcSHa61G%Z|GVff@R&g9r zL=nf}KVkl+Y&t5A2z+ek20JxatXv-y@{^ zGbpAR|K+hzAH(={BJB@Qucwv#9?LkAcl4HU_xhe2BfU2yua9-zoVcm1|GK_ht8Qu% zLecJn>&7w|Moc31<@R4kQfuerRFp5)OVmHIKcBjGV4Vz45by%88(5dRwsLyLitxeO z$_X_dII)pIAf^E?kpV+YI>b1T*8qOO+c5u(F0rRq7+sP+^>gT#l%w>ex zjWXY1=A}t8SG|^FPxZ_+jT!E4zl-?_Bd0+=yn)=Pj4+LpNTdDw4;QP`(vW&%^ip|} z#vVzdWu-KpH)(|U&}64kj5LaF$M^-lNZ}ccE%N?Sb}-6B`d!a}hHYzTWRYqaTOZV2 zAN_!L!yhHqe zReyKJE1BE&cuG7c&0aZ^J3lS6rUI0GCA1FLW_m6k4cG<6F zy{`$lGr-k$-A<*Cap)JNjH(G)8l5RS!~r`_y(vE6|Gab}=+7M6qj=$6js5ZIXV$7v z7^C$=?8j#?Kamk%D_+Q35Wjh(pT7rwx~dMGzAa_=oydTh+=WHi=RQvw4L*W%tWx-^a)pb|$CK z2p}YJFg8TQQi=`y9MWw;o_7lbJj?rCu%iOTe&(usdp)oK-~!;x0Y|rw^c(M5Gux*U zc<~o)-qQM-ZKe@@Ey9^!BPz!&*g(tnIShLLH{r{UXI^ZdLBN_{!gxS{+qS}vrTQSJ zJ?aS)20k#)6QIkfhabP9bNaet8xTBJ!fxQ5^5VT@$11X;5O`U8(PoP09s5;;`XmK{ zX>3)N8sMj}Pr&dR^VA4fCt$O9Mp>lq$+{-%LzO_%N&qkTTkz3Wc*dS2Iig_juEoCx z{JfX#Ik2+xQHR;momFwR%#J+Noh5Um^T3}#{B2oC!0jq|AOfA?TK{hbdN2N6da&pGmFmufK8hoZnQ8NynaO2@Rm>bc=+duajTrT z&zhjG%fuDcB%=GRiSa51Hv_Lh;)KyjAUh|<51EkPfB$SmGn}KbQB!-0qF&jdO2;Qe z@Z(-XXuW>#v-&fj&4o>}AqEOi)T_$`p*0OY3|*5`GV70#JT^I1F+Nej&!|3IPGClaJNdx+YZv3cW7vnw1;W{yBXYVFw^_#FmK%@-kaux5ocX00PdM;{7z7xvsA8Mr5P zcckmV!~^l(`;zze74&}s!=ZPY!#GYOj{IN2K1yYaY0ErPeDsk>cTvwnm0cSW50~~n zn0%nGu>byn`%?E>XO~1F{)+1#fS&S6$1f4limp@y_yo$-PJpMrO(l0As2X@B*I=)l zT9+CW|DzaJBE+kxw&%KT7$Ujm(4XCx(`ZKd3YJHza^1@77H_LTztp{^VMP=$lpe9< zc@}wT&cS{&m1jrilF?b&@v9KVeXj-iA1wy5q->53WxZ=Ruocyh;p`Cow?e(v9%O!8 z+_%EL78c;gtJMO!wC8TbIf1k%sDg*q4O&MEj(#HC9q73$+;val?r86w$veV*`Te&? z25w8;+OniE(3HMG#L;-;M~k}^k1M^CdRDyKPaC9R(p=!40`BZ%PP;V#HVfF{_1IUa z@}O-GG&YFsLF_5Y@bOgg`raF?uS|@;p!iDbv&-03Xc-XO3WTv0Bbnmg%7>8PT^}uS zd^7wZ>yel&^kld#V0)Q!y|!Lu9%XLXi8u-ZAEo)zV$I+7vNDVR7(RU`fExtvrTCF} z9{_LgGI+FAN6OoDW{_&3bMeD^Ia;5tNz|BZAE1szE!1{Blmni|IE2b_TFv0-U-{WQZf3^h1ac z_I}vJ@k;_frvUxY#rNWWP}%Koa`8RWcn`R~d$Ffh;i`G3tWwH)Ww}}>=wD;K2tv=i zlkJ$b0qNa`y|S*flv@I)H>JI+xi(=>=GFYJ1T@D{9@Z5vma!7B3BWp(+%x>6BnTBh zarMBI0AAFrhgRUn@5dfoKVCxUXzWy_?3>#YH+9_{Vb6;8+}MrvavCkahUu4*l{3?5 z+sP@;z?ND)?cUk!!mo7OMu}m42d1L1i7uR7`r|VcqSG@GCLNMgnZB`gvQ3H}f!${i zdyZ8-IrcD*pE1Yu8sMJ#2KEQz+11{c^q*uMc7iVYTOVDKPY&CD4EV#q&$1*PM{bLc z6O8!my>{D)zmomNw&y}#^D*q7=ChkG9p4R+J!+&-`pPEI_O(8@O^Y6rmFp9bLy>%Vrxl1HbKez#H!RW39=An2Y6R zY=E2t{xRTpK8Zc@k4o7)S4EHPIQmqit17WQ+P$r3YrMBIxy3rrU*1;{OO*}8Eldce zXG;4j`pXC6sWPh_+U(49)lYtESL(XXRiq{{X$>Z$&AetcHDa3JGk;_;q6!mi?UMi- z9ojy_BX;<&sU-sBBgA~sp%-Kx*>v==aPNlX!;!8> z6C1a57xz5USJeMdc;La*1DUm1sE{T0Z1ypp*--oSj}}j5sd$v1H#MUOm8~d!KCIq! zPAmZGdC-lZ{u}U&^kv*K62TQ*C*?dmts`> zg<ch&Opxhy7O4ve6K9}9M(CltiPffxDX?n)Rcq=a6gK1wNk(xgwkV4kAN ztj4r3M>1La-qO3)qQYp)GRo&uplP`Vd+JraI&!HjeI0^ry8zr7;A)$dqgOh66mWDB z>|39N{)qD?8nh0pc#P~-&1w&e=h0!Qa@$UwuD#RSs9BrtvaN%CIS1Ba9WFMB9SAY0 zXP4UEmlH&q38dNfhd3)jm0i`bm}^Q|y{zXjmpv={Bk{){8Q%nL z_aUi76k|j&M$$lMh`qMItr2}IJ6XIjGyN_Teuryg;z+^zj6aU?{6AiLo#J~Q2vrb>Z;Xv8;|+yuf6BG0p) zK%~aNCZyAfJSCLu_s9wk)<*V=mS^jvHOSU5Xp=v{*(G_B$Ii?nj~?9^?%vSzaI~v9 z@knWJQSzZ^--G=RSigaI+=^aiskJtB|3G0VyOKBSmu30W`V+ay80JUC72w>IHbjl` zJAFs|k=*%Gx?TzJE7zhN3g0QmGQPuXp9bI`2Yw~-aYgZK1#BKLJr;Y`6t>c&9-;O9 zbrEN7O0JmtCT%RRgP^o0+EIaMw;f;i2gn z@!YH*FXdzXCmKhZ>nXz|5B^y|7X>54G~l_M`%Gh z(RkFU^XAsYP zF3$GAGuqP3jpz%yj-Uek7J!q3b7k5ISizRTu`Tc^=xx9ocH>=QY^wtPdtb)+CJNuo z0cuR2lO+V10jPYm0sn+2ez8Y!8;r6miLcoZ@S-9PbM9 zVhZWb-h#7kl&tXR%jI-Ti*b!}EB#~MMY106*Coy|;_WgqE}k_Z5-FLKg^5JTtvGW= zjd3a+YU5F^tCrCjD+0ZoLLf~qr;H7Pw)rlc!-Hp=k8J)pWU4@Tz7e>^0h|RyTwFEy zwE-3dO#1*S89+a^Fz23SwDiQJpL=|g?fs|3<(eB;;aVoA8iATQ z;5QH0CctJ1SYZv4`OEtZ@Y?h^b&0Wq=|q9s1Y9HA*l%h88wPB*;u+}&u;W-`rXyIC z?=lQYUORw4^A()$16p2;muGRE_P|B}d(Q*onOvhD*aTq1E?5BZodawTFsXm1Or=IH z5GVJu&^Lj@G8ox`{oK@TM0OBo6L3Z@gHr>X6PLkh1(BA;hKGq)Y`h{6Ouk`(*mBBSz2c}@eglg}t~*$5v+-nU_r&zx0DUbd+m z?ZYjct+HMrpqTvY!pO5ieO{h-$bY%~Kj(VhBG1prKNi4b2gy_Nh7$%T^E$(jFZqsM z#6EKju!d)G1{$7Go_XuYhNBOMyC3X%Alh4)yuYlgDDe>2$(#aVE&$XS(D`aGC%9Ya z)LKxK%RpsZ5aLo$MAm^?`tR>6w7!QG%@_ETc{Br$u*D|exdPXHi^4UsX32RH#Qgxc93NVj!F3&vAU3*O43;@x#rbWD-<)}X z?BjU`SuuSF*6#h|Sagn!!!1^Lm0zU_ehs>xqIKqRj?dBd?ySnQ&v$24pM|p+`&1if zj(0#lWa7o%-$pIlA*$ql|0q>w*(|fo7&xA%c5gP>SZ%g@OgOvN7%)I6I=2wA_~fzk zNOSZXI75#BT!i^!s3*V>Bo|@%faU#`11koM=RRKe)sLr#*6VnJ60=IL1+ePFIEPQe z0=ORrtOl?u!W_MQyKZ*jr4it?17}p#vCO+fUlz-I# z1+9mZNBpJeFTg+U!iT={oZ&q)d0@S40&c@^IAalEalo2Ue@W8gD!^|yU~%Ae5@6;S z)D2~qvdSy#yB&BVz|&)*BUej}UtfCfhTwcrY$*JzqqIU@BA!!-r}j6Y+bLdhY&tTw z(y(KAcRS~np}s751%)QDz(E!A9l8-9LhB7^&V1W|i@L8{26~ zAY4-FH)u+J zpy%OuC#(su9Gq9`mNV^u1pza%fOtv3qJRbQ%!{yLzzPvxfB;;CjRCeBcr^l1urq)S zdSG*aeE=AFj~8Xc1;7HwmeG5h{;4@a#mlp0TY%3t;1$4+=hO}ZRziR?r$}T}C|)8? zJ#fZ6I9MOC_ZzYt9l$vQob`C-MOX^33BalZqVhZf*!tgwo=%uMJ^F4cU5)Y*T&&`l z1%5s7gT%+hc+UgY>4F6S^KXGZ4H)%}w0N0I#723>GjvY_NAlyAXJS!gUsdNfRReDU zc%(l1gY^P(O%^kA!c4N;;WH{}lPjrB62L2h~ zw*bEd&n}+skv-qaw?2}{?8@4X6;R4V)>Qz%95BZy=ww{fb&D|oXEv!iCzsZ3x~C+@^11Oi^X_=n_$SLjnVf#n`r2mSurUtcs5S< zR>*4m&2i#Xo_D0!IjQ(%7I`@KyJ%M>E0j*cwLV$&lb&naSXsW@t>EWjoVSc;SNVD} zq=Z}@5_Df2!JC0Q2i)~8IVAqfVl!o4u#a;u{nu@&vs_k%w>l8lahzMMWwPW`ToasQ zkJR`NymLI?*quMhdB+P}@Fsv)fb)-ae2#tAwZ0+zxee_EJdq!I&MN7;^a=8BA@B|Z z@3<>n*}_svPI{h;>8u;}0ql3eNTt|H&M@Fv(f;q?>|sUglr2UtqFArBm#P~ZR51@% z`N#EOhvxGg1N|uIXB55gk*K4kvK5Pb6sEv`fw=%Nuo@*iyd-Pkla@QYq9Rprv9s)671d!GKsHj)a1>w@MX)_)st zPXM=3mCb3Va-69FOx6hd^bqi}ejj+Qz9iX|J!!IeMb3S*IV~G<4|KDjTmL=uSC>vC z_690Fr8JyTr>8Ys*L^Buz>4&&>GoR{Xoi{TFrVvrPz&sF16C?y9~ukG$(V0W&x8r^ z1(8l1Lb3V#;5S!46F!nQj_{0=3ay}hq&+L0_~3^OI4mnPy4eT0hRyF)@W~O(VwHB(f zZOfdEpjJor{`d?w_H83|h|~cjkwSVr+dGJ75SxCN$l&fAJtmIMEEB>-B3#FGSyXaeuz*zv!IfbL- zpzVW}ZkOWtk^=tddyu)VzAJuNGM{nJ#bw@MHOs_fhx}Vn8pJRgK|E~xImA_P67}R7 ztHj+zgb0^sjLdr)Cv}Wtav!6&ON_4WtdGKK@W5Z-9QbAFxuRz16(83C|J)DlIh`7J zxhKapPH|jm1Kx>KIA`A7$GG+&X9#fqM_Uv`yI~_GrbJ$x0By}*LVs2KXxhFfLnczz zAGk7Q1zj%%NOytdv2Y%}O4s2nD+ElHNDz2|XRuEBSNLv%YJ)M)e%x~UmC<8Oe9B${ zsb?)d7je`hj$-f<Qof#N{DOms|DR7QXhONwxfgNi*Bkw-fW9akML+_4qFbBt^-X;ze%j zN965d;GY3L&qD+Lteou?=BW~LwwD|-5#bR!RdQwyj^#pUbuG4D;=mm1n=%pHA^$ZP zd3f@K^&ywfaV!Or+l1c;(r=x$ZA{9R7UmAvC=F_1?uxb8H5`Gg_@CuOea|Bf5k&U6 zl9!i#lo^H3LNELqj7_&IdUf_A*Zj-5=)Wa-YwvB5{#*L;B3$dwyccF{>LD}Jw#rDS zf|Yy(GH=XO%YeJx-UFG%FmH2O9CJgh4;3)PBtuM6yBCNJ?lkh5kMA(dd3i(D^5VVz zFS2e7f2jFTRt@TP56=8&-QXh6<^gsLusngF-El7h%!fKm5vJ!qX6#&U4ov>41x+Jp z^m!kqpILp#h6r9uFAc*dGywcg;75^e3!agN!ME2?n78A=X;_1^>4}4jd~p)6dcb_D z+>R_LqkdX<=Et%TS@ctxYOK?AzFjC6zVo2RQK1vNiDO@nibbXpT8|P8Qa>f2Yvy+$ zm;qdr*Oh>^0M?*%v_Z#LC`xllIsMV1v~X};!PO_SVGe=Vfl9S=I)u|kS=UL#mB>cE z@NDW@^Z@u*$TmeKqAIjXAa!>NG_#-yDjHLF_Fh)j7r*~;0XRk$5yo$K;`Ot0H>|FZXnIZw%trRd;=p($wtQxBkE}S+=}>(z@NjnF^ZQR&(iVpfuABi zzLg1XB+2U9Sdy2fk^deUepQWE+5Y-3NxU zY;C};zZrU&n}_VVScH@42y6tu$sXep>C!eQKzsUDe2YT$HzysOePy*ddpxzdRE(to zqi?l=p6$rJHMN995}UJ9G9(!eTd(&IS0H#)^E&phGJ>A#cj>T z@JgO&1n%4&_-2Qztg3|exjO9H?qd3eK*w+4bleS@n1dIde5o_D(z!)QT;jl_FUVL65zP6V^*MEWR^613*f#a@n%wr|KbDOHLB#Rz9f3WC|8VxM znQ6r^w(j9R8%Nb`ia2wF3t$0Q`RQftjGZ!yHP%H3!D9btAT;*NX zu0s6bTFj?`pGwEK=Wz58gg~;-=$`?8@h$i^#|rq~IbTj4>?~J^PyX5r`~bcc(~%xO z&evr|#2(BA2wgLZ-by)#tdc%Nqq78KUrCu+k zf01SuG#5T5&H5Kr`(0@|Y@0Y})+4{ISdVhGrS+a_6E3USt-~_60RNcb5xfWRI}BLv zi|P7{?4#h~BdU|>@OxQBz^TQvE1g7!G``fi##Z&n?`rJ^zMuHG$jj#es{-si0fs;I z_$0F@-C{um2VR0c{Sx#EwGO7|J93sWCq1P+Sue(u^<=Wb6YtU|fGjJ&;T6QUteAgZ z>?19J?FNkbK^{c6A+ykc3avtP9Jb$}#%oo0woYfNRn$}xX%8*ZoB_=c;=WRv0BHR6 zs29*^e|W=N^kmkJP12jXde72knk#O^lXwn^O{0R)W@?~avaq?8{xgTN8~joWhw%)}@i z)&;sO`AUf?n&=Y#y%=|pzCM%Ik(sa`TK~whs7zQi_UdQ0U66Ed;5%A)cJZSYMbef? zRPiD|us(!*nffDr8(^mZi)IMDV;jf*EqNl{!9mC@s~Qgj`!Js0h3_}H@+YQ&Qnu=@ zUJHvzA$8On4Se>FDYh&QVS;a=Zf>O@t0oWIs|7MQZ$}!Zkw&pfqs{r|gKT1y3HmPD zCDXN|GTJ5C-tU3Fsu?mt(K~ilIX^evySf2Y;hOUXL9+mw);FP3uf~hndnj9ix5#m1 zaypqZ>}19=Se5Pxo3g2Gei?EvfNymn3dG^g_hs#=n?TYE`cZsauE-L4udWfHu3@F0 z=;R9m^*HE8zG%xNWdkXt`J>Vtd}yBr_65m%+Gi+a%vnGjXYhSG_B~z!{Bjz=Unmc{ z4+^IG4S2)${mccp7&ybgF?8hnC}3m%hI(iRebe3cIO_DhC3^`f$WFqX>_aC&SAsH} za<#jheJ<@TXKGjWWzi^?Hgyo5TL5i4()@Ybu4F&vN%$UI2l9cozv4Xedx+Iv(enlL z7xok9K=Np>mA2QG>ZvL)>w6fqL3~Sa%$ufYOwxD74six}6TnN~4-9>Cz<+KiS=IW^4@J@e; zZZ;AARft0nW!((o=|mfeu64;aQgS5cRmcV4A6ND3jAb%akXEX9l7+4gydY>=KLPou z`mkfil|{BNXX)?+>d)LUVAV~xrq|07ANB!@UhFvCT=mBk;*Q^oZ}YkGgEckV7FGy2 zzXN#~IGT^0HFm5)*Q1im_`&)(2i)2OzNx74>Xc84hUEj(yWqz_e11zafNvdMQ0Y5y z%bJ1AZf)Bzo0z1dvW0-{hL-AIRW=|)Tbe`-g8ne*XM6FDN=2{b zA*G;fFpP7aYwvp#yJ7nH7DpVX5l3qu=Gd$70$%ku}I%%`#%%yiY6l6;hs>u zH<vsn>qe-^n`#SC}&@ixh_jGK4pDFg`zXrYN-`VyphYuZRT5;HL7_pWPv267O85I=siwB;Da6T^9qt75EMJ;~S88Mq3F# zifs+L4;>FR@6v)UGM{OTfM$K+mGx=j&jP;{_*arA;^)2&x$(e7vmPS)w&+JP%M=;Q zZ`+A}#4$Vw{M`@YdyuYrim}&oEk|Sky8jnZ7}pTQzS2TEd_VN(8Go41QPAfUU0JyT zz@Gzt-ce)o%3V0Wjnwx@0!SpOO%f5X7~)Q#2w zYzpTCZ9|y(;YiuniGVG3@_oN%q|@;o%*obDI;Z8#eOWGeM4sbklw&@9y8BJww~i_i1Evr$VxZ&zkzx3D8A#1 zEb*$vFCQ@9A7Q*zZL9P<*?#O80p+`6JD3y>Qiq3fw9mhHGKLdG0dd@uEA&QR+Z~yz ztN50-A&uRCjJj}-AGGK?{x?y2V8~i7jz-W`5i<|EFa|^Hg=Y|F?g@OuclmbQn!Keq zFV=rk9}aUzt7K-#dGE4V1?L^JZiQL27DQkKe*2A|e&d(l$N+3yeiZH%u1?Vx%N+o} zet3p3{xIXW{>-dbA=!|fEXv=2U)w>*jrY7dFeSb0Q!bu! zm2F7t9Ma1DPp~hjy4LzSTS@#&D)}Xb2%QhYbVE>wJ7Wb8OAeIqWVe<57hcsT&ywMv z=HatQSDYE-^A3zut@}0|!h8?kaV}Bve=PGz$y%yY&g>^xi2PXF#i3?8{21lc^|sQxQ^4%sQT#Da1!5gR^^({Jo4G2p9%MtB%g?OZBBf;yzlY;Pd?Z4WcR1S1DmWr zBIl!$pFjJ)@hwrJU_O(`-Jj}tl9Z$w*u;ol(*)b}9h>f_pC%e2YhherMjaa1$%0fC;Uho~coS2oZ!BRfw@fk%t8GFg6ESr1;cTzw*hG2-gwQ>vv0E z&W^R%pz|vKwn72U;TSW6csl4&rij3ht5K7c@Ekje)*rrczX`(b!v{R z`HfsA{1zwYQDBEDu&oPAc!lFsE#gT1(=v7)ncKPjPB2abzZ3WufS;@K?9#cYK5n6Y zB?kn55j8qBDURo=`Ip`7x}9wf$(9{54C4;pmbT2a?kRJDO3)7CTjbiuM&<&JPZphZjcOvM9<`GZ(-z}use`M{ePH8o{ zz**md_2>65E)t(t0e*#mC7=(s6JX|2%d8Vr0q;EUhFy3otrN5Yw*~s*h{6^9FGf2N zZ64C9%Afie@CuQiAn|aC{sUMwU_%5regfK=0JA_w;aUrr1O6%CA5O@@M<|lco|h3m=oF4K(%8fj>?<$3b)YW75olW;e>8 z@RpxTa;hwj#{+O!$N6tVpNH-|qiCG@pZGV^u44FE@kk}`JCR1xn}+Nnk~$XKQ#0^< zf-4zS%SO~-y$344DCdDN3n(ieOY zvJ-q5#4|6Jvjnhqq&@7;Gxc`y$-tH}@y2ipN}5K{kHD>PxY?elXeUw795@#kF3S_ z)3foR$UWF2ySiF@H!0hi5La{T;^N?vJjrooDo@PLRvs^9MczOAhRK!c`ss&Dnf-Fy zVz>S5`=un3JWn8<;j)X1MM{n-eo&tap&2PA)*!Nm*B8wy9muhE(g+-ZGg?jo%io6b zquo03?B!eTW?;sG2&!zuoUY>HV)J!^r~Eb|+%_${qvK}twCuTle`#$qtxm*IiL^TK zoR(HuH=U^^t<4A;1>P|5<`rI>Yt6{kZCCPTuLR8k=yy+{FM9jG+JE`f$8i*6eDN88FNydXfAaH{uNw3dpwIs)Y`3a>YMv+VLh_xJDD9}m{I7=paL={f z>#7H^ze0Cr_T+8+rY_)gUGW7FZ}Bf;z3s0ru6VZl1>jTzCwCV5Bb|2B&XTgvc7K)o zb?}|Za^7*W{~P*l9W)Oo^}+5t4Pcx2wPPr(=7yhxFtPBC9lURO-rvx0YsbeqL>$AT z+CAnz-`^zZ1R2}zvM0HtxB>L*>R&?r@5Fd^pOjzEAvx)D&ONT?yzynuO0rAOz7HQj zo;9F*xW$>KnJaSYNm8DjjPn;|{*t|`|MtH815=YxjM|r|igEOo*!|r#{DrChnwVR+XWvOr9eQ+3Q6AtUx6%$ft^6 zYkF`bdmD*brY5Texs-3O@R2jJQl6rK(&8kDDDPF1Q!f(1I_Jc~30)++T&TAZ>3P@@ z5X@o=?c@}!gc0+KQ#PW4Th2hMQ~71_i&%Kwg~RzJP*p>SX-VxcGuF$KQ_r{}eq9+a z{+a{~$B7*;uy1hkPQnT_4I`BrBEmBUQyrJ_mU#tBa4c;33dY-iylB3els3O`t=rI# zKx-mOCm#Xb3D6B84)iVK53=kz?37Ehz~3|vd8*`q%&}x&mV5vi>y0>**R_>0JL39m zo3om)zKD$}l&|=!kehE~{TK28>8p8YjPuZP&O_s=+XwRDyAPY0nbVF~3?K$)raZQF zYx1sM)Y=_=cUm8^he>@8n5VtQTxgI&jFWXXjI`%b7VV3mWWGA*m@>bVmW?+(iU7yl z4?s8jKW$sNmWAR!DTYQ`ze@S7D0u9>2^EI9SLP7f{FXZjkq(&U)7a=3mQPGUI0lhU z<<~G@`d=3p&p-x%Q0evZ<*^9$`Zy;LWz@@Ma&TzL0N4n!PQ+30WsKXaB@V5F+xzaz z=-LsM13zW+{kMc2E7HWr%HO>G!P)oGU2N(qB;mtA`@vT)F8Y<8pzB7eg%uQ|Jy6Vp zvWDUMKfD^ne1(|#kh;rRIH(KJ`4hj4`U4-evwbdMYdHbD1>g<4@RqZ+aPBb& z+}86~ZfD2^P5!Td4m4SeA6F56#emHKR!e|hvrF@-7Wg^;tbH3Dd9}np zimY-NxB=kmHAiQC8e)oOAsTn=8I^3hbnFkz+bPg5fL`0~9Q&K->FMj$0pMQ%e%`{8 zyx~lCWfI8;PT*G|KY>$-XOvg`^kDQ^$JaQ1L4*4f$dvY(jyMdbo z?&&lglw!p>gH|~?MNmcQ2I$?OPl3L05%xj%97eu&7~{mYjB}W6T>p-wvN37dE*1`1 zIhityv7M70u5%xtQKV89%ZV&LKo<~yi?z5omsSs~f`|+bvKGQ>K}YuHk7MEHUju)v zT3l?s1~1&+#p7OOuU0t^e|@&+_U`;>;*PF6!`!81=}(eQP09+U`h5H)wuF5*gR^Mp zM2sQCxMZ^zl45@zMcM`H7L5<9Gba__rzYgrQ~{82k(bYbW)3t(1bFt-Fg)ki(SKG5 zC*Ktz+<)DoEe`^K1pzztqlL6M;o~X&1HqEldf-eT-c|zKG6U=UJ(<}5pUl7-zv&{1 z$CIEbaHU1ktJl>UxLN~OYv5`PT&;ntHE^{CuGYZS8n{{mS8L!(Y9R2B3Ttpi-Q(xf zy+hwS^*yQYgZe(C?<4x2`#%(2jlR$8`@}yexc7fz11E==XO_^p&AB=rRqy`I>dq-B zuQ82(`pI_< zp~f?GCLNQH5*=QA89bAZ;AQYke3wgarErtZO5vA_|L3KT>vC1;dyT%=>btkzE|-qU zziAgQy$Sz$;SHXAEkqT6wZznYPTw2!y;$Fa`d*{&YwA=E+IA{B|BLFqL%(0stlm5I z``QEQJ*nTb;qS_8P`~HwQ}09i{aW~}^BU3b#dRv3wwKktL*EDWeMH~K^!>EHPw4xc zzAxx|u54_F9aGTr&~q@UB_LmED+@9T89N&lpV&+0qJCpZ0^hA-$l?cEGFYhS)M zRlWoIe!VCDLJcp`ce=hTSH8Fozj1}|DjmKa;Vha-zedBG^!+A;x$Cz@!-sVJ-mK#{ z)z@Ld_5Btd&OGum{5PV*^8i~e|D!tm))m6XbogzI6qjiq^@epS9e%q8c*7@jIM*Y1 zdBbOQ_#G;EN%))&zjKA~c^!WD3gN!bt9HHz;megjSBKxF!wrAsX?Tgg2Rz|H4Ik0{ z{1XUg+nDyL)!{ArUZCks`*&#gsJ`F3LikA?ejmc!SBsuEOz80Y5$G;ou9i=8I-F~L z?Ej{G3mWd%a`1r_!t-?agF2jP@G|ii>hOmEbITVKUZTT`RtS&l@P|F&H5y*4?;BPK zZ_?q9AlzMkLmza>cc3icufPGtAExh_`tj+07u4ZwSA)ZNOn8kBXS*&JUVEAFT%8W( zzneaw;l=t+`M+FvP=~V=?(z>kt;!YG;gtXGa3de8bhwfKrhQF#jSi>$Uyi<3hZ}w0 z+rNWazoGmmJibIcMn0PH!{mQN(~au886Qlzsh^WN+&jLU z*6;;==lH?AoBr)TsOmSM?;Jne;e{F=)OU^_?(n#VH|YCH<42PY=lDT-{&=-%c&EOb z@xg>AHGEXxIli#$CjVm^KB4a%Kiu)pYWTdqFBv~7RQX5seaZNt;R~9cg8&o(|{u;4WXGhF9r3<-5ClH5%SgtJ@LbyiENx={ z|3>~AK;fe*9`hd5?^T~x^J@dI((qajyhXoP-KuJ!M4VvGrZSib!6e@QRbydt;ht2F(>3iW4lKj)#V(fEZcq&w@uuhsY`FO!Z*?{fDEPy7u! z-sBa=ueeS1C8lNGV_LwQ_kaI{%87aZ@{{TB6`xY?M(-c|zIr!ywqp+|{-xaDby5zX z_W6t)s1>NsXY|g(2UI^YdL+j)UT_b4op;&GfxEGD2C0zX;<(9cObbS1w>zWrXT~kc zH(r#J?#pmVRZyj`qwx)Dg^Y5Um+6g^qjIIx&A=O8QST<-$&KpWNC%U?(Q^&nM~kX_ zsAmCP)bB0(6+N0;%5TaIK9=|O`&9VP^!txYLn-k8^Zzm3-dFFcs;}0=)p}qJT;+qS zd~lTyuJ(hga^R{QxGD#(%7LqL;QvoK5csT_xvBi04yu{qU`)-#MrRZ}ezSsiHmBPm z7XDVf=l-dhn9Z+JGu4q9Ghi}+ns`I((_gLQ}te>!)xD6FJIEYvlQOgn9AqL zO!Yqafx=H}J5^q;g3o)(Kcw?9no{^fYgIg*9{X9IPQOFfSKiN5y^rd84tVM%xmu;? z*ZHf`^>6BJ{(hBS@EV0z__S{CAE|om)ae-f-Ey#&md|uIH1weFimNiyU}^bsF8i3+3tUNeKZZK&V-;+rqCETg~ohc&(phZ+tw zA8f9B(YIm$f#$sjn|vD%yxzQb!^;QW-0X-b&2=yPB&?zCP=jwn z{aXhPz4e;9Hy>0XpWl1%(EisCq`mAx*ulMxNc=4X$Wv2eGn3qp|K`0d_}_;YguPy0 z*Ieh@u(x5)zJqnI?cLK*4_fo4DE8DHJXrUZqA<^-V#J6*vQzik{+E!2*AcOb@Wn%i zBujgw{Ehn$ylUF>-+HIriJYvs6z9+A2Xi-eFq2MtFZcPlUO?NDp|92t=3Zb3^*z1y zeZKoV^evj++yj@P--Pggfo;j8pVSZLZqhe)DPzwv(S8;WtOG+orXS3`tx#c->)BHb zy|I&(YPg{{_C9kr_B|DgW5+b_+wef!nV~oKGIOuh;?m^b;FI*xzFo_ zn19A$@~^=@ngiJ;hThl*&E41sP5Or3l)qWihfRdq-ssbJW1l2F*M_|G?*c}iV*Ypy zen0(g?3xPd_$`?CxA5Sm_v$gT#;~gixPHdV+}rW&rZ;}P=AO0Pbs#g_Y;6~P1MjE`+DI63d7);Xw190 zpF{xjZ|IA)eZE-J&wCii(3$wo;(;w}=#4%V_^iS#))i~Gz|fm^_$g@J=^OieRZwA$ zuyC%+(3|_;BFs(i)8nD9N@19EOcaLBy#F^m5N7h9tLbw!y;jc*`jYnh2ZS(v?`7yI YU)`6Nf70^qLX7J4|J4QKqc^Sp1BQuYBme*a literal 0 HcmV?d00001 diff --git a/third_party/qt5/larch64/bin/lupdate b/third_party/qt5/larch64/bin/lupdate new file mode 100755 index 0000000000000000000000000000000000000000..67b7ea5cb50edbf3a2daf656f31379ab88ead332 GIT binary patch literal 1095744 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�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%0DTj;q{jL}vxI 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�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�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

  • 1HKGZ&C4`}nF7d3ibbTg5HO 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 NwEu*Y#I Date: Wed, 20 Jul 2022 19:36:22 -0700 Subject: [PATCH 388/436] less beeps for steering override faults (#25196) * less beeps for steering override faults * less repeat --- selfdrive/assets/sounds/prompt.wav | Bin 64634 -> 144642 bytes selfdrive/controls/lib/events.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/assets/sounds/prompt.wav b/selfdrive/assets/sounds/prompt.wav index 420e9fabee62e2c877717a01ddfd43bebd3c15e4..1ae77051eb5722b278994f7dc9789970ddceaabe 100644 GIT binary patch literal 144642 zcmY&<1$@-Z6X?0SyVbowixqcww-0xx=!d(zI}|TcoKjlaLfxsmyZ0=~NnXniq zf3rI~J3BKwGrPIVCDX=^8U+R5Ftdlw8o6>qu!e)dV6aFz7=a)=u^1eNgIT^Lcu5|@ z10x?i#s=dsb{LBcqaPUQ*^nL#+hGF6`rkN8hY_kB>DyscB8s6Dlph6@1C@nR9CU|7 zw2$h?V9=SUep|N;!hbCWp*ZF{q@q1kGRlnt4w?H`J}L#(gh%WmB2-5Y`(Gcm*J0J) z7L|jp%^?ewgK8xr6o*y^9kmbTLdP64Q9IF@4l7U#Fc`-u>JNurhsXY|)UkHdHgv6j ztw24Ca--55BaRl;gN|VkPE@l4bIh}K+h#$mgu1!@`V^J%7ckwvLw${|1pPR02ghGO zI>t~t(NR<%DjS3Nj)2HU<)ePYV=Ucz92_V&IuErIjU#HI1JJ%BPf#lQacFbI6U7~R z35Yxr0%&|3v2#S<5oL#G9GUYs%749v@;QoQ9K~_n@%|s?h#wk#RFcD|=qNgafPf>S zwr&eZ-7EbYQwJ|vJ1{#U1GN%ey+andE=M%bmH)+1espz?taU`v;StoMs4uO^C=CCN z0P0^X0;ne~-M+IUxE+y*@>{wkp}b(X=TS*EWDKp*nP`j7L&s6+j!~4u*3E&A{14E$ z+YlayMi^;PJ^%k4RJTI|I)X+Mjky(RVc61b5o#5>S~L$)43&sx0ctObAtJj!C=CTi z{2iH#j-u4R*?}MkML8M>ao9z=bG<_Ahe+y8+hGaa*0WeyC@O(Nn6A~Fw+04m3k zkB$}n#WDZaZpWIS?v*$^fsXz4!CxPuTqvMkM!~^jL;QzQ(Um)D5a~nz^&K<_p@0a* ziqNqbGcptE#_g~f=^K$gh|IMiqYi$D1S`VhD3)$+$GC&XhRB1vX9L}`(HNom=jNxHwKU#~wm60L>3{7olTlPC2e6=m?q*j(zxdkD)yjM|sel z?VvkabVobRUbOExw;ioxAOF>YN^nq6TTot9&tFc|V#hd&k-BA~rv-X)pmbCNN+ovB zK~Eh=L8YSWL(di>!cF)OP#phXAt7VrZmB5m{|ccSM{5$oO+@D6kQ(J7AwcLZ_-+ar z8AoX-5dUi(eUyvT&580*y1_w1xk-pLl#h%6rMo3`<7l7UJx=bPk8+X_$^QeCj&h(a z6_Lv5mVox?h)lFc?H;Egv*<{VfwVM42U@8A6%vMtNT4GW8dB&8m4N^q!O@n1v`l1- zjo>Tu<`GiASGzU=}%}_Mk(3$_sB1bkkq)`!mG;U<@)?u^=I3eJQ)NV-YiE+kwAU$^k zL%|EFeY(L5v_4mDzx8nIJ_5hFH=kXq0ke<8AJ9JqpRE`kf-TsRN@1LwmTa1NY~VCmf@1*wza z6gUA+h2sz`9!^9`0)odQ1#J`H7&r-z?WX=lYLqJunGugrQxHf;AQOQ`5Ifrto7<2Tbix2qJFZ5KD>b=$CzFxbqPvWVc!h_= z5sjz{iJunnm@^VJm+p9>k@7~s7xA|Pe7j@if>2Q&4KhdG?NGOS=hmdWj0I!<}KzpV!tc$-QXvD9-ag1;AVS- z{iuDm-Q8}t71$ze4{Yachi&_9M{SpFFKj8c4x6WavHgj?(;kF)t{U!zIf2Q*D6n%8 zJsDU$t{-j{?lJBUj*p*!KaWqqV+p>5$%K`Jjf4$^6@;mTL4;le4S`0;!{5SB!iVBv zToLv@xZ8G&_>HKuCRvU_zbyiy!WwQWF#BQME!FL<#xs~6#4TMtx@L75Y;}~rh89yW zbPDK0JYc(TO|o&Z?+B5MN!Fzu!wJo_{$vxr){Y@AB=RZWNHx?ol5}>wd64BYQzh>d z+F@DkN$LUtQF54mr{32VqIk)`+v-6|=UzCL9}f>D>07q9I>DBl!AAFgKMhtx)O@Duz<;m*dL%w4PJzgasX!tJz&n!kcm{$En?~Z8-6U(nKRtx5yT= z_hFAwaindM)iMHgU1uibE6?)s;-1pGwZ*E}sE6tv;_6CX>cc!gYUDO?Q+DZ8>l5Xq zMzT@Rp|C?<@9WP5p73-v7#hdcylpQEoTh;~i zs_S3*B=6pjkJ&ehCgmQ-oy%1hW!Dpm=ey<0SJn*7W7?7fq#oat6DwCyA<<@m%=taQ zAw)?^nX$S$$xYHCQE#O;c+2Id+ij<#q9b6mG{Ga@htXpbrO6hh`8xEF|4_r0y2s@h z>K}`u^j%qE=D~&=Io}FirR{r{pPe5&rF2HlzR0PSUgc{ONxv`R&x+<^oy?2NFRNy% zw!3rzr@%+vJ9~~5$FQAoKAr(;Z*HaMCM`%BOL#?z5^wP}^nggO6n`j7<(Z>|t^}92 zVuKEDLs{JrM*(r{R>ytzosLcudW~&w~_5I*| zUE_>h@72W17{0IHMbUTFRIiskYJ8!f-Oi)QMe;E+T#xq+^=St?@)|Re(~5Z)54`A~ zQuX-Ytv)ZazK#mV+Ru6Q*FCM8~D9fX|Z(!MX0%{*Aq^V%{o}gF>BB`_A(Eq)nYPV8#x=!K|77$$gaG zeWc2g$s{fFZ0u^&kGG%hoQnkS#NPfEI_0+gQ0|38uiT%XdfD3MT(#;EjxdTlYtkxIYjZ5};;@3l!#K1X&2fIf1{W5Z{-%EloWTHImsIFV_V@^nq@AAjEn-i|>K04*(!wX60*Prq@ z>;Lq{t?SR&&kn`zCvaP}jm7X;HtgOp@cf`)6(w-!i0t7hvnoBt_Pjs&v`b~L&%Sq9 z-@JSHdh(|U+x?FEaa>%zW;mS~JAUF;PhM}m`_KWaXT}AEE1iY^s?FRx%%wI)Vs>0= zWZ>I1VL>-voNqZZ`P|^sM~|*O|KQ5DN5)(9^EF8)3+88hZopKL8HWQds)lj*>VOf! zBZrLM>NnQy&E)?5rv_CFTBn%n&(YSfj2>-1wF-BUr|T)rY~L}XPY2riZty+nUNh>( zs2fA_e98n>E-QP#Q~YV3S~s#|SzcAroY1IypI_4Mt-Dlmc+WZ2?cS$bPiig<3DbW& z@%-11p!nd{54`8tSa`1}!}Y2gZ@~Kz)4hv)z<@(T_e@ylf7J7*U!=#nfGL`6)@>ix zfS$tfyj}t02d(Y#)7#0_d5CUeY2O>pZPMf+FGns?z9l(Px6w-Ud)ht}&HkbI#(txH zboyb};{{i)9G-J&^=v5ruMV+BKGj^ zvBr5Uz7u~<-WxZul%BRHwcwXi_|}&xH+tMne!AyQ)G6H6U7^PwMO^Rod_e5ytcJ{u znf8*!#Ch6DLMy>fN9;Gpw>WTqUsE5f?{dF?gKh?H>|N@%O|i@6h<}Oa7k-FyuK-Wo zO3kYt=Z5eDdH(afH}%wyc|O=nyOXutqa` zcouSh_66=`_m_{~U3xk$a#M8QL{7D^DI9hp#`5lSGX*CHUKn)4vqXI|aKKovK6O5$ zJ?45m?j7zkM)p+m!XI+82nH!7ffIUv^>B0L`TXpcFl^KitgF`hL4TpoJlzs$ZwVXN zpkE41F5jIU7yjV$t+)QqOfQaKeSRtJUdm(ZP3JqZk3HYpLZ8RPWVkhS8YVZj=-W6) zyl;DABo{q;_N(bXxA*8C{6QDJussI%daW7SbArbw`9^iUdx_Ttwcg{7=NTVek8(fk zuzo!b^jhEhw0GRV^}WAqa)>W@!<1*i^2VzLbJB)q9{Ta{o&M#>=Vi|**DROJ&w9R& zeTcih^WE*}7eB6*T+EnM_R0PeUdf>`Uu*Acc4;d6g#;|=U7%g)^K#f?ua~}io%>2J zc$axSQjipWzL#7dDjHmvKA#5E^!)7fk9VE_=^=-POjY7lrS5iT5oZbBod{Q+Yxq>M zFSa__C+t~RTIkV`j(1ldKD&SN`IZ-wH_4&Y^5%rn8=KQKwN&39@qig(B0VJ`E0 z)&7@tzdVPiKT8%HAo~}>^78rBkAIK;))~+JSpVMbWzmD(ucMz#zZ>*?>BrWuLtd;+ z*qSxE&|;I-RCmp1?B;FJ91*VcNYd`{>>JqP<`jh0=6GQH_E9eK8sqdyOxG@SKco32 z3-Pa1ySoPUGC1cCOzbh-si)socYWagKxf4(j)JpUve>@JRNS^V@qF2X_?(aKvH!ll z7*_v0=RwKa4bN!r20lCg{z#1EQ%>c%Y(_EPHo;iHJji7VZV7nmegS*jNBL}bt=B#e z3i5Ds|KndIgmf6Usj|l!s{1wdF=@Q_MBP5W=0QIdnLU|)bKL1eYINSNr_@R{NtRBg zGq=~Ib*^j-`Ms==_4#R7#P^{uPKVun>i&rNV)64aUmm<+$KaFdzkjQ|S5?|7V2p=I z)G7Q^szmn!oyf;UhU>YaN4fTUKb;(K{^^$|N!4cSI@AQ|FxR`9JL<9hon#eWL;9`L z1rA~PgzAjG?_9sRja6=w4kt!g*An~G&T1|#a{fZiqJ6p^HYdFBw&8Vg$k^xo-uOTF z`qks@+TYXu98XybOsqfMIhlNylBMh=3DdYLaGsQ2YBf3Fjq6&qI8daX;gsai% zrc~t%!`p}Nr`|pErsYM$`|205p`65P5liy;g}iK&>7lU^vxD=D86%r5&v2RI>fvMb z3m4DoHNcau{obop|O)a zuXGazs&o+n5xs|NXZG#yk*})Io>EtHyZ{bmM{EDO+jS2j>N8oYX;tRZR& zeGHOV(FLaj_X%Q?(|Y&YQd?lCM(HXYx>;G(qn9sLySnd17f&(394y~VGjtup$x0>V zQ5A~sd$JOuPP~&v$R2(FFy(8X*XQ2ueQ!*vi#ndRyJA^(l|IMt$gqjggLzi;iStYq z=Q`W9)nl0`!zaXRs`9GOd+7pMp=+tKS-8dNmlvIL%W0?kHr3|=%akpeMSXv&T?4Lo z1hZZ0w{OvJ^F>*QYu2ah>eWuK{oORXe4U-Pi=3sNlD?Gw#zyk6 zws}R%>J_m`>8mr>h3$^o8rJsdVr2a5rC)~s*zkM&ubNbPnPX<_~ zW0g;|!HRh2T&1h?BbQY9V7F5mv#d;sl~jwpv|g?%R*)jZ7b|__yV32a0@KgOd4lUr zpGT_k9ucZH{A=ubUM#*>%K~ddr7-{BYI)SlxJ_}Z-oN@8@NxXd*l(>LbdepuFQqI< zSdz1*=0bIeX};wSbcmtigZy`beVW(03rc?{Z-u|hJ=d@De~`1uTRB`)EwA7=N(t_h zgk!aV-iJg)zkJVLTA#qZx`QrukK?L*%?D{Qr-c+uy955H-DG@Pd@Q4P{@(D$U$WmX z!f@X@!v=n7h`JmR9QiJ8?jJ(V@{0bAn;UQ0?%+G{Q7o0{vD7a4sHs+^svo)bQx$2( zs#%IU%_YS<*$2rmg}<~|>FK_OAEPn)tXE$3yXv-2^Sme4^_TNj=OTHf?4*!EUuxZj z8)WdRh-(;jkFIU`g?eH`zKKMTM9_TXO z$IYoxciHKw^a^JHGo3sXh&8XPH|Dddwxy;0vc|pqRP&wlz4yDKPnIup!bipUBrHhe zWM`HGHA_1dn_gNnDMf4yKVC#q{8CTUO?4Wq8l!b~DNwIe_K}I@?MkjHQubYxr@o+i zsM+dXAmg|V^82N8_hGy5a?bEJIxSZolx-8ZQ)NJ-6K zeZLC7d3==rERBizG3vJ_<3jGK^1|BC`gGG(j2CVj^$Tl-Fhmxk_#|DWsB%hBKT_XP zEtYZEx+cuLcPHKqeq3}zULp05bXn}_i`8UG;tnl2^?ul zfrd3cZFp8NAmx2wOXA?DsWFc}XGFb>x&Gy8WJuhlgn%r0{+0swx=kH>42!KJuzbpM z_H1E`=%GZTNK>5BPE$XZ$y8+ZRMmb-j?h`=ESsXS$QyY(wEK1Ly3Ou=K$Afny1d`&eIj^WkdKUXe${3gb`iEEz_DWmPHhvOV`B;)0hdi zh(kY^U+@14P6~*e6hAs^PVU|cN_Bl*f)Rr6;A8Lt8eLE#HcQ@1>J=yDB;_~VIa#eN zMlO+x6j*Vfc&DV7q)Owe@)K>=5ZyPrw7bNqzv#-mqMfg)4AL`_i;5KPN{q?IvR~>l zHk>Ux@n=BJugqhynK1*RPe%sFE{Y&TPfXnL>-e8h$c@)M)wt$4#=+KInC}EKEm*W% z(o1CIua$eaJd<-JW%4odBborkWL|<0BcH8`k|y!|m4}@VtM6!PwMDvB9s}IxYnE$A zYd@*iD*3!Hb_%84ZnQP%L+kQN=ak5EcBiH%9Qe@{YyGwI$InRc``Dzg%(Dq^vYr-S zt6A56u>F|+4}6C)iam{fhO<{XU7aW&D`BhrWY<;2n$f}vX{5|3@sx4JzA|@NsC=yk zuN|rUuAzF3ae3>s-3g;P(MD-EX6XdUgyE2x7C|f3-q!_5W7h|9p)47(c@ZJ_jQqdp9EvFRGVCfynXz>{3B6*(ZfatnZRHYuQX^Q0jzZNF{`+G<9@n4z0e`Tbl+A^=@-!86f`_W2j zuK^Na8e;(?i_uO`6&_O0lf04z35AlO&KIRk;)@c9&s6l3M+!4V;mWk?KNaLc4QGIb6C4I;J$eO|`pbIgh%%aA?7D2=9g2>##$zxOBrmg=)`jsB7O0Xnm zC-hFcnSQr)NR@BJ{bp+CDBBI<1Kc8FH2tk8RN^ApFY4ev(U8^AlEI2AymNBo-kb2K z=%o09qD1bb*ePA1Nb`86o~GWa%T)$>uGF0tua-;{xr$kwLqs|hW1av+Hw~ycSok*c zQJyumBmQyRrI>%>C&i78ca7hj*1tfO%P75GdAl(W8U?gLM=@`xZo(b>&Af2dT6sTh zsW?FKTkuZeqYD;a5)Y72Bv<5m=|1T^X+Ld&nynn+#8d8bF?%dmyi+yEn`Bp12l-Ky z0NlTXZT2$#p{Dras)|V^y)qVRgz8)7Hm5X=D*aI~J!4POoit4Rq*&drN58A%Fo^+a zGgF5a&?^_0k8Qlwvdff)--eq&BvLZC64^~DBVI@ ziVy`}zDofrZo2sEeyA=uHK@d{B)3%Aed%klpKvQLhIYtaWt?K>v~RBqE+u7!W-rND zobdGb_MbU1VZVR={x|kml25K8JGwBad|qvX$=`g&JP`St-IKS1U&DROIV%o8u9A(& zudT_#f!Z$FWNDY|ndrQ{SaD2&mqE%5ol;HFsg)vs~6FYL%STa8w#4R&i7D(qpWyTJEjF z$rR3FT?aRi8N!6=uB2tKw&PF3&epVQcGXS%Q=7v2dDR2M#cro0nDJ$qCP+6_7G_r3A*M zC-h4xLi7(!-IZUK^P=EmIi*UW=L5k;H;V^mJw28)j(e5;54%~qUA|T6E2VM=D1((Z zrM=~w1z_r=Ka_K%Vq44krYW5h4YRGeR0(|=6-ODv z6Uwt?TO~s70_g_zd0CjENZ2Lpr|KblBe^W2D(5IO6fw#oMWoXa=f0}1N^g0AYOHpt z*uehCSVL1$JqZe9Xj56smwHX*y29P*-_l=XtW9l9Fvjgmic0ZGzni)^8(VRptao)~ z!{OGww%yn*FcCL^Qp@ufH3;Lm9(NDb%{MEcTfhYGUelzd{C@~If5365PG(K-jzW<+oX_1NkiT4s0 zCnY8|C3|Ft=QQT;E*nw4vL({E)vyGdV#ATXv1f9tS;v_JM9K0!3ao4lKS|nKrIK$@ z+!arhd{u6iXUMiImZ;yWcd6=?(^OjL4^9J=50E=?(Xxp$A$Ke3H4ehew@oo$ZlqL_ zD$7fr=ci`gN`9C$C%G$$pYS>9XWFa`cFvca*Cjt1RE-N-V>@q|CBy;L9+V>#5sNNX z%d(_X1h9~!oU0h1yefSwx}$iaScH5Rtd_G>IQ1Q6k@B+kr<1!@uf!`JE2n8r%AI+$ z8G0I()`J)dMz$q4*ES?pPbk^@hn{gDeRJxc#4+)L#K(!>Q@*9~GJh1VD{ZPcSvR5S zqw$W-VqbAzQzu=mxGLuf(u8l6xr%qn1nE-o z7G;TYu|lR?rs}D2*90k_DN?kBPAv5m$sXY_iL<;|@Q}P8vlaQb`-f#mTW!^c%2O4~ zOZj;x({`moslC%)C-+E}X0&B|%ifwBR*+nKtg&zN(YCbCW$;e2f#gcuOJ?yz(g#u( z0f)zyg(z97-Euckmn=qgLorCXLXoH>Xs#+xCt0q?hHG+5@{&|ZIxbU|DJxi2R#Wa&bGSaZ!^0v& z{x`k_OUQ{F5&tvyINQXFmXa06fwoYAUPGmMII$ zbKCy7<^VZh)~K9Yc|Quq6o-@s*RE=ox2ihuhJP$McoXp=;Rf<0SjOKd_{3Ym!SfD_ z21ruGe8DDuoA9U5Dqbq#izbLKh`Yo?Wg}!4PvzY zB6Rt+|85U#aj9`B+g13zP*Hp=FFto}{;=G}{H~&;QmA@v?Sgt-%dUA4p>VTeb2ti2PMI1u~m`R*Hyk6X+>{&d85G#Bo*v@0~ zjtM3p9;XY}39pJaiK4`ym?)htwF;FYk{BmS70lv*v^&(5lpuVU?W|ef`Kv?L%ByEr zZ!gO#?o~3b@M3;-!Pfl81t&``l?GRis0*r3Y+Bwi(b&^+)D~>Jjy*$u!w6;#Vi4$l zoHxR=f@i!7oE6+`VYaZ7KcAl^FbQu7<_mL0LnX&0G9gR2N6eA5h?9Axu@o`w9rvr_l46r5aEJ$)_vx*$Msk1ylC;w%xw3nmI| z{GWo=!uf*xLY%0t7#1()PvxcY)A-qZi1n4yNVbu6#7~$KFh$>2FYkJb++vQcHk1>p zmREo!A*BmS9+Zr$I9ZWYWv_qIpl{mSezi*q5^b#(iQNZ3h%%G@lm49goxX`1$Xm%9 z#a+z~;@#t4tkpd z)M7l*dA|8r-I@Bf+EbO6D;AXPD-oAZEr-fls*Y99uNl>t)%>bsvJq#*09n@67&cKt z&LCbU2Q$K1acn@%Dj zYy&fgiXjwYA7cC1bx^Uvt=+wCTMMqCuy#q+kn);}dliLcw=1eEqAH%&80*F~T3TD$ zXq}zLB*1JNi(Q9#hU-J#MZ3p1$PA^IFqGWg+%WE3t{bP7o5$nuOZc1k68=CwMQ}zi zSr{x#;5Bg{@IG+oaEG%W(OSrR$^Q`VU`e*U=IO?f21$pxZ;EfjbuBY(f<8lDwqe*cgl2LM)k5LXtjvY%CiXSXAhr`n#GAGeN@P6{9^W*q~1^Wf3_`Ufz`9(Z0zKU1H;xTcIle7f#bV4>f!I}p}nVE+6_9e~Q zrWXy?+Srn#bcSc&oWOK2>l{&?=ZOXyo(xaXfE+0^gH=kKM{R$GFXCp+BY!!+%Ad zjncxgmKo+AT}L_|b}U7{C(LL_sa;x^Sa+)SUTsV5np#D@bK~(QQroHa#`b4jJB%*S zTiZOF%C5uu6OEMZv^L5iY9>>{naI7)?P5>nPT?~JX@X)TkIRwOzUCM6_w%oDm#~kp zf>;+>PZ&zdL_#ut1fc<2Y#Rz}H{LPzH{!aS+9o$SH9c*b-8iU0TmPvRtV?d#+2Gvl z(yDBE-B#F9q2B>K1w6rrmTG%Ao(Af9JITGzyUAO_ zuj4oIM(`}0o*X|;FlRKop596+A!{jdq?dSSSZdK(oD8}^vJpqX|N@|;>KR*xS?cB9qOf6+eC@|Z5{ zyBs&}Q?{AY!M)1c!e7A)_MF0oK?IA?hT}*auT@{dB?aC&KlNr=6Ixd(_<;eNfLsR zFaQVGMb_cK2J<)5aKrh|DQ&pcpDo8*4mH~w(;Fu@4sHr*Ue#i29o)9OJ-u_VAsG41 z-33T2Cfhx1AG`%0MwmbvOcl{LG5%phGMrgNwwg=kdT==0>zokoP@an0&JlB_vx`{2 zS<%dc^qbVd9WiKSD8-gcXod3xY&NAExaYRIlO5~Q+{(p^R$*f zEeBe_Hg)H(u3iQgW0NT#SY-w5#jpWWip?N|k=fKI)YsH4^g&DwdpG+I%L$2V3`dJ- zox}~{EN0iS)tsyB!_1%b`Lt|m4z-%pK{$$=fjr5WV@rZ6fjHw@!$0EE{GDVoq02863mR9>&OfEJY z-;Z#H^o}x_7EC`&-^cjO~-u_ ztcMHjay{LM|BziXTLEneUxP-;0uw#Sd+kbUfq1l>U=+XCdt+zxC2b|yZb^nh}O zDxvPCh0zHt7CVbsz~0Hyu^KqOY#E4;jWf~Y;IY-zLtQYJW#!aRt>o#KoeLV8Z`6(%g zoJX9EpNd}zN7)y{8f%%Q7+hupq&_j?0{4}31Ju(Cul8t!7DSelIb?1Kl zB!j&x$nZ*EsGnjyXKFT;o4vqD0JK!v*4j2;ZemVh{}9d-rQ`*aIaD+C3O#~Z%Lrk$ zG9NJBuxGLgm|E6O`ab%6`eNE%$}lpE^d5hPun;H2EWjjKH(Re-M?x~_A<$`hU;<6v zCb99RalHPXo?slVzijX~X!T~pUSpZj)pXBPXnF_qvCM@|SW|7gZ3D3;Y;XJxVg@mQ zoK0z<^`*~YjA7Q$zp(zW9xz?mR~Zu-LChR_AblCNiad=pp0tek2=^7&22Zz7glAb> zEn6%CumRW#6q-fm4pXpkkD<5enIYcbY*grJ#xUamW3KTq0$&il>w&4@Aj=VGtIZvG zPI?k<7w#FpgeWA3Qxd5}`ght+2Ah?`_{zG+T*Da6N?~kd+@n9C_M&d2oF^|O9wRv8 zi!fKQD)^bL*~YL`K_8*9U<)uDSY#ey(wdK&ei%2K<{GHR0F%VXGhH(3jN?oqa}VjUd6=pm$nMBqF?3}^wCxu1E6>6?*lt}`w%xtgjB zBTSo2=S*!TcSJV}*a6u*^)8Cuq&`$1RvtR;?Jymi1kbZgw9mI>Kw%aYv>jRi z`G8l<>A-TZ)+_-lOovS0fpKOh;FNi(X_xt}IRJpncHkeN8VItAEXmgA_DUNOtHQ0u zZzZ-7pOAwnBWOM80Ns-rON(SYXZkR%GEKB2R1!^1l~JA%A%X@k!AWqt;pcFXO>ZID zYi2Uz9c=^ypas%qQ|FSah*JsSgbz47 z^1eWZ?YLcHOR|i#j)G=GgP|qhB@hNv!58LOOdCSdZ91%WsHg`3zoyE<vDw7rdS4cU)y2ZG$1`jU9%DP*i{3?+ld=d#LVx^8Oeg#rIfr7bX^4bz5DEE3n+^z| zKnMm80S$l`G#wlOeFKi0(KAL5l$$F}jiys3H=xm6ZoF&Av z-!=diU;^-;3FDD(qgF}@{W^od@@IWzE@kuB7ujoB1V$n~kd{FWrff&9RA;aTtQb>m z!`pb4i^RuqU_$B3VXTDuG$x3ur&I*pdX4niGL!APDSft}%t1UYia8 z(dM5(0zd_4T2EW^?4cMg7E4eOmy?H3sdO$gnm(OLL;ep8MeZ0TFcKJ-7|R)(D1Au7 ziO&h!@GCKU;1JtW>k{iTs2Wm2Z^7N56}ShTfG$E~q2D0eg16``Vv9HU30d_c$O!&6 z`$Fu~P3t*8wxXR*VJ7^z`eSteoXdrDM_oM|e7BgE| z64pf46ZS05CH8FQNBVZ!BielG4dNZV0s9Ae59p~a8o5qoLrLIO@B+9Q909!nmxB_^ zFN@AfwGu1`E$!Aq>qbj_w?}FrE8uL_oBEqBn-xHfDZo4&SOr?38`dZr4)X%%fnQI2 zMG?_PGddZ&83n9soXMO`>}IB#8OAJPbWjhH9}sDTc>H!OY#U~qXw89`mK5+c5DuOL z+W<0D3hl7CT9;d#ErXD^W7M{H)_lkaby))}FmgA3jQP4b8ptxm7?VtN^J3s7cn`d5 z;n^GPKG-OH5Yde?n$}4j&RD`;%_ea!AbL-*&FpaIS$Y8VH~A|0EwKqZ2t$Q)tyoEg_CdA?#F95a3$Pnn09cGSOiN5B zOap-q({uAK=n<4`wb}WYefTd#5@|EVPETQsXGO6ym>=2ixGUKOEE}Vi=1ni5rX#O$ z4#kNu9q?WIXs8(41Wg7sP!li+$*(I=I!Ll;Y)frAd$7%F8H>EHblfJkTOfwTXq^KM z0bBusInX2qvQ0{3oq4`F3P`X>EqMD&%qFZa;T>rqC4+XE>A_geJiyIIzWsh-doakX zU(9zj56WlaKZFVRT5PE8qcz6r4}OAPf&#N2GzlCHEJS|2JhMEt#@Oarx7dqdoE?Wb zZS%4Yfg7#wEFF+HP-ET$_6I@@M@^SZR^tkAnK=V!vF^93VPC8sw}Nz+VxVYfcUTix zsVq0{DyAQMJ135Dn$eS%PX0k!M2y6}h6QkuMQio6&NsgUr+^F08=wi`d@$Y;XAQNS zu{Of5?Q=2r;WJhOHWx0i&9pCsTA*pxBCyi5!Mwq2GE4z1ra{L3&?R6dq_*#|$*?2v zD+nh^@2M7AEWMunfH9oO;;~p1b_%nYa+%hXHj}&q{|7q}BZYUvcR^?9tc78o0!hF* z<^&4>v7s-P5B53sMtcWln@x#bh>N!Wz>@7r)^qR@>q5&)Xb14#G#+Gt4SK*l&V1eE zW0isZEl=RN$P3KZ@Sdcnl$Z3=v_k4l&Rmv^HGtzrqcK~UL#RATCUF!2i|fLM*xy?A z*){_{P)`fXG#=Uq`T(((Pz%nw!L|_t*&f?P_&T@&w;kJMEyHRtXYEC{7SIhj;ew#& zMuIsAIArJpt~2*Gt+56}{jKZaC71{JsmN=cL&(SJGa3Ep0!}JDoqnB7r#H}DXqCkC zL{H*Xe2jgEy@$0H8flwpUJq=6)aJ9$D{v0jX)U*T+jO?C*k;T=>?b@Ic@QBK&&61= zr|su$bM4!$bx;7vF-A)8Q4_)hFinne6a{6Z2E z=|nN{6YeNs4PhX5AMOzL4(2Xy8Fnq~hFOZOfj?r35TCXp?+#wEEyO&w8LS)aV=a}? zY0C(x$XpE=%}iq(u+iOO<_}=nLn9pIsK7)BSC^M{72$L;v(@%UM%~AV1PK7FQZm5r^1!=p^W9WGyJgD zR6`tbkil-9+?n37q4lVKt@(1B8R%U(&rHK~HXMih)_m^rv@5C(VotS=YP@T_UD6JF zSw=R5G!JP1w`qoz$+%paV;ltAEf|lf&~d^NVLH1>#I z$c>Jhqh=`!riJsTl*67i;u!&LX4-C(L-m=qpE(-&kEL&KeMh>nW-Wg#ZPr3l#;JZBecKX!CrJsU=sw!`k(tLr{@ z%}rlecDYenT3=mTGo!Yt$-9P#sKQ%i@V&BA@SyKioQ9ul^=^krIEa{sa9s z{3l~+<|6kiev}cMv2UdFCZ#O*UN&pPxqq&#+_ZN4lHW70%&VH+d(piS=P?U)qX$e= z?4$QfaEsP=mVr}?y>EA5vAOl|w^x6LW+=;@+Owj1w?Qq^oVRrgqX#xUFu*B3 zg@vz@zszd#jy6Z``WW>frKTH17|ir~4B7RMBRZFK^qUpWR$vxZ`(f=-OCjL%+=7F^l6?MXxqA z)*p_mDHxlS)m~li9bHj#w6wicothjru4F^*g*e&!b9Y8WtLwAM9!9MDZ7MIa@l~F5 z68W;rjh<7cYv$<39$$K9+rtgv+a_d)IdJ9da4(B~r_=e+1~uKoLb}&az_oME*-Wu^2Hy#i@E|E2y zz-I;{Z6-#g$DQi-&mc}w-zgE(Lzg~Te0pl`(kCls&)79%YgfP?B{+jTo(JzOk3Qq=Mcd#kC9`iR4iV*{LI30 z)B7yHvnYPZ!om3Pra||!Kl@!^ckuoin#C5^1$`UelB@e(D=yW1>G0`##f7HIvSok6 zYW}TWRlT=%K}(+ox1WbALo3t2_Wd)eO852e%dyXUl-w*I`{&cgeNURo6KIWNi{y_= z>gSiFOrta6(h>(Oo4j!H=?Sx^_^$Y8cHqeQ6RIZ9FIwGyS?03X=P`rgdj{<$m+JSZ zqGbIP`{d>-w>MkQV)-EbY^*50cUj zO%KX)Uf+9?^+zqc*L=AqSgbS7K;ZT$#xMFw+tuB6^;QjEJt2Pdgt3;%UFI&EcxYJ4 zq_xBH^8!*oraVkP7y65L(4f%ga>m-%)J^}y=Fd>N*49@Z{!vjeU3i#}G_=$Ph<#*p zt3Ndu6kkMd$_D;@QycU9P5F|_mXES82R=Xlj@O*8-B*9@$G(;WM3dVrRur=>HZ<#K zw@bafx)qGrFzMW++oPk3#Pj|g?NM0VXHrjAzaeQMk>Nf=c;5ro;rAt;;v{jL(7RD5 zSW_MIMceqLDXoPN{rr2mX>8LL#UEvlib3$&U{Uk2pQ7>((yc!ql}vxT=+l#iDV4Xr zK7VJgP0{yny`@@iILKuJeupnl9+)~a=jPxagOf+yoG@X6_t<+QuaBQOSTN{T|0k(L z#@&dj$oS|;l!ttjyc3ri{WQZHQr@3zDiw@r3>V-1(W#+Pn%_*dRMl=2FH%aXmee3Z zjmWLy{m;(j2fi`oGs`mGE%_SqeR_2q|C69pwnuo=dDyKqtW)?CU$0K}UH=_WI_lw| z`f-9WtSKi(=8Y+z+*%;)6_8Qd?q!!{5$C)+;69c>meaCRaIltaSSt8k{k-LE<&$4t zs#&!IL=R+=iYm$A`XmusF{swuc)zx|e$bCu6#>nkKJP8feAoBuh5CK9ZEC3BpQXzz zqtL|L(lb4emIyyes^%dMb zuyx%Gj;0}Er%)k!+IZrVP&lMk(Nxzu;m=V)Pf6PbwN%?QUXUk@tvy}wPu=QfMrV#_YIq{$4{JQ_w#XY}%Wcpyo@WP>^#z&0L=))eip|icm zw}Px-dxub;FxPMXQ>=H4=hZ>Vfx^k68tLpZV{>-X!pdUZ5qMugs&7|+k>6Ilua0U7 zZPC;$se05js_ehAX_fze$gJL8_37u>sztvlTSmxtYai$b+D7~C;T??~k~F^Eg&xbh zEa|&w*sfvIir)3k89i!P-=5(^(^9@<7NrmKDUGZ4bTSLMuhdhtDjiQ#+E`puAm3Fv zSk4uA{rRlDS$|6KRO3;1PY6TJaw~TKiL4ym;HWoC zv(#Cplh!yc6Q1QeudOusMDpu_KMD%^H4WS{Xx}(qkCVe+4hYEE+52AX?$pMPA9!bi zyFtd%3xBS;BmF5>snhCqH>WF-tMU6oI zskk(~Jia>iOG)+Z>RAmd_~+|OHCIg?@p^i%Te~)k0^>6GW~n;eA2PGYfPwu6G>ssN z{_9d$c&^8_PE+#y!c*dU#<;l*^GE z{MW)9)tROQ(QA2e&8Dhs#h%L3e*zkAe>PWcsM%Ahu5wiG5ndNnNX8f&s1&!;M5lnU z?cc@x?y@pFsIz1cQD_`2?#&)pHms=2v_4}yPf6O8H9SZhsR-JK1#%0F)w=G+M~c|`Y_(c6sYaplmmU<}mF{bJ#~-V9)CT_TD@rf_RxWPX_~~R>ZQa4Y z3+vj{qq1gYxOScWAQ~R<&|^{5rH(J!KJWQAFQ$M$Jg(oC!G_+#LDPmV%jn)MF}-Jp zqe<7iZu#jwbI>p(s`ZcZi1x0^tMPI}m@2wqLra5fPt9GGLH?|9o9w$VUbt2>gN@;Qe`Q6jS@2nb zHPvF#14s$$(YyKJ*#Tq*nT(qJBJGUM-IBwb8(+V1;>)eRIIH!d}8o?);QKGCf6FE ze<;lq*GtAL{?=F54U?X1njtwZ?b9$q+FNm3_+4~DxS>fWruivliyAjnIsQDaUtQ|_ zp|5%K=XrHHy!!E$Jl1(QYMd`zbzU>V?aAi0zMZ1F@6A6xY*XKuzCJ_84Z1tPKlgOE z$6fX(%}X2=+|zfy?*-0Xi4D7ir&R5PhzyZ^Y#t+# z%f2>j=DDNR^R~_*ES;Lh-$Er+?JAPlSE0(e~b5uVKvCJj*?9h$@ox_=wgGrO~ z2j>6mOb(pWZ%N;meY+I+_V|&rJSQr-U&N%COF>6GZ*osLO6*b#&#*_K6-^NC5?878 zjei^W$se^0k>@MAH-8c>Q^$&q@T-Nw+9>#JbARSFF0YQQ=-SNvwYO}HU}m$lWvTSD za;!0xXy@AAB{yVaIIn$V=a(IObzRcqb@wfU1{U-v*x37J_jz4ECbcH{wZ9wLDmuMn72EZJM}3u#GniRhVZP0f&o!lok? zZTQ_Phkws&7*#u|QO~ca>n!eQ@ORF)OYxJuTEF=A17qDftjf8W-M%-{YjU2c5811r z=cl~*th|i2sax6?L=Zkr-kUt)h_|->trc3Wp++-H7%Qk0t&@IJ^$@D~Ve)RW0kSID zSkX<tlA3@iMt{w9pWj#pT6Ei((bT|>fa~Vr+vKws4mR{-#={C`G z@od#_*~*sE7LK%DK3+LWj0w6bwDSMNI-#*)h0r11TwPSPy$-2q-_WP>=MQ@NsO6}FjoPW6c`CeOk_bMnVxSyZaB`Y(e!`imX+RhI< z<>kx%10&hiIN7@1Vx25O`&4yNU~c>--m8$Ro{Ob?PuVWzJ}FDA748w|NvAZ{)HOH# zs!MPDQ=|H|v7%djZ3ETxvw1Fmq57>Yfl9<)Vr|gVv~NrPpS;z5 zWW6Tl@6I}tJ~}fqaYW=Nzgga=yw~ssI9Fb4tGbgj~%!km^+$s&bLhKi;M&&X0` z_43)$V=ZeW5;0HIzWGz#HvX38sw&;zpH*)4Z5zJVZY(>}l&v|UJF8uz`$KQ{S_Pc~ zBO`mo56$S%bx`iXZku~W_Hp*e=+Th3tm~DWAt{3rRz@xe>l||2-3wpfh_q$eo)||c z>zkj6N|Xo{gs>zZJ zYwjhPFE3HQlD%y1B3>?6iT^efG+?5G{My>m^3_#)n-BBHHQcBW)!Edm_49S340CLr zE_Zw*LJPxG;floFnem-@T{h>Q%p>x{dyLB6(rI;eX5D$oDlb7H(IpQEgIs2v2$+M+}~rY$8p!1;G^-{Ky;Ks8P03`Jb}Cu!r=lbiep`%lF3nqTJ?74Vt>vyV$OOfYB*9*QQqT97=pSdJ;Oq#Om>^y$Apl%hpCpurr zex3X?{!KgAHdDez_;e<^aOCWKCe1WM@loZjU2XUxZ_}IyW8}7J-l`-5t%NVRDBmiN z6TXq8w~#G!n}h4zgf5L4^{;Ar|NYQ(Ry4FFSAGS$eXtI__Elp zX@9d5bEo7xa?5qc*L1t@@{d zGNc>gs3NY_??AX;uqkF-Qg*s`*W50LI-Bw$y1(wK&2{D+OBba*Y5O7kQj{$a^SR6^ zba;_6a-}U=6CjC@RVtENC6Z%&w&c2awW>g|w`H7^D_$=-BHAhN6|HMr-*lw*WmQRY zME&r(Gu6_Ha=}*l8^s@)R@vXeV$JZH&D-xWCG1Sx$TXP$)6v|C=(f0-{RZCYt5drA66FjmMJ0UyJ&syMlVYk5MABw zcG}ylIl19o`eaY-9MoleTA%dT)Wh*p6M6;h@k|J~=rP>&0yS8_*R&t*L|v3t;RK07 zamYATHdPQW{VwLVI^>*|iP9iZPw{AeJYOvO*67)6uU=d&5{?w~;(u*OYaFUz8#$I? zM#5HuNIbp66#++r7sM8Kn3h%CDKV{W*7e*DIT4*2vYw^PN&AwpI^=XzZD@txcMo`@ zLB?TY=Vx(h5?OIxdZCjqSgg4hV3F{Bld~CAtAMu6F@}`@jY;A_+sVU!l zg*@k464)!yGcYA;U;M_@8(CiI^%+|`@6KM6iR27V-=F+EksV1!&5bnqfA@_f^63)n z9IdlG(#=zODc)&|^gLOlG)|1k`nOu8tHj;Jf|gsd3*z2_e_Of>Dw_`1u^KP)-I{kc z619gKHb{fjQL5RB4!RAFQucVB)-}{;SkUh_VM!kn%^km`%d>uFkI9vHs!2bTwJAx} zZf5&|(Z!MH{YzW|-M(_y;yKiOczgNYm~Eb~o**riR7n=KhN)^rPb9a6!Lrq|0AYl< zxaCgs(D8h17=B{#WGzCeynI-8}C?KyZgzh zS#3IB%=Yi}D@T_#IL$lbYtpcG5pmogLBMeTt{yjBxp|y+ZW1F@$$212uZEE?cSfNif6`G$q zJy=oRbin(dp%Iy}Gm?TcGBO`$MrIGp5p}A{3CQB5PD+u-osN!=`7h*IKu@xC~` zdN;@M1#cQHIML!Sw8`#NbJNqy`nvft? zCt(U`b=8&`mTM0vEz%hAN5vlAI9M3Tg$-5SQ?px;MS$-{QAx%xaq3 z!pP=mPO2wrUK{_ALF@&tFL}$o&Iehd(FFhY>tWDqT3SiQpR9@LZBrvt`g9Pt>l)t} zy)b;9-%SsN`xw^|taw^uo^9G@b(rj}iOM~)U&?Szu40Vzo5)qvM_M51BH{~o@Gmqq zHCHtU^3OKsH9V=`-_)_?xTL@Iqr|T8&~30~F#QlacG`6&^hf7KU5+-#bxoX`IwWIQ z`j)gE>HAXE$y1X@bokbGNz~Mk4MA`HUA)$F`yszb36>8&)5zwl%|uh@rn!QV zlEKOp^*YTNL%6*jdCfh=`{^O_w}(xNtxVX|eo#`Mlxyi{GtXvhN>iom?3mY~UAxw} zeUS@-WBu-X9(3op)S_AB1lv06L-R>J-fB>eSAJKnQ$CYlmz)vr7B3P%1?^VypEO@- zKGA%unQUrpL>hICmHfrRO0m22qP&A5V3)KyI$^PY@>R%pmH#{`@XI$U*=A@y? z`;u2B(@CjGpAr+>7sVAtjS8C=B=f)LJ(ZWpeuQQ-kEjcd^A=M7UhAUI)@xcZ^%+H5 zWna}xrH6tm-6Ay{lNlUONYi7vy})SE}>_f36`nG z8hH7L83t?rRew`&Yn|09P!Cl`C^T}5T%hQrh?5=>zYrsm_R=9zL^4MFSJEtBtWMJk zwMz_hEgmF`xXC8CyWHRV@Pl$A+D4W}b%~wbc58x1J8AsIwz;wIqU}+gBOe8e{k*-e zxvk|b<}Sn@(o3Dm_JOuNmUBjhZlpF^i|RUPE7TKIE~*@rQe{-?WJ6@%rMD&J()QB# z;_c#bVv9s2Z&nUcT~Slo8q*%fA1aawA=oaj+*N+IfU=+)VXq=Q+61%}#eI&Ajhz-d zI`(aww~;?Wmjrk9m-{aCX?EMnO(T{fa%3gb-_dMV8)^-|jXXpD)*GrGs#KLqC08cO z17#beQPRt@CRs1ZX7N1nItd1^`nM{&DNm~J>8_g6Y=r$^r;te_NZvCKr3dYk5YQ(y zEsBY38?_;NZp@1|#y0C>#F6*H^F#jz-w)RK)p(wC>&5NDEn_j{%Ye@$1b^SeNEb;_`6tyD z^?uDEEou;$&)N@BCzv*PM^2FIRIksz-~6#)mypfjJ)+M>mqvGtJ{8q2x*|#)F*fvD z(E0!YjO}{uA#hp7*^i^xIbDp^;@-C{aCqG?kPVb{V7XQ zjFWbej+FM7ZdB}4tWsQ2RjJQwM;qA|t}VuCrgO0goM*h-?i;)_{Zay>!mfv}jGP&@ z6>d-3#H@^-6j>QIGW1yR+#o;yY_E6jBG&=jS?qDxJGzW)u-mN~%SzJ=gG6U&{ik(z zYooeenXPP=8{}=2zh!HsCaIs)E)&U*%Ua}f6u~M@E3Lh)uP`-Q^PC}!3%-DL-zC+3 z8+4uh2#gKs7xpYHA+j*KFghmspGZ978w_1O74j9jd*6CXJ#M;LxmrSlMI%e8Rn8Z- zEf$sOk^$4t)@fQVYnH1|DxHdQr9`2WU6IX~4wlh!mW%}Xm&(tpcBs=drP@FG>!!iB ztxh+31M0()ab#|^SDDXu|E__1gKvg^jhG$jh&UB~C1OluLd1~J$eqw*~A`QfG7nH~K*zQ&3tq5oct`E5yx-diZ`gT7szkO+9+14FDgY!o$8zVgO1d-On3(rhz24JCRH zJ+39yG3rWnca5tmUYVufDgLN_D=#SQN`F<8=8yIt?P-0n(ZdpJFLlhPt|ATi1J+jV zY`4iCAH660-ty%KkU=v-UWUknmIiMN(F8vZa`-EJr}_5v`R=*Y?Y@hH)4(dm{m@YQ z0vYODZ`*6}F^8FM7?$gTw5wY0w7$~tRn5voB}eU|7ATe~hbkLXt6R@$=C&s2%Jq5X zLzZY;gX1W*7I}r8UyvAmr23DgB`)_pfZ2H|1JL(-=5w) zk2trvyx}hW*vYsZ=CmKA^o~ONOY2BWmMO_#)!o*G>4?_vYCm_ zz~g|CfqMd){9JrmJQqP%VLq>vt77#c8nFrl|6CDLFU_03! z!E1E^?01Q7s&=1thjyf9wpyUFsupXq)T>pU)IZcz>tXF(EvY+XSZEHkuCdK?{2>1_ zchUEF4|cdqIj_!brN;!XS3U;6(|%QcdjGQl!vbdn9Q0H9vi*{MeZ22^JcW11S*`;) zi&+&|KQtYwC%-uUvs<8JV7+Ohp|8G!UaVc(x?eL=Jwy|w*{u?Q?@H3lZq04o(rVOZ z8g>|unz~q0Y!jTTsUNf}x)Z<4)MUk7=@z(Jj&@w(_)3wGMT(+OB%3 z=4#EFj+*q=tJ-XXz%avj$MnZiW$);$BG1qoq#pNWg>c%s9Oh}=xgLCvVlUd~kYAqf zCZ8zZ!G2D^*}liUzIZlz)qCE9riM*iD>s+pPyCCK@J5kK?{@sKacq2eCDvf*qW?#C zMgL11-uhQ_L)}grr47)OYnn8}be#ukxkoG_0!E;78=-PacA7imwlVp^d_ukEF4qdlxUs{dse zYuayKYhGowI(9kMtL%lv(7pt4nTBI%0 zC2PlMrCL-UZQN({H=Z^Lt@mwD?8}@VoRQ2tbQU@UuOQk(cghl%34o(@?)e_yJcdH2 zUx}Bi_kW(RJY!%Y8ppH5{UYz3YXL9RWjp5|mM4A+KZTy4XH!d^tKrQp4gGGfOhu+% z2BAJdcT&4Z-=NI|UAwQ{ua7Y(_0@*Yrg#fs)!VAzwO$cDkd8s7qYto)tfTA!oGj>1 zaqv#V+tXWaCp`vw={%-_w7)&Pz3M!=yKiz6ySch`aaC|Pun)4IvDRRd&_t$%IztH@ z>ut-dhb@?Si0QK767=(R(AQ~;ATpexd!~2jiuB_QO~wfGN_cU*!x471!=^#r3`GGhVVs8T8xj_xRv`*t5H*i-*;HoO=fJsuc5>{R5QyLw7xN7Cz;L z@%XNjT_U&-*<*=`L?FHb>CS{vRnAt2(e}`?%BsA;^*sG>olQ4Rr_$#c z`x(y}ea$?p!1~61(Z1U;l)g*XF}`RwbQM9eq^v>gXPj>?^IT(jIj-@%a`y*r7Pm^b zZf+4CyP@Z725%2fF_#>1uT6jGqi=45N$gtJeC|ddgW%hEm~7G98OL@sao>qKsI~4&s(^Z*$ji zgLzYVYk54k9ngie);-cK&}|s+G4C}`=SsNz;HaS|={IW-aS)w><)H<1Fco}{Hjc^UzW87KX#jfG5Gh73BT1rwCUyL!3~ay{$P#2v}m z#;#%WiFbG)76IHH59uvr8N@~dY(uOv%RZCR*b}7Ewi17w9u-izqM)whI^HPl-ir2|I!l#F@xd!}`9i2`&#^8eQ!!gIyQ8_H)g3 zmAIt3ByiKXeK@1psjR=)dw9RAV)oDj$Qb8M=x$nX{bUZY)S5P#%*OqO_r|lvb;eXv zxaqO^s3qK5Y4x(lIwv}XVJV>N?K$Tyw=a6N8BPtaWS+ zJBag+O>qWuhjVG}6mFu+PnS#=H;{LtOEq^J_Z8nst2XQGYBM`i$hm3#^B zyZ_hKrkAE7czaxA@v>QMSL{6;hn!QXkCcP{hY5mL-v_Y( zdniz*a z!tzlPJoQVm-nqfi(f-;>TRbc)&B-wTXsRj8G~A>!4Kt6h7%fMw70{KXbQqj^Qcg{v z;k`Su11-e5VM%xf@thD6M?mg0_9OOB_ICCR&VL*+Cx@%#T<0>}``kcoB6k7z0%s%# zV^hQcmOCNC!qJ|Lk^V^?g#GSopJTgcZMO2wjiv+UTGK9bceBbg#PZkj-m0>~+jDsJ zzlKaAk5kRSI)1u z8e*@qPj?J*+MMS>;(ox{^OTu`4g@WY!CzxNah&);JS1F*%ft{?73(wWJZl*HHam&q z!$CN=IG;GHIQQ9A>{QN7_AS;B;wU~4Pr-cA5XP16Lz$ry<(hrI?K#+WJL^P?(X2De z%$MMEjJ0Oj?%KQ@Ct&3|@+vip{zac-RLD_OfR;diLmGY!WIlp7;%kY6gpvqgd9ucW z{2ca5wkyZL7PCD#F`Oj$ZabS{^<{k^x)8gd&+rTKhq*zc^m&K~4fcHS9>1-vmi6#; z-ZCFLt|nVgS*JlKOG3zyoxk z7L$FQzo3uws7+#h4qZ2umQ3q*=s%iii?*+aZnUXRHkl6{48!SsV2#>>d_upV*U$mit0rsJ7)uqhTAvVR$FgbFIk6JDQk+Y7fiIk9lIUtocl=$`GdMg$3hQ&3-TV> zh2BB~u%Fm&YykEY`vz>j=aufWd`1;kdulNHXo&Qi0Uv7WN@tnsX#un!%G zJwPjV2F*feB31NLue`Gk8f^EUZV`H%ztQ6~oNiZRH4LgU=!k6O(L?Cew?6;6LpLLCOfVG+B zWJR);5qpS!I2(V5;ixZ?zy#4FseNP*XJ<#X-PLZlmD?Jv<82ph3v7$*jdnliglls4 zA?H&c>F4w^=6}pzr_k@{IJ63V4c)Wl*a++<8j6j=FiebH!2+;UoP~G6^MDv_4Dp_L zK)AC`v$9zg&fizj`&Wvv5pcGh=HtNq7yz5FF|jhQ_&sF5n4mBp=(*}6gwRD7)OMCGxSy} zZJ+G4JhgL)hFZ@>WhgG z0{aV^pMn3tDzJt4J6wx*Ck_!d{36s}N8%G9m+pcUV%w2PfT0`c-INzKlgxE4a9(gA zj*E`f(BmnG=|MTp;m!)^Xs(9-_1^R+W;>IFOhC6lkK1|F2bH6*(EaEFv^z?m!_lYc zek2#zxXvSq&~5b$m1Fe$wHFl z?BSdYUAe=Y9UNC2ZJmrGf!qy!NSml>^f{W%{0j_c+o4bDCh`Y$z>5E&Q_#L>Pjoga zK*UHtG!5yCUPSN0zV-nS0RR z^&I-s0IxGw3eoaC8 z!peU1Bx)A9k@O;?o!6ap4y8lk9PN}ir;-=R<&+P+ zji$nxKSHJ=RY*_ht$KxMm>&oa{f%6JotL9~u@X#)>9Kiu8on1lhCj#J;G3{=v<$Sg z3-XR>qTAC)sIJrmaspZB9OMiqr#iPfv&mP^7vwmYV#lTT(;Q|ra}pT>YivhvA10(ad#qsHjsbG%~TgEjDAL^F>Qcpa1gNR zJ%N})g_NTokoo9p^c$S;DP$TVWTqiTh{h)(>A)d(6?x7)gb5h?kxl3u=#AE(|A3wE z$GYQk^aM5=8-#jeEOaU|2iXSfcAJ4o?l83kR_aA&QDx*|at8H@+(s4B!|1m(#vEd< zGM9lbZzb{%>O=z3D{>VB(R5KFM@|KbZ%7ezkyk zH&6rUdV5Ta=3~X!ZkWk&1oScc0fISA3AT}z>KSdOn>AJ zaPYaIlhB)BWqq;hXgSKmCZZ3}bo4gprZ=)1I0ciK%P=3X4}FjlQ)cP{)ty#TNpvNx zqNl^Z`U5x_A0eseCD`K*px65#w-dweZvh;X1B>GsW(yO^e4xiO?u-Xa{E*YTfTNd# z^hegfPcg`H;211Mo6vRWbMz}U^XNfzEUlzg({}*5 zvY777Cngcug{(%JVCJj^`Xe&{J-dQM{Q*qt4W9EW^B>a7$Z^Do=#lm?4Pz<#5}gY;Fc(b%L^zHV0zczx&~P?mh4}I~;KmAiCw-p&4V|>E zFwN&OFi{Q%-TH|5p_yoZG#j$_c3@8^dJR|+bHJh=GDDec`1@Su3%!RXfrSx)6IX(d z5&b{o;yUC5QjIJGD~ktqz6^^ggQ%|* zv?U5yDks72Zbg5=B#-gvcjPR{{tP4;1?-_;ndiVe8NpyoA+SE)fX^Mo^aReuK`_ni z0&^cuO3GXZj5z~Vu0-6?-DnfwoEX^*e2#4qGeq%1rXT3#9>$C54_uZn={d}4h&CPY zvZ4U6!hpD--snhFj%)!oNFAK_Z8(8q*sm5itCg_R&*`6Fop0%hbUdto6;8^H5z;NFH(k zaX{?H2R)sFZbkD!$6`63I%x-&fuxG&SeN=CyJ3Ja|5&P-t* z1IK12{Om};ytBw#NF`84&6mav5}(LfWF! zfQvH+yzvZR^lGZOeu`++942OiN3V5I#I?BNnpj+DV9i4o{T$OD%F z1L!97EFhvQ#J)d~!^k{f`+NzWJCylDm(ay@27EPAaw?UUgO5w1yU`Z`69lw3vlbAz zib(*CN{5-V13=y*u=XirFR~HY1pK8_VDEP$!vTA~!oMqnu4V-?2%_Af5UUk7~a z4@Alo0>0#eejY^r0nc&@&i@ed1n}Z1Xx{~dMl47JG6`(A2f7##elJ8i<$zt6LGF&Q zHjdr_ZApRisf7+`Z$<+8oI>}eS@a{yLLH$V1Ec8!iUV?I13J6_bm4*pENAWlDhZg* z$THB8V$jipu&Y zaJxQXb}&9*g?H%fu&x*Vges;`$X+f{&*2wI4T4(j2>J>jlMh(TOz`tA@M#M`uM1%1 zTgZ8^Hw3%^g`@zc_CSw=Z#WO0eKAU*M({PCAgccixw9SgY#%xdEeBlK1u}O;jxi&c zbVflhr5Dj{>66g${+L<>b(KQ!J!-0)GEywMBfX3kfxXw#2Ozq+0zK*<09RMTxn4vn zz+z=cI}`)!xe1o@5F+YXs3)d@Nr7RQ1(l*6SSI!iy^H>aoj8D|pa+pkh`pTPFUCV0 zDFJJCp>yeAm=sY*O{emy3Tgqhk-AA;rbJW}oI@cn+m_KS5I>J)?!lgyG2`KsH^V-+ z0xr|Y8t@}a&=#;DrmzXZ*<4hXbN@*q!*z65M#?AF8ddl(f_yGEan^7 z&rZ6UBIpLH7iidgnBH-j>P@YsZcuM2K84Xe=)Sa#u7~w6!P+HEPnce_8cz8((g^!J z8hpY>(56@DaEQqBuz?tZZp4NGzwKyj4CV{@SRS?mBt8f+Xe{~!n4UXB#=Ca`J=H3;AdB(Cqeh8fsIH2qv8Rf&(m$_ zI691;3e3H$C=~QNiHf2YQk|$t)CsDLDxu=(QSjNgj0SvQ30OEAEPE0}O)h8}WDMU> zK4|a^kZ(Hn7vy}4-Nlw-53uvt8rbpa7zv);4eJXG!cpjCu)ZkBQ+9!GFQX5GJ*%iD zu;WB(Gc}yjlM^T#=>iqgt}st?BlQTr+S9W@at@=WQ-LM=Et3iseHPIn!(bQpp=`_x z)_n(2;!VtmUBXmY6ZQnYu41dO-cTViqfh>qJ3@5O0DGN}d;@-FsASM*K!-YlO+AP6 zjie?}9bg(y0Yp}AR0@?2yS^E8u9C{9=h3UdkHx`MrY6W9CPRLJgB@*wDu^2NX)Nq@ zGxi*7#k}!asKgi{H~atS4ZH}Dp7VFJrq zxSQ%r@u)9k8>*I6k_ps!svq?a)e2{@ls-UD2Ok^`o~9S@JzoH8P62#&pnWh0+5%DH zzt{@=I~InI!{1}ExCh>V-Nqhb{V*vy1)C4K+#<9PxdI;G67zt8=$`%$r1qjeLT12& z3B>y`Uu$m35bKLU}D7t@C~CN|Gx^_y9(=! z<=|$hK%9Y^S6_S$9srpU2mgg_{{OWKau~ zxljYBjxd2JfvO~*lQx*T!-o@J0DpRg@}l2UrvX*O^j(P6aPU}_fQVaBB}$@4uwU3^ zY&0HeFn-wbCGk`x@2S4`)Y`+G14mN28EG}et%xkc!Fu>_^kTL9_ zWN`E0N}Yq3+>gmeuyPftCxfYpR1ZM&Fz}yUi3V3o8RZy#(T+J>Xr%5M7UebN&i@*bRJS3pIu64|`ij zB9suamCxio_~6r<;?wasybkp2UqCek=7dB-l!K(RQgJ@t@m zB7c#a$O~}VXUWTCJ!yq`C5K?@mWi53@1s+gQGlaE!8`gxMBE59mt&YK?gdhA#dqQg z{5k#>|AMcBT2~+V(`VQjYzWk0-k^O@Gej%V;OjpFils8&=~WN~wWrHLw>MA$Q2DWu zN6A~{DRMr!1HL;?-UTh!L*90ZdPZeKymF5o4%!(B%;q+zgOq{h9t0asgFSu;(w5*1 zUXA~T)pz1!;oNoDHn2Hw>=x9f7^uLmgb3~r_}u`e8Zxj6bO`MCVK~o`6o(Rk#HYz) zP!m{4t|qsUC&;U$hzx+!xCgbpArPm=K~#*xzhOj{fG5txj$j%r5BSHQ<2Ar(X2U6* z4^p4Q|A8u-2{3ID=7xQQb7mlNSpXdIzW^0F!b#tyN78|`0&=sJR4z!}OnxJ8kju!W z$@l@N`L!5#Df{p-hWdh#y5m2`}Mfax*XfF8Z8{jQSG8J_DDtU#>B`3qW z)5y8xO{jmoAbU^)!GHGz{~@8LF~68K5Pbzh<~162TZT;nPW2W%oDksM2w%bidtQcL zz?b1k_&u=dUf5Ic)G3goT!Sn_4H;A_BZl4kkIttV>OM7w+6mr*54GBrAn9hZCpiRG z9!rh@UHb%8pLu{ajnriN9X%GZ2{(wsvZ1OMjqQbsWtN}!uL)B2jAa?VEzZ(cxn2er8X%ze;HVQuh^(0Rso9GWU^DLMKOu{GGg%1F$ zzKjjW6zCaxNp-{!Uj86ar ze+IFFFYNa?(6K`hr~4ppz^;5i<{R_^@Y3bf9kAK{P+R;+ULyYii3?$_Z2}ojrjxxv z&!3QLGM73Eh_VQ7)5k$JGXyFwn^6Lr3+UMgKMHysL*x>3Vf85>c_^IrQ+z2Ngjc|4 z3&%?W6ie&^Az=ma?F2@q*dMr%RG{(zsj1G*hX^dM#uTZri(buPhziPszO0$c(o-41&P zbt2%;8tq6con_Fz3Q31CS9 z`UP2!^nxnS3CQYj<}*YlV?etlfKwBwZgAK91FWiu98JcO9weI#f;wXwpvqxVO!kL3 z#S`*gJzdPWLN1jKu|XEZ07-zCt#}I2o!A67GHZ!-!~i0WP~sQiw!{k2Q2|70I{>+j zkOk#|c5i~pXgni=ibF3tfEH3;!DA&uH1-BCa}AjXw|qV%LE4?3aMlB%l6xBv?an%S*H-Ka6dK={N+1f>S=~*%LJ&@ zTmtM?GhZQhNeApc0rAE_+Lb;_?ExH%qA=Cm14V*?96Gi35sWh06I!x&Zt} zXUH?Qpj?P@op2lV7j9hBiJio8VmEP-m`&t?RsY6UK=o3FO~K-@mrx~&M4v)D(-C>V zY=(-SE1dLZIB6qggXp6VM1XvVNv^>D=97UiDb@%#egu4mDS&OCAX3{-`9n=eLEmJ0 zAm1S-;=|-$F}4o0+YR=3G;tm7LN>r%cVED8KKScSxBz@O2jYVrC>v@mlMoL?3z^zz zs9BT%j!lPq^$*}TW>XR1rJs|<_Un9rZ80nfMy=HhlBF;eYRI(2ZXoJbBL*OM649WbXC)D;Fn*!0wT78}a~ZJS?ON>QCL_)twN0 z7$c_*ny#}doKzx3Hj)s#?hsZH;HTT?q~$zMSR3pfsTELJf|zR7gkO{Pe2SBgxrIQ-y|jxEbAIn zq;lyr`UZ6xvMLjaQfC3JN0F{zRT@}V08R@2vG)+VJxn~=D+G4>vCdR0~T|CA&^AoMDsBOq1j zC`FN`pr9hX_zI#T76gy%k}R2 z%*6NipZUx@_qq3+v-e(m?S1w+_dYW#vcB59XPurYNZ!A*Lg(?Ji?Mo>p?UE1w;B0E zgZB>HMLn|RzauQ5T*#u*BfDczR#<5)NQL{-~a%*$df;Pnmy`2%5-W z?%kEV{d;5|F=`j;lV_>WHtN5(@ASS^`tD>`qo?;?=E@H;Z(PjUZapIFR&wZ`L*rOk zUQczhDHXzB2YyZNUXw^3ocRL$o)hHvq#Au-U}xry^@ko~&+Mtr57;}nuy-wL`X&2+ z+y83+(xbl1obgjUy&vp;|EMRKOFqTg=VUncIQIrOWgqreu=_1`ZI9_}$vpi871Nrex8hVG*#Fd{AJatM0987yaOK}IF!0!31;%w29Fr(W&OVj zd+1xU*7y?l_CCw{W-8U^j?7hu@%In68@0x$DXjALX0>qzI|y@d=X)>qBCqH=fJ?s9wdL5IqX7K zeS5HzunlwN-qifxXOHzyz3=b)BOKeGb;g23)4o(hJB|80lHX&cb5Q?6{SWb^!6JQ6 zuvfPbJ5)cy(+hG};&?p05_=c=`frVMi~GMr#eX+>ZZ)d0 zli6v1n){+#u;cs;p8hI(&TnyN@7F`S3@twN9{WOHW?$}AYJpP+J_E)pl2PWPI$DKU z`kaAhsfsS=uIo8Nt25{9(DPW&|FDKXgq6&8@aeJs_frXef*iIfGxQARu!C6JJ;7ee zI(^Uf9>=P33cHtIW`BHg=Mr{wH{ssGW84qfxAF8ftg$KlgatbXrM)Ywjg_bpmL4#ey#FVxuzFZytbxS~Qh(me9rP{whj@19 zfBF{eyPSRY72()#*fU;&`yhX1_ha+M)8`L;gBj~R?s5Kzy1sw#33e-XrpncC#g{wtU-M^S|>KB}Kga$WzAcuHyv zx#TN-OR>s1vUfeIvGaQNWkvgL=W0B?4R^_2X0QE-p$&#+8+wQenE&T9IElzQiPh|9 zkh(4_ti8x<*AL81&2-z~8t`HzR>Nzu>%28P=^OQ3*f%G$<@r3J&@*Z*&w-6*MSUY{ z%FiMBdUiyYqwYSjcSG)8UBa%z2Ha7(lUi_Z?y*i~b^Ie{>P7JM`K(&kr8>Eu`RRax zFOsu2ZPt}14ctK8y(9Z3%TxU>&^f&`zUP9T_fzXE$G!4*`##)%A_z}J_B%YG@i++l zq+d@T-NbIB;_B4iPjTn$7S^I$^o-?6o8Pg%Tbmqu4;AemLmwD=js2qU4X#P`{Q#@y zA5(pAPY&Oe|1EKRQlV$j>KXYyC50#Pe%Xi74nk~=cF6D>VzxWq>2-{KF zPa#vE#A;_RRvEhw?9cJ#17ij^8$6495C;yu!?Vt>@?Scpv0w8%F@1U8g7Et!c8VVB zf1c-RZzZc8$qc;+716PMoAeDaU+u|G~~LvQNLq{>>D7yH#5d>kvwx?%fVlAuVY_!`*-R*LM8Zc&u6HL*5m1`w|O4r2dvs3 z#O}LT+n$BwPf~y0+xK(!yl3*n*0-7(^sha8vx6`bPyd|z1uGF*H&gfTOiVw=KGfF+ z*Jm&G(Sd(p>G!CG4&?VKtbN{Q6@SLy%Y(accV~0hy$SW$M(mm_&Ac#yCs4jdP4*YO zeG5-|p3iRNr>MD~WiRJ*ePg-r@FVW3yvhBm?{Nog9(JkDVz%6nyBqg5b?90{Q@MZj zLsmPBf%jEd{S#)3L&+;=!muIs=8omQ-WIG8*5ba-2YSxuNu85DeF$~YJe@~}E?_@-9aea^ z4*rz+@crxy-pL;9sRPHrv||P?VE5o%Z2lp4KepgWo$>4dEXXK*$0Xf zrvDr;|2@q9cK_!1`6>4K_u$^fgVcfB_AX34b{e}TEA_nGxvuj=R`QGUyusgz=}mcp z;1TYc?aR|LV+Wsw&wrrW{@K9kWb%9P^V(#Z2dM*p!~M&ncs^l2YX03>>8uKm?(cga zada?yald8;`7zjUiR1}zV6VQF*lRzVdoN3nP0r^Y>DpxPTd2ssKwb0}^Yk&q^b$kQ z5B`On{w-KhzcFw>`_AWr`31Z_0=G9}Pw*k`ah$?a0{fCdcjXSXo(7qpn*N5q>3ysB z??y%VW3tFM`~Qc!cR^N^7w}Zj>V0q1>gznU@Cy6ghjDLX4(@{KKH%Ey3*I;MTjuH2 zi0P}CleQmRk+u8;c26#)9=V8>>~r{eQ|8h8xqo})(Azwn@i3n-znUHIlX&uE8)lAY z`{rks_-g-tNZ*fW-h?NvCNy_d*5NMJmE2p{h`siIaA#wip2fJAbOABFNoO%U{d=mD z)v5dMrbgI@dr0$gSLfb=e-TgDqroKh`sL@_2j7R}r-ydn>BNkbNU7x_bs*9he8sA3AswfA{`?p{MwdmlyGr_;Kuy?MY-U$jtD! zz8Cry>R%t(yYjPX|0-n9C;Kk$JCLWj$5IC#%`*V=_CCrT&M)+=LiWCayX-rXy`=?I?1Ux-|CGrR3)bmrs^+EYBmaW;{) z4fV(U)ZZ&2dsB9Y*6#lR75CHZI3L2@oVmEC_X~Cemt?N|BeluK%+vR=%K9RA4F`vA zrvl!cwdK34bxsEF4-bxKu6~I+=xJ>442-8X{o&xvgLCqHz_mjwkpJGI9=o|`HSU(Y z2*3HO=Ij8E?O(Bf)&6y{cv0r($3fz7?p@B;_hj!m~&)}_r(X4*9WxwS5!Lh8mFX!I;cUfV5l0D>k zdiUcw%SEUGPVc*;Z))E>{Y!H{V_En$ogLwe*>V0HweNGi7s2jzd*4IXpY&|cQzkF4 z3;TVZ57B!1B6blr9a>=MDRSshR7UF$E>0ah$X$&-_6b*IrTZiHlEx5y7jx%uKX$y= zW+!(>&$g`Zrt|dj5q+2TJ=!;|Z(RQZe2=CMdkD0D#D2+geUo9s_u$x^y${o8U+&Sq z!!v3>Ww&TaYVXU2emwLUB)^WQf5!dcEs!>zoH9G}=91{~Mdp(mnI}I-7VC!{S9O-) z>8}Yqb9X}T~8EmhF#~ydM7ZmA4G-F zOZGm43Sl*#d%K#7YzM0T>3CE8-hn6JRv?EjhV84dSF#_gfjbAsq1|~yGlsS%_s+>4 z+nv;c-(WVHtM6mX#%EF;J>5Kl4xf%#;IzBSm1yqP;!>Vff3o?$pLN%n_+VF78ohn@_MS!!`azy{yT0cbJiSEE zi|pYZL@hW6RsT6W_wOXV2zS+)sLb=ubR%wiUb2FO#dzgim|H=grC2{67zlkKk#9e}nd?vH2{VS3-bXNF?oNE|$`0kh>_J|^^B376uwX9^+4Wl)VDY@<*mKH;STNE)FyZFG}3N7 zll2l+=%M7XId}@;OzzWcI<(Nx8-w?P?JucC4`FURn9Oo2_qXn3O}7jc+s}BaZn4hp zJY71OdhCbX6CcNXZ~|7nMP2q$*!-=&@58O5@bXt0$>X_Ce?QL@9Yi*nhvxv!Wlg&V z+2kr}ldZ^Ok8$VlJIq+i49y&Tl-;27xp#RiIrHejQ;6^@dH$r2%)T2L{yz3*G8#UTQvi2Ip{At)cFLl&O zoyS;he36~!XL^=^WheGt$LujT@%u^a{vUP$_ad`x!#ueJ`+-mLtiW+iHksPY8=Lja zOKoyG&-T2Z=THB|8e$jLcXJKB$^Gen5l81B{cK*Z`iXLs&@xNEu4&^xSIZ(`s5LY}PoGnwUPoF~8VxN7B&X1@e zJDrW#kG!PkZJx*5gDUu+z0dXb_Khcde7tX4X2@;PVO`K3!~MeR;n-KvaQ5B@*?HcT zXDTOBVI06L-QVGpO5DHOVrVIH=yO*klw9%#$_TN+Ml}{Zukne=wnoZhr+QHc%o{`;1l@vI%4St6lWxjfV{C*5iQ7zAI`z2%(%~&^*3qQ>|egd=f z;mm^{BzsRIV?9hH-G+xB8GMx|_ZDY0urn*6zwj*8*v>}abUyXL!aU*l19s0R^Blq_ znYVt;lT=SqkxKT;eQUzE)nM3s+!ub7=Xy@;{Q~vxyF4-fBc3Oompt|h?xBrmmi{|W z-tNL(;)US%OM?^0)DQCed3-!OcTqQk*(cKGIc)wI&)c5I6RKmWpm|b;XCQkRb4Nb& za1N0@0jAAMHL?n8j8*uLbn~(Y`XsaIFUck!A)DL}FL&nY@W(p8?d-wo?p1apj;F@k z7>|yoCYv<)9G1U0_$HsMS(vJLJD7bc`F#@4>}^c`{59*sLFSD;m^rQ`mn}wB{WW6e zuXuYhbI3UI=CWA4Xy4q}{2cYc8LWstP8F&(?H8G`o@1{33VUHBp~<<7=you!Ga!(iEcJfHY}Dvtx0i~ojK zCzHwN?wh}FA?`-cK^A+S=fi)`vtys?eP8eM)NtS6ZqhsK*c{3o;vSwXI+HxMEl++d z#5!vRviT_a;2b=^w<JPq?2&m(U_wm+F?xL)bZ$5V=5d{> zJ+ce2a01WZcetB42FdU8DTKS5JoZ0LHo1Y?WHa_cZ)0z6TXOf)@aIS5=8q9s3vthM z6uTI6u>x3?GrJJWzu@lmqtszbA^EG!?3b~B(8GG=(`4c^8>{EyS;5Z}*{31p0iO1G z7tBYI!)CIJ^&l0*Db&duHXJ*Ns%T+$Y|h~Mhow6&QiJ{!25(6YT@+i#@x3rUUK8K{ z4{NG3sPiA^a}Z0h^8PCOOBb=Wdxt9YqwwW8W`c)_>m|7}_Z1L7ulE`v>19|xfaNpD zFpqQQH}GJ~-nn@a_1E-Sf*qT)SZ%C84SF3l$2Z9)A7Kx8S)L^u&+BTu+kq=X4t3>na z?AU&dXFV>UnwrQx@#TBA>e+{R;7`ofQ+pQWS&}`Fa3;CqJ}QP+df&w3FJtqKWY8nw z*fMbJVs!m58T1PFExbO67GRbjd zv77jeM~9W=20WF2IQLzz=LzRgd_rI=W{2aDa}C~o2HBI5{BKr57xKLG-c&_1$N)#e zu^uw$51D`FgJXYXmfm-08}iAA$R2CK>aDV|?U8%}yWQ7!p6I;W znV+@kHhhNSSSs*4dR}LrZY@4trO5s>RsO$`{UVvc3%uV8OjQ3K4>RZOxr0xI ze42I6b3<3cvHkJu=B)76<>#YRx_cAlr-1eC>@$yoUz>10;sEY^{egY8hw=1i>Z%X- z?7~yPzrfdzl1Z1N^7%6NzJJG#!QIGwju?9qwExarx-U=j&%w^~ujsKT8T9mK&s#g8 zr!!;iiq)IItWB_Z2QWAstS=pUfGjeWrzAe!*%j}cNanth>UT^l`!T}xf?Z=~Oe7H3fj?o5V%o7(X(_9Lf*+hJs;IXX|`&y$JiFETf6*I4}Jq3==a zUc`Lz5_?ihq2K42FTT&*dM>?g;`1|;$YkTFZ#M4Pk%h$ib*Vpms zwY%Z=mDDXqQ!lR0102_qK|jW4TyA8y{gbS)p2N0N@$^^l?=D!rC%gWNs7t7JCzC~1 zK);>1CvXI_B{vg?N2(Ydc-_u{N` z&cu5wQiI+J-p6D0K0{yP=iBT}p2Qq>4bQUkzv#(yTk)jX!7%#_R^OL%U-ovaq8J|kMjGE+XqUv1MBm0os$HDHis2`VQoudr;VLlb~ zIPvpK_`N?h?tWf8hC9T+BaYt!C`vj;ioDYD^LnkO%wq{2K8iO2DMGP~)Q5ou48ITzvTr>MXVL&x9q+}0JWaBlD1 z!}mjcPb8yEXU<%T{qt?9!cSrk>M3@QmZCP>ll<{h5I>`ld@%c*Iy8CgW@2f=ObVfRyxpE0A z?+38+56C_f$$x;^4^fNs!hsD~6MUO`@iesj7x_zA-%CyO2>(U=Vdm z0oCkrHJ{}e%ebfgR32fb<{O zZ@h*4^(vpzSe{vGHzc3HPTZw<`9>t)#i!iv!|wZF*<Jkj(s^3w;IH(uf?{eP0nE?}m(0)9O~l|45) zZN_Ix4yMvR7s*!<$#>%4d&y$=@bdu2kK^lUJb|%n&$Y0y)*lmA^nd4G&*Cj~46@I=ZVxOVAYk#{X3QL-SF!@Z2llM=a-oue~Fj*YZ1tLknevZ z`EI^%=lDM6OwC;5VC+Z9Wydi8Kfqng1zAIGOCJ3ec7GR}cjhy@LH1Qx`~Z??u$o$nnEEO#{0({Ya^&6#ryl)}vi#X5GNg z%^Xc08e+Cs6Xt(~dkJTdMXpB9BUs4)wZ*@WG;(il{QV?3dhkD;c&m+ELiD&eg``3uP1 z8$WN%J(XVWBwfr(bi;(PG%?VP!ZO#U#Myvj2ei@>toh?-wf|4-_fohKbOU}dvA_4cm#d_!{E7E#o>lx*Mo}|0Lghn&*yRCt6OT=3l3?EcY1RLGnZ-Poo-{ zpS|eK*lGPfc~1Pg4~+Q-6XeTRvGH+Ob{CRwZK{;J(Bws;dI9*o3l+*~@cZ$GWh*0j zGh}Z{4qF@CM)0xl^!H-Be*>8Zz<3gK(-dr;i0$`-@m-DV z8$jf4uD;32V_B-5y_vPnr}}@L8hqK_53vIHILuxhhK*uB@J0~XlNn}A&qIuRD0fOX zfKg+q65c`bDC~b9`|qD*raBr-E`mw-G4=~&%Ga2kCSmaeq}_?HFQHnviPtAVVg{e7 z_L}E+_)}SQZtfke!~ME-(O_X_t5;Y7o=Tm(4!islVD1S>_&7TQbF!94QEZ=^y^Pg~ z=6%59cdTk|!pl#wGc*;sFY)?3vY!I$tHJt8ynQ>epQl1un7ya%$Q37&q3`6g?{o2N z$p^7_HP+BevX-3)7N;?veu8Rj5}rDa?7b=T!{Q)5J4_pk%^!f-n-R^2!|f|U`w=8g z0nxXRJ(<1NCy~eBqG2Ank$LPIJpByU#`1sUH|DdwKOk=JVWrp4(|HRcc@;9){H!x3 zvPb<#_ANe6HSr>-ozA@QIp(Zos0H|wQ^NGHY2QI&a zth@2{KX^9tIv99g!@wbGqm5W?9l^b=OR;bgcrQf7yBrqJ1H&dWi=N5VZD98x{eDmE z>`LBP5k@aS)?5^Rtwp5m0_(qzH!jAm3848FatHYRCOS;O@Aot|OY-&j;~tndNMwD8 z`SPoLTA#lZ!(2U`Rpz36FOKAy`2IG~IGkL!EOGS+emowJezb||rI5P>l2>P*`W$zS zkB8g;0@KIg^qW{afX#37I+1=4fcSL{+E>!zUQl_5b;r`|2k*lU`~~>>F(h|b>n(!Z zvAr|E;0`o8iqD6y$DYYE;Ppd1x-I#8H4tAMxvQYXR$#3ip|fHC{mf7=BXN4CkH0xE zfTv%^YSjtXlLfDC)|mIvW-@Ec_p{>pB0E99gva-=AKk&*bF-rB$L1Gd*tul6J*i<9 zq%yghY;gdY@}tz(%k%vKc)oG7NAX>v>O3^N1t!0YMQn-SpLfyV1+vy7V0;TwRiXV8 zc0WLye(J&X;O=**0WM&N^4~n4&_g88$QQ~`fS(o0}G4S!E-B;O7a z%AcFTZvFu?>^+X%pWPeDYY|@`=IOm{m`M($*1iCHZpN!GBe$Pt@cVdXcL>Q-kbMiY z#`VqYeJOe6CUAI@wbnA^W!2v&kiV~i;V}Jeg)oEMLrRY=IU7z?rzU6_=((Yxr{pDUjACZQ$+WjtbZJV+Kn1w zJd&S-ALo&w4~A9y;NjhoyEm)r<6z!j8TYPc4xCBVF&p;IN!8j*uh+qL0#@DvPp)db zeGBM33p!)S-s=)sUu9l83A`_ZSvT=>B}eDOutQjnY(zx&z_B}6WBm%Nzsag~FQk77 ztiMOia2E5)b=a)gUWGqq~L+p+crey+vl`{DZ=cx62MPoHExe+U)d zZ&=;_1`5+J7W`hoTmNAFbOLeoE#`^+&_Mfs$CBMHX2ge(Jf(@| zxxsoaZ0m3heB6Y$f8tf!BG;fKMr zgPThG6tuYrv>!q8RAN}%o|l!ktR8^FZ{yXcu=*x&zL9pSM^%@3Kgq(9Cfgw%>^!&vLd0 zB=y%jJ_q6lv93N5lz&0pc`95#ik!YDRp2`CaDY1cKJdPnYVZ_f{{+iVAh-OQ45k_I zalG+1HuoTLKB@^xo{fm^fW;)(tUUzbeLIpL;Qgy`LY`i|$=~~u!GDZ*e*xEi$1K$)Hc5^(Ew!)5+jJWu`cZqqB)})zptLW4w-EXM>xE03Bvm!>hU{~EN(wc>xs?$vLKmkO(bl`?)%rtm`7mw5j@p*ASmnv z+H2#Fx!IR{sfmt%V)eO1*ID@ZJobbxZF1*BjQ={Tmr-P$`G~6duz5~w9wM{7f}9D+ zyBEp#@FOiWS8C^dJgc3J;P=kdW#5EphhqCdAn;{;uq6yzhLMJdn#a&sYld^N^=#^| z^BaC$&uEWh^BYL+Yckk84cc>pyz0=m;PUg$F2eovzYqPN;QcGKo}GAI1*^B94%mY| z!f%1HklvSPU$-ZFu1)5ipSV&rc^_=LgnL;RVC@Ce1b-)*HTOT+Ska*-8Hc=uv3SA8 zW>w#B5kt?yl1GtwFBad|NPY>)z3eEDXCB^|tg#!J^ef2xIzM~i`E43*ufSM?F!w2X zYc2jaq+dXUUDT}QZegs)K;(5Ir3bm=uy_G%pBv=+!Rl>1F`1rEAX&SE+WUTvGt z9;%a-_%!y%uzE*!j`rbuZ|vWRy5Zw6Y`G>YO=a|lK=xYfycD^Yp}|#nSrz;QY@Ukb zet0z>(ig?z1&Qd{;Ky`w!6fk29^J!?pxsdI#7;%>oK%=AQHg$>Y_=ovzJRrRV*QR- z{Bi1`RY7|!`F{$wJp|s@H{O=VH3Mm1_Zh~X3V!{_nhT5N>ABHjHoWvMG5s>Ko<{Z~ z4YL&qFK~8{yW|Tp7k&VieH5#=gK4|)y#u>LTY|%yXtfZMXQ2B8`rb}t{tIibAe(A^ ztQ~yq7s%!gV=0R%o;6#J2JdO${W6j9G~Rjy>5rrT^I-HgI?aZTOi3lBw7*1lmGyZhlk~%|Y(&BZrD3 zsuG^z`)R%>Hks#L+K-{#crwQ7V7o!%?~h^c#_SHQg%_43;^#s88O$*+qVc~$T>Hn$ zM-SjR?K-{&BGbqueb_uFt;aQz$AUqJieYMl*UPk>|M8e`&w+gb> zNBV~Rd;|=XJC{T9T;#&(jPN4J>b{I*K8yz1(H3@+{2qSe5BC2@viz*Nzo*HeZ#ABM z0m&~Q|0U3S1<5nXCUe5Z_hH=%SiJ@quglLm9IuR*7l#E}wY*1Ltq{dhmxe$%d> zEO@U$Pb;gr8=F=Cjsb;1SU;8T$+Ua1L3$E$C0RWb4f6GJ$X=Zo{t(|EY}oaFz4QJqA-a6N7GJmxFG(1AK!~NKZ_!Ffd;$TiH{Dfn2xq@ptmrd!WeJxUOQkjn+mXx zQM8tn9kO^d-}2>qT%Sg(X-#YG7Ea|@I1S+=Aud)ffb<2BzaUao*{Hr4+pHS+z&-8X z=KGERd{0IDcN)3xAz3m;F^XlY9@M)IV-Gf(z1vu>$X6fDWAc#Jpvry9L1UZjCtK%h zP*Y~o-ixxrdyTGIujyN3y@SkYjm7era(6#JqnlTuEp~Go04c(JChey&it@;GddY{< zcnm#er;lP|E{;^anxUhS+uQhB-kH&8JcC!g)85Pw+Vim+tQ<`b&D3I>M(p8?FqI6U z#vje*Td(?7&(8n$Rc!R~qfun5qmm&&MlsT%hpbne>3vVb0qs$YX=L$1O}>>y^rNWg zFgwU@Nzu-c{L4ppkZZ|WlWA)?R_euj{3`5LRg8Bfw>h2*Q2n#McLBQtm(+oYYaR&S3l9E6E+_1<3(Hg@2P z^cnS^BWoo2%9LuMT&^~1As+|<)p;}Uzxb{=pWggv2UVUKL?gA;sAAk4j3Q3x_w0=Z z9#Ke`6QdYcXsGTPB+?!2>LE7C7iuYgiEZMl=5pN`(ESkIn~)}!EWew5n)y|K%*LzY zQ3%UV!cE$%-sS&^@S`e89y04S>!~hPepR+p{0jvk`rd!!D{2&XiUP4gzEvyDTjS_G z7m+?YF{tQOHKKf>zYsPJZLPKH`Pabm-A1<9rmU)U?l|(|Jm@X`75CChF`_t?)yfyj z>@ztQCnb3_y|pJWFa74Dx8_9k)tBN`GsEjmcAiX6GWz`dDBmjf70W_vDv~Gjt(p84vb$uz z#hB7td13)1FWhA4Maj>~mvb|sm^pwHVKs%`+JjKvR~gS0#3-a_CuCuIX+GB&i!{iK z-CDuDP0uOK{I9BdVl$3-qpPw`nOU{QlFUYn^SuD0X&#r}Z=>aurqWQ|r2Cw@=O_Ou zVw73c_kHwTiq|EPtd*2}Ej9_G=h68Ysx4J$n$gr->nL$cGlHZp$M2=lU2~FVe=$sW zsdms^T|J@j4DVh92R$#KjJyDCmq6;W+znZV-%B<#={WS(Urp97m@0>-snI4hRsFLK zYKly)K;}bd_14_KJhGQWwq^+N`<=#Sts5RiwpKyUfVpO2Rn)VAjwCOM*6-(gIeITn zZ_UZlTa0`Ot5xAXLH}nso(vA}5F5SdEnY0f_fps`%ogRnc1x9M)LWi@6`ZCtSiJ@g z!b@3N@h_Gsrd;ds@3paawuWPpt$9FqQ6%dvj+LF1qa4|)P&Gp-67+?s&6l#F)_`Le zO}kneQ@faoCbgcyNUGdbQ@p{Cc&i+z{HK{kyMU_l#ouwwkK$e#Pm;u;x3FJUskV5N zBSo-wOtk{mKCv07$Z>_B3?QwB!2Io|uOjDVzQz1EIj6Z#8Qqy}eh|_6Kx;c?3s(po zbX0#ut!g7>DaqB$B@U^T^09K2=4kmqvz}%o&0<<}D?2JbI8zEoc}Z7Q?`6J}rK(9( z5h>eime5Zh?{!AfJb$T|W*La(j} zgoiA4U&&K6C}%tCOR}w%MAtf!taF;9hrm*^q4I*p&;;P^`W%a$k9vhB(vn{o!TmYYbB>R60X)uTBfzm%cs#$@2tOZH6j_=I#{lI zxWBw&tj(abI^KM@bMD)Yn(Iz>)2c^wLkqrJJ>O@1Q81^ku{>LG6rm*(nW# ziKW=0$nhA`M_=lrD>|!pKA#+Bt&P6riciY0_NV)rLmo-3jH`RaK8+Nl%!clk*4Unx zHR>mQgolqTOFFBkaf;@$(>Cb5-bH7R6Q8J+y%Pk@RavheS?BBJp|*65oVJ!341<&; z=*zl?Yi5jaaywl&#*PAOX`gvjq85qi%n`Dxn{&3)8nwMG4RlT8>b3n_QlytPkiC{J z>!qjWc5A72z8d|ct))qO$GAN!3ARNHRZmG(f4vW@eO?&)oX4|O>LHtDxvl4rFY98o zjhne5o${qN=8E+fVwNuZG^%WqEjm^N&uBbtd~8t|WE6x%*jV@!x6Be*ElYGSMn7#I z*os2mNA96M(!(};{IoR3tf^zzm_)zwkj65SYF$07xoxwaVU7Liwqb`b)HsO^^{^*& zE?;JH{4d+o$2N#ddY`jC5<|?fF%2^6TbfCN^^ja2NtQ8jyu3#?fA4?!X#JGuHJ){} zH?6&TM<>~vcjAn4mUwMmq@@wkbz5qCx^c9YK}Q@CuiW0)8Y80_run+w=|`4JV~uZ| zY_TMJ22Sss_Z4Fyo180TlRT(7Q8rjBU6q#cZQ|O!)y_ybVtrQat(|^nMoT7;MbWIH zK<8|EwDrj0h|h)*9>eX+KWdZSKGM6yr6W()xJRLAeI(Ba#NxDw4}?K?jCyRziZ868 zW!NXiG1faK<1zIVaEWDWy<~?tmONvf)ZfUN1<4YLe0fh__Oi5(4E5`S zH>zkn2db6%V?3mZ@{S`ya`mH;Jz{yxToDFp)6P?BZFFQ)j;)91MvY}$Wq}bli=wmo zxo7mtNNSN>r_tOad7@?pAGvllg2q7>Xjjkvb`*Gal2(x^ojk+DGl`gB8P>=5_Nnuc z#_?GGEgK>!{7^qhw4`F0&gq(wE#@WD*na)CS2VU{TQBw2m+>fm_P<=jz~Z#x(+)UmhWhQ?KFvkRD=`B%6CiT-}4cUghofxMi)5(pIAxPh${m zV@WYnnpiK5Y@b?YjwQ8yrVvn1w{ks_{H4)-Y;7~Hqc*YM49YK!U=G-F@!Lp9ve7kK z8qL^QLh!0~;;u*3sMgJ|j#gXm0K)p+rQbyNFxB#JlTj{RXvZNGUfDgKf6=4t%xHHBm=TgeOVV=MF9*R8#AGJ?TN z`s$2jOXrMl%k@qgCBmeS$M$Hl->9289xvMaO5{eCUL`e>Jhu91WdB%V#tEx}MPAz; z)Vb2h5^SY4R=r@)CkBG3CEHt;E>F3YMsR-e_?BqT+r!p4@|YaB^!k76x^NU@~Hno^=!sxMr62<#+WgPeh(y?qgj4@84ontP)No!CXmi>84{}x6RTezBSfnrF7PL_s}_` z5maJ{y%e;{3iS-DCA0e5-r_(z7nf#^x}ZI)SL1JMy0u887#cFH zRD0u)zV>FFYX)t8#iON>Z1g+FODrr81lw-CENfOWbR}|vo?~7ks5Z(>pOGC+&b3($ znm>hD`O%WycO?JHziMZ1`KVhfd#73yGaSd=^+egDQQC2-D}|wXHQY1W;nXMMG}DV8 z_NH+!CfN(oGQ80KMElN6w*-6M@-%KTmUB?}o9op#)rUPHd#auY&x2ZCElcm(N3un} z5)k68=1b`Sk-Usvz?(lw(}IM>zoQTd`fS~YgYkQV+C!qzFi*I1V3QRDTr zkF552=SHz5vw^IU4CxS!b;ieawh*xWg`o9{)I>)jz@vwQ${dcaHkQWIbLq(I@qU|) znIX;2;arsEmfOw`IhM4NIBRXHzjJ{1rjuX0^GMs`jBV`oPDg3sIo14&ezU^b`gsg% z(k)TvwEw6mh)%}e*&x169xLD3pCfH{TqxHiHauS2C*}8OCOyJb`OWXUnUN9dRo+UQ zI%?}!l13is%t&^oonMWa8JrATan#n{ybO}zk^2jQ;kZl7oYzsTa&$VkBzmQb+pAT@ zujE_L!q{4R#Q0Pz&bpHkX|W?dEN#NQ(n@lZ!Aq;M-@KG=zANC^ixxG@hsW`^^mkPd zgsP9Uw_WDGq^W;mAoF+lAFRWp_#iSha@ksW#M!{Mq-|tnY`+Fu`_l8FcG|Pk?DHkZ zqZsGZ$mKuV7B94Il$PdZ#d&nMo`q6(mdn*PUotmVR2d2+_RFhhS9R(;0KJExnF4mcHV*b_DCZH7yTB2jv6v z+%>KCZ*%9+8P64lQL7wT;~C57=U9~o6a$IzXl?|f&8#d>OY0n*;w@_$RBXL130o3f zL0#hnm5QV=!|`Q3+Q+deuNmE=<+rhxB!B6>b*dv@vE5~b?J|blDEM3|$LQ%cBhhe8 zVw@#AR%iVGzps%O)*3NKu+GW5VNBU#pF3+Ag^?O$XF!ul%p%y z8#ikjekfwP^|ow}>rvG|NA{2Gk=C|3}-Xrr(Mk^a?4(QIJ88HaB)(o2eZ!8LfAa31UZDgm$v7sx;W%fk(3@X;8 zruN>D#}vxaGgj4+?8`Ovwim>>c&2*<<+bvgj--*!`A1`ff7Z}C7q4`r*|3eGFlhVD zJ%emUunb3w^wU@7ciCOMvM!z%XRU?3867l^5!S1Z%P&DiNVui1xvjL2WX~G8;&aBw zm>8)V)d<9HXQ0R)Ia*1{oZ|T;RhzvMzm`WmZuvME*6ggd!d^@pjz`eYtL2#=t~C6_ z%kY(e@y_}`BdUKmEL-wr_Ov}EeRB2xel6zN21|(dobil=#<4AJHpm09yUoWi(TMnp zaZ3*HY9;K8jbTw6H6I86T$y#taK_%Z{?<4BMzX7oP<$j#SSR<(7`7oO#iDRJ2o-M8 zXShGivWj_+RLnN#g1mGHJ3W%GxP7EbR=Z~MERb5L&7DMiH}5=h7!j{lzcv!W*E%}- zyr<^O=~&41XsTEHFqlN|YSGqSw)m^Y^SoFi1?Om^{<)Kqy}ueqqm^~i&tA>hj25Pt zL(bIJIr*tH5YN@3-Pd+`)LiwLw%_)arskR>$zzqp*525cEkl+IzaWw< zQhj|CdpvvChdSphX+PTkZlT@ctSciwK5KhT=c2jCigl9PUHwN|5DfR)XhqMA61&>- zh{l!8MlKqdMY@*xPrgc9+fa6v%+w7vnn%}oWw~wcw!Gf`H%k&9)l&B9Y_8;eDl9Q2 zdBip*@7j{KJ+?J=SIk>)BUf@{RmpBgyFBGWt#h(e60RyUYW&rj2227&&UZD2{pKzCP-@7=uh4F zU3vDu(+IC2uagGM@VIk=w zk1pB6zdVA^X0*&9MpY8>6`b2%$ThuY1oiPO*WUjZZ)BBPRm)i)C9_p~`@yX(Gup(i zZcMv%w8U^Aqngj!v(3G;M5*+*C)FoBwA@Iq`;5{c+GyV8)X7t7=v6;#zS}*0_oiZ{k~XF7JIMoU$c_csylKblWKV)IK&@x3=|#qtA*9 zLD-SwzP3^px9yd*S{3Q(mfGD7-epsm79Gom_{n|L!)=93P}CXc1+&+BRlTRt!X(?> z=5CT+) z+xSygxu@oe$#_XC9a$4e%u%>t|4B!Ct+IQx&770kqZ_N};@qiw@_uelSY`y{Y6LCU zSFKI)N4k0Rs*8f2_4b(Nm95dJg=Cv0xe~O~%G@#P8ZYhSN9}cF^wQp0p!Uh4i+cJv zXch}Ra(OaRhp$sdw#%9HNc@Gzsfg=(MX&H{+f@36+qzm3Jf`~#J6A`ZIbxZ5c5}SD zzijqQ5nS>b>GoAIsz~y* zx;_hD@!Ty*V};H7R6r{2*cV=@oxb8fd#LS2$4=^DqwRRqy40~!BiJTkZd7VcsZ7!c zL}L8ej@{@N?8S3?!G5!T#v~TbYL}vIk0jSmqS$Bj&R-Q#ZV``G)VhV=r%mFqEzi}> zaG8?Q=3Zqr&v??umJV-eOuBp951CaR{pMJ3t^KI7c;u0zBS<<9jfZ|~taj!W(s>5N zd@Bx=mX#HQfvy=(`#R^_ufy?{m7PX(J#zRMk*`Jq3mwxUAUC-5`V_0Ea^Uq zU8!r#{a|V&6LV&XaFt(OSrp^KB->$2rPFX9>%B%){;8jEPTDKmcz;N}^<^enYOzvz zBI`vy8P+%2%Xkl;~DW%9xckyQ^#GSdZELJ2Qc0MN)aw z-V%RUd}Ea6`5-)4u-)KRV* z+r(vjVLS9r_KT0|nXBpR*FwLM<+iFXq<(MaRuD{b1>eBDfnbidl$nPIJu;yP!H zku8kY$qUgcUKK88qvLPZ`9XRb!P3|nCbpL5;dZdMZE0sI8Y`0R5pyPNbZg7haiL)S z(?433r^*A?H0-E+7)_$Fak4k1YgnYQ!@4>b`*pSE7mcnq)+70(ur#}Zh}%hz`Wnsv zUnvcWZQ*K=7>SMBS{LUYNfhF0n^m!}HnKi5vYxl}vc2s|qv%mJivKc2^A zkxu0&VN++E`GRGWNPq$p3Pizx+UCem@Zd=>d)cXZmq5E&FB+Ht0 z897$fSn;)e9N)DWBqYNr@i1Ny7U~_;-A?c0UAJua(D87IrJHfnc=~E1;5HfA(NMKf zv}-d(a%wJ1?Uu2nYpSt$I}(c>-THS+DOu4Y{q2>!Yd>L~bKSaSZ5aHdPqg#yk9elY zRsUk(@FQOjb8Q`8C`Kkv9IlwNUL)?AB}4aADIK~T%G>y zESTQbWmfo?&BKpMwyn$i#`yo(Z(AKH$&4N+)=Ors+H$u|SH)G?+HINMsYm!`UpfLx z59<(q+e7M^v2T0}J0rM_Ge_E6Q?<~SEYGY_n);l3bk{z% zuVO=ExL@?@w$|_TTVHNjNA_D=ew}ePZI2dRjf1QT2V{pw7sf{4z6xe)sq;cVoGvff z3+?x^-D9ftNOWaGzLJmB&b7HeHB#&-sg7WCAb)4Z(Pd;crjd-s*1L39Yme7`RN7~x z;?T%SFkZLi)~7gW>w}7`zIHuTsK)C#)0SQ5f<)?X=D{N}|M! zDs4)ul2P-8a7tAok7hh!RdaN5tfRMS_P@W}->oxJ8&AuZ9LtxKVqKkW^C-uaFOn-p zYS3n~#vT3>TZMMpYu!579O)Xv%ucte<8Iycd5!u+r!g;-^FC)Xa;z(4;v>t?@7zB! zM$*K#7+v9Rj%r+M7>~(Lwe_Bg;w9K;EcY!8>L^+}zQg41F|2RJTyPa{ofSQc6mA+n zKa$|%jG7p+9mcdf{)|a7w3({hmh37FjBAc;M{x1TvN{~iaWoZng{bz3EOIsi2 z4#z%mDLpbuY?o}0YWp=(%@WeUW4CSaakUDIN47Ms-8Li=gMGc)zh;UvskWtl^c_(*7DDKtHDYTZW3=GXA{elLyV?HWD4 zXj>}H+|qn00s z26@eB@rfo@2CbFmO zC>-l(IJ48<{&cL$)}U>>%Qp89n-UwrQRlPjsok~Y583Xx%u(59iN#N0rmxD8y4L+8 z-SySh)d&;@y02{Rsil!N->O$u4SC8zcXZOGT}fE`FfvSwPuj5{uXwa}W>#hw7RI{m zyZCZ=W)4SWk21YjAG#}|LS*ow-8~hUqK__gRRqd_bLMVuJ*EQwkifgqG7U7YR zb+!^dnJZ*Tw^ZpHM5E__XPI@g9rn1rne);*GIGVttFfhP8<}<_6zk({$tvrNxiJdg zwSEk0g>WI!)~%cOvfVw~zss|}Zaapbja88tJ*(QN8bOS!&#q}y*<>~)Lxi2qT;0+t z7NtwE(5*C@P&bEDm$X?P8HKKnQXiSu&S}Gsg`Lqi^1(8`9r-%kS02F;)aG6=$cTF1 z&MUGrdK(+ZWZTxVUsBuo*t4RotZ{r)UTq)Q&hnla5kFV=kl(cRC! zvIaq=UK1VGFSBGk4Ng&ZKG>A zt5NGow!*=3?IBksW?*@(^l4i({5V)hUUcc!RPXAVjx8hX&3$vF(*5eU<+xSp5qyF^tMx$+!87PZ&ws>eX>UT-%eo`Yft1F3Q>f(dt+MlXM-A-Crv(mb3H+$P- z32iak=M*L7)zo%Is_jA9;~ZW-%DKpJ9P4bi?cLgVHp)4-EyR3OtT8vDiIB79#b)bj zOv4$sN(OTls`xE?jYHPJg`?%kieMU!m=Ts^U5kO4HSHnGlU_cG4YA1ewrg#n;g-U~ zOer3PIl1Cj&FtQ}s>(u=rL+3B_0k#3h~}AXVuQ{nvg9Fa>$7dmlRxt+X<>$XwUKWw zbzOJuB;Djc{~wre)3Q98J=8v9Pj$}}w!wdCu|C~kslMj16DL{4yAHMt>0DMuV_BRp zUss!OPFE@l#WJHVuK4MxXrsPHIHSc6HGa^mmT8kSr9m5)v2J~E!km%>GuI1>GT-0IWa!Nd`vcd|5@DVTPjvmZug99tR;j7{{4 zw^AXrQ7KQij|wMyJ+?C0o}d=S?8XwrR@v(AT^ zMKX%o)|ag9#v@qSs{b1uUA2FlVKdt(&a>ufU+K=y(!#QZh2le_S^MUItFbn(mS#*8 zx$Yy|{H}YJbY4zhOUXOEIwotEqH=s`o!Aaa!NupJv-R!%U0T^Lzwh>O+w*DFJtJjI z^|qh8`E5J$I}zfnADx4#-go;bw&zN+ivDtZcPvW25RAosZ)R&PkVtl28l1xBc!+?cZMCdzZ<~HD}yDTKH8+T2kyVYH1tWJWpAdv{YZ~A1~M! zx?V?aopB;B(Otdt8leYczUWOY504buCBnu-=Oc?jO$Os>hFn zyc1sPXQt|GA!zOV-rR5vr<<|HSEzcfh#j%dezcD~x@SJKNM}m2k7A=^+H;uWSl5N7 z-nms(-zrUBiR;w|&mO ziKP|u@n&@NC?oThuiGb@Q?IYU`~q^Z4`V2p#! za1@J;{%(;WpX}CChTm7tc~2 zD_%C|oJWV>D~yjUKe!hLg^s-)R&8t|GQ-=iwxC+H{hjgK zx`dfwZ!ywZM2>S)-OKe={Vok8+wGm7D`rdIjFZ|pcZ4(3irubDVrywIT%+jF&Cgl4 zFJ?;@GbX=_C-GzOb_+A6J06^ktNrjX68+BGL3dW|!`aNGMksTb{a1QKXW1T0i$_MJ z&8A{xcvza1&TXvCCr7U>PgIDz=Erb}IxlV9!g?j!dG%7cN-V6%%ezQ*rccdMY$)WS zzhniwv`ecxZ%+EWbxd}U&iazJ-O^pHIx=FD+lK4iGg5n$s-+z16?4`*nwIX8ApI=Q z*TolU=JUcUxLelZ{y!LGtkOmV^hg zBwlFuvCgqPEnqCDo_z}&>nShE z0)6Gxm`PGam&cTzu}j*?Llr~KApiR-qj_YZm7nBTWyrQ&>Jh0Ly*%hB6H^?e(xJ^* z3oVK2YwdN;ILu1(s^0C*AlPO=+w(!oqxxNG6b4w&@`CL)meREFH6|HHJ#0gJ{JI+a z;ul*j{pyPG3wq&I$u9ZEH+a{1W9uFoS-NEXmHZw2b=7TzvExhnl!v=f2!0+hm9})K zsL65SximC7(oFWYxnR6}#^?lrTuJNXfFPXfZm$~DtBdlwkWs7PZ0U)-)F;A0W7rez z*wUHU?J>I9)t00&9AjpQIcqN#zNKZiU!`e%*&@ByQ9H7XsJvtk#1`u%eI0MI&HgP5 z1!R+vUYqj_oa`tEfmap|Bju} z!}5)(J>J$XINHxf*T}ZV^wp7V!+^+i-RRFgWCfHqZP{UrXHd5+oWeSzEp5y{A@$#X z+`3}d=twi~2j^av^wHd%irvT<*C3c^6GO~Pvqr7d-$*1Q#Q)8OVRt`f#t{87@{XPtfO|r-FB^_jmV_#z>Yo~2{1fBEw$TK(0s7C2V@4~@+Y{!_q zt?NQZ@smCCLMh|4S)y}#UyKaOx?-(LR?Vu(%H0~OkH^z>wRVMN%oFt)&)V9iNNO`8 zh-A#_Cym<@X3eHT%SX~k@vm_#+ZiYGtvzQ}s7>aA*eV@^c#a)osec>un_p$UrN;Jn z+RU)krM)fId&`cVvBVzn*w#80q-romZRFz*GbWrjDGfC8!bn@CYwyM3e zX;3Mg(j)U)Mw=C@tF5xBWn0(sM;nJ4%UT)}GtBKY;(z-}c5$=}N3z%2jg5# z)RzoLy3r_oiev6+nbE*mF-RNXFfRE!SBnwJrMgmm5*gOa{uqu^IHeIBwH`xOWc>7x z&pn1)pK>uVf$`$P_kauBOWzNb~rode}q3eZ%@Uu-S6V(@`q#2esQ+f-kIOa*5q?p>MwaZb-KqB0>yP7$zIP($r?r~ z-tenYE?s?OIo3kQBabw8jjJ|cgS?nIS(=o6kssf=hj}V{J&N1AXZvg+RyvPliap={ zU6CqJIF}l)kx$}C18Zr?u_@z4p4+9RB$W2)X}M-#yyJecTdy@@x2!~nu8Unp$w%^e zqFwwj7o%g_6XAdRT(^GFT$))I>nf?@Sl%VejO4eiXzP&t?dQ2ALHsdFI*J!0Q(uuA zUDDcIbkx;f$~5!5ma={$b+U};Zz*n7TG&6fOWKXJPO^IkG0@b>kQ>`kiCJR<^IipBgZfi z#iO(i&Uu<(CT9wpSmF_)b77(0p09GpOp@K-^I|(|86`_?`^5ISU-YqE9=+}5jILLG zsc&*e<~(bwE2VMH#p2{@x5}LBF=Loq^t(ot=EYW%!c8)qR~t!PMA%Q)SymK)o; zuUU4x26SCeapx?f_^2P(^=)*A%V_IeBPe!Uy(RC+5{)C=bxlXoDY2T2Xzv9BwGoQO zBAzve6tlzUy5MDgnj?Ptx0oY|j(3f!uP`+@XRR68_LkXipBV+Uw+(uY_1%`pTH{wO zyRA{*=nz&09W&5qxsBHjddk!N;$?HP&dIJ^lV-)8;((*wdJSimdpkaiM_OcDW1!x3 z6n!f@=FIT?;Cy4Ig=N9p{>Zo)-RzM(=R3==hT^jLmYlB zc|fg$PTOa*ngN1sycSvQS7RAnt+ge~uC@ibrq;=xm0fidp1D^n@>z{%MrUMs%lBB~ zi)6mUcVX+^-M{S-V_~KjzlECR>nKbQtA*aIuyX%+JUnzHwrkZ$F8zF@^SNdjMoU*q zLyapete<(H_iB}E#g1+(q@lhnCz=-TeckLxTeo(<{0@J_C~MNrHqosuCx}&@;5kCE zFWqM)O?IlE_JNYK(q8A~506{*NZYcqy`5=nzic;8Y*XUfxwc*DYLv9Kchti+cAruaUTA8%Rb^QG|tA&oPIojymQ7jnUSY>t_1tSs5_0C_iIczni)jn;cwYj6V z6{WUN8M$(qBTu7R-((t{iFDiJcRH3Fzq(EIHxB-tzSgP6vTlj~Su?JDXs$@N$fzTG zU(Bw(foNup<$Fh**Fa&ib;|FuA>*_;msUo{wrgA;<*Lubx9)F0Cq`^(WEpk!HuCLp zjGWI``!<4U9i77QZe~|bF0JF0j4Q3|3$dizD?(a)m#u2=%q82bwZ^I=>uWEWT{_eK z6W?pL&#~~a2lIdRTouI68q2NiO>2{JBhQ{4$<4%9`72myOdlsQbw*#7lXX)uuzI?- ziw~uJY7y(@=qT1YFSJ((_Cxu!j;urUa69`UHH7`ty$caMq`mqY`SN!Atd1*R#y*eG zjYS)uZf*r5*KLX${dKJ2qec%~W1V7Icw~tg&l%BPlScBdWz-eVURg6|Jm-w~G&wSM zn zj~+eM&h}e!un1QY(T+D={crJ8JQEy6gm&7V)1glQgrdeN<=KNcwD8WnW7Zuju5Z~kqMk~)5L$RuIhIrTJp7b<&xi9X!Z01g82Km}-@#w+Qng*F<(pY8x8(ryN zG2(M0y+7Q_aLy9M8}rK;M0zx{$E>NZ#k29NEDf(TW;FG%*G zXm3{^p_QKULdkXwmmIEkLZ{mjoz03W6;UiH?&qkorsTxu-dB=+!@tA{?VIQC$oRUE zHn$z=k*F)lh~54#WNfc~$3E9D8rvMqtSha<7kMRL?&~W-#Bvj>L81_=KGrLGr*0^1 zeZ~3~honhxw&m??+V+}pZT}WyoiC(?{OhYeC#$;q$L8o1ucfc}BI#KVJ6;_-8mX{q z%gZRf8t$vL85MN>8jZ~KT1TX}b+I1dayxE3enzp3#8P=xmL&c(ds_GO^=s*s(Q-zk zj{Hgap`j& zp_Z+5nZQuIXQ6da$Z7n`azgf2~zUX?Jw|ZCavoii~F>X8Cz6);tn{DlVhv*`0 z^*4ptl~aj%5@^Vuya50plrM&sV#@eQqn&#_|fuVzXtNZPl)@NXtl! zG<(UvM_N-JQG01-%@g$=rOnaUmGQ%G$8;NwZr()?%ZaaM6>Y{!IbNARm6GcNjqe{b zr7&yz&APR-ht8C(mSAg>d4jyhcZ|1Xh$rp&zI^07QR9dEnYY?FNb>*nV}FJf?ViCw z=jdCGN% ztx+rcq{fpy<>@+a{r$dhvb7qgt+R8H5YJi5(R*tgsUA<~{pIKun$;tgB+}aBj+79T zEz3DUSw$mP-=1mJ z%3k$TqG6!M*Lh>Bk&If-S*su!t!;~Pf_BQg+ZfIC82Q@fmNhqOZHC(>jij%5&f4j> zwR23xd%-_@8}1eD)GJk>aMPDHschmi<%hIL+*`6Ruzq^4<7i-?)$ibw__DqkC9mmS zoNUW=pEd{6&pz;PYb43V(2|zkHA`D&TXIINE6xSU3bNRiMMfKy>Q$&an{?NsYG;qc z7sb}HA-ecAdIX=gM>CobZda9MS$nMDkQ|dLR3qBHh8E2)dsbZVQ5)x+5yJkmj*fUE zVDzJhx!Ju#m%hrv*2sC?7&(`CCp~@(vocRdeqNyFF?fvs{Y?!As-IF13v>f{I4fPus)xs6ApS@jzsHthOBWcND6ht^~u(5Rox* zrZi^K!{ZLuNh~%C(pNod{Nz535W6yu+TS{F-K!QVTXc2gS9x)ywYsK})YHe36WJs2 zj9!rI!f!(TO4&iSZxNj|j?#p9}Y17wjJcQ{ z(6+v5J!>@LA-A#&%Q51Kw2YWnbItg)IhZ`*NT@Mlu~E0@)mBHvm55mmQ-x2{Yx+QjG89OvGJz=pdG+})=m zZ1De;_V-oZffKuH1iThQ)rX(rjGGXK1gyVax1aM=4HK{uBlt8&TG6BD`<_~4W;u4t zCD-jJcN`4p32*(>LXN{AV=`2|h@*Kju{LJgrX8}kb;#&yTl@Vc6F>bde*K97bgJ8W z!|I*k{U@K5?r)aM*f*EXohL@hfMZi?wfV-%Rg8U(ACH`k=b{MrvL5fv?D|uGwtxFx zO%CnLl-l^U7JZI0PL$S4Mtm>RU0Vyr6$>wmm!&jWl-AccN<$!OjIpL-YBNq!^RV>% zu?m)b5vjppkdf-0D}1&>w1>5Ngr}UBRgYp28@#OlLc8a4skHs3Lzh|lLGEBzVl8lK67Y`_d+N%Jh3WXVz&N$ z{G$AxL=!v3;AmbFTb-Q&C~_HSUeqRe}|S7GT& zj@!w_IR0D(F3(mD9?+v2)K;h zUBzz%Jo2;$xKMcVR0HI`)H`9dc6;;{;@bD zoV!w1r(t{VD)6^mMW+0$-MF=5_O=&N2j+AY{rb*4 zJL2ZL;yFuSsUls;o)&L@W!yEd{cYbm5QltLr9~F!#$%Mm*`A2U^4a=3P3%m@eZSdV zhWM7S&zD+x8E=+icgh>bjMz_Dp-%xJZix|;WmKz7Ei+*YMo z7MWxF%|h6#UnWtQIbdY{`o7N(@pw5lf_G)AYOMlUh40o#Sdgb%v7c)j7^%l86ua3p zYijYuU`~iaHk|WfpWRRqE!KR=Pi?j5%~fKRTcsGEX!K{z?K@@p{`9GRt%G3vm1V8D z_H<5Moa0aIo@eKxRZm8Jy@|vYqLtxVIWouRpsVQlxLk^ z(#`6!Y2B%)UwK{5?-_YXjm?Vh_%CDWhxuf_ijvn)d~j<#vdk5$uUV}A@#QGzJ0{QT z#mDPG+*G%tKpCP`UPSYe4y%|ur#7oZeybB=wjXU@KlQZE$kLscPr3GP#`XJ?1$~wi zbNLHZy2Xnu$6k!R*K)-jgxKW_%Xb~u5`CX9=fhPj5T(Axb!U4Xi>Iy2KA(o!NU4F# zMA|AA(F{X;GD{TAukNg_YOR_uQ|_Jp-jjPpp2nI!FVD>8TE&1lg)9ppRnc5&FEA4--{BT?|orC)0iz{GefG8S_`MxVQkooPbS60dy%@wWBtB9S7-Wu$7%iH5mYdb zMRscn}d-JdEe)}1xp2em}QZIdKItA)&KEt^+hBJ*b5sA@7CIxTlkW-ijEcfpRl zsO#U_nI=Rq@7;mQWbt1miTyWn?HFr_i9oH&I`fww&sa6QU+Z(bG^97)i^_GPYi61a zt#UOLv}P0sM`HQ3{R_KSTyjWe*ys2U!3 z-Rm|&))-C8G+X@bO-8bD-_;dwMjpoOtMWxZeD_Sw@vUYzJ3KD}E9%v^bB)G(xN9{n z&1(_Xk{-yxVp(=nRgr9j8SH&?@xDr&e{-^L8i_)y=G|;wwcPs}?+4U7dz62t(;eT~ z^3G`p#_jTb`MYbYR;+gZ0*|{gSKaK(|Ggq(yrxQPvC5Q3IShfS8^hLLH9AXKHbae< zad&P13WaFxm3#46nf!)GHQHa9-}QYn^j0LQ3M^K$<8C%%p<@-pOy^B2n`bOA(YWSc zJ^m-Gxemo;tI{&$S?_T?O_f_lY%c=y&3Sr~!^>P+l%K2RxMD>7;a?WEn~Y;H@G_5b zqy03)*jltx{AK@p4``0_uJx_mD#-hj6eBLN;8n%xUp#&0xfQ$Kjzsery>mbFob}=X zi&nfBb>+3!X9Rx7$Fq&aA1{?=Hs(=GjtiKBYoai6s$8fuq(Q@7d-sn(3nb;A1$E4-B>pYb1__oz+A#_2tik56?t z8V<~rGswd#?Q-9!aEA~J_f>|h6B*vFvv}|~4R()3oOm@pv0Zkmoz@6z?D{ua{m_mj z6-`yLYi=)-gHTj`AoIoCone_iJ%FlSJb0e>;z1QLAr?od7Cq@jevRFH`aZ_=ECbGu zjo7-a>i@?2Mxq*>A$rgdg-03oU#97IC$Jz+$51oGH(PsV_nTqt-@BtlqXpO)L&njV z&CNp`J3>BX1q6M<(BH|Cq8B3GUEJc>gMh0CFQX4em+UX>EFC!XPK27{jfT4_CCQE z9Z@&e-(beGI(+l=*+!h{wP?kyMv6KV=|#^|3)gXK>*7cu%vFV-qx0O+k+IeMPuB3T zEL2mn)bG~1PvZ6Vmzh~~1aohnbHwaUW~C7azU956EB@uL z{AJmkbwA<$cKM!CM6w(kT`}>b;BQL(*FV`ZC)HcWa^y8v?(fwd*S%yYACB#Az*U!# zw%@U4yE|2`T06>1oa@b2RlT0F`{re9)3}lStGMNFiqXz`z?6@o4v9J?I!5%n`c>ro zmD5nI=k=4NRj_NXRi1Z7`I60L@b_6THa&vTfzU#e24UB<5YF#a0{E z%2MphzkceC7hB^3ExpRJh1T!t!alfFZR?VF^eF=KY5e+!iF?))s8LseF9X`*9ki_v zc?IDZWel?~YxT!;V?^iN`t%&raAchC>PMRlj>w08iKkfm)YN~+ zh<7~WJDx0L;So~B#nvG#iAN_ES87OYf9vr3sQvk3Djf zD)VN0_T(miF=4dyjSFn~+MLwU%4znQ<4oTBjI?hpu~EN%Z>(w*=JoY?W_0zc$2&4a zvU6FDtE+5Ps^F)e*!eOUMRkimp+u1h5bj z-w~Pf)CP867rX0)EoJ(X{rrn!na<)nJC11e2Ya;ow72I8(WrnTH1ha#k0_gYE)I@8 z1H=N^Z8a*BdcaD|<3)6z`N3oNJ0h(ni#}_yw4=WHG=@ET`}A<HqVu5$Y2@8hNqQmhy+{^3)yI#qJv2>OVHqviMShneRLF`21vkDr?@0 zwdX+gr74Me@zrRts|PD9X7d2bvcxlh>{wgQQz_u!RUJ7}-u=y&?U7k=4j6W}dD_b& zcf*d0TQ~t_*9Et+hTBD$!BZ60^rJY{gi1i73mB zwyO1+%FIS1TG^Jddnd1LJsW+BF6)ipy2w(apO5zWc^<5mRCw3$;wNum_Sonu3M_s8 zAd+<8T*gmVV+)M*TxH5?6-lPEVjFXBMhAFTn_!61m?S1i#&dvA$I1zQKY+f9S9lmf4{^Y>P8fq9{e_a zx%iHDnXpsJMF~?5~de?En<- zv-ooN=1S`_l7}kOnvVrD90wkqL66wFf7zTOFHgk)JI)3_7FubzvZeQQ^SS?gVrd`g zo~zY&t48XtrKCTr)Imp<9Wmh)Yk#JpAI&Mjy97d8FgY~NPu;Qp&6 zjBiGDW}CR_?X1WgFZRaH#-2OgaYefs+O}N|z1uf-GvOHC8wUzwS0&;gH_dZ&W6YCU z|NI6j)?VxBY`!~-$vf}OiS?kK+y#EcWL{gFTvPG!X@7pME`Mh}zO=1w_k^c)S0UpI z_j<@b_*13GYr!iBjZGVlrE1}~dDNv(MSYrWWLU6<(s(n_49ds0{`3;YDjdrb?KIETtD2vv&Uk`R9#RwHhN_tp>o9HIj^tPtat1_ z>l0J=GpT8s^7$!U7Q^Z+j_ZLLwcJyPyUwx#^X**K!6E@EOyoiP-gBn7`pQE4TDq9E zPsYsp%avnjyKGOx?blQ9`m!JD;$o~3v!ZA@rikA7tS#nrzqzyW7^>^xwZ6}Mrn5J{ zd35#2V%Rw{vy~!Gu?)W3w@fdhyZRWFv#~w4%RKnUl%F4_JjaCQlPCNb@7*Ory5qNk z*0*&?>*>uoREWF4jruYaHd(zrFk?35NWP~={EcbngUgpa{S6;VaDLpn+H=*9Is!K@ z{fJJ5s0Xc>MOQ#9m*ILMbHR6=}QMPuJsz) zTu)kM_3KV^oHAY(j9VLG<2**<@mEAu5u;(NZ!2tR7{?|*Woh5 z#OYsF?B79mcKKC?|UckdDr2}rdIOoalW~3ymuhtfEhe( zO$0*~7_0ARyVb?M$G=)|zDmUoF~Ev>{kC4zdQIW!P_9HTQf+Fl7PD=}rV?s1S=-f*%)H9N#`ivE%z5dmajDkh@4E!CP0PcUHPk7OAb=z1)LK8+dfpi4!R~6B z>c>X5n4hg7$A+lf5f@_>t_I9me#Z38wg}~YQOUKL-*cz^#X=66adQ>6&wfWc(!YY> z+2mrmS&Vy_=tnkUgyZK+#G3(Uu6b33x{lNEdz91eUjJTI<18+`BV8WG%sL?8H>a0dt>{t2?;i7;OKZ;yD~bX$t>lXMNS-aF|UIJ9CWSujd~yuX5!%v_*{? zu~cQc=Gj`*#j+!#y!!WNU~c3zI6L6h0{+2tM-7vBvc#7e<18uWuHA zpTF$3PMMWhjoa_u^jcmR$t!)8qx!|GR=6WcKSp_VrG`>zIY1*@E7bga#<6V6h3D2^ z*y5`!crET2f1l5=yjbNyR>mqbdnVbv6BvJwx5vMF^xrUI{*Bg+-d&&#vtXXC zZtrL#D-T;W^;RB>PY!Nt+!2@OQw@ybs}~$hJM_RP@mA&Z>&rQ^HVd>e>)xH0%TyzZ zH2qrB(j74s+zf-#oommMMWe+=e{vzuS|$IoQfsR$PpX@lma0^I)9Wm27+w3#%$q%9 z37e>5+1#Q8aWGT;spE3yvEG}Z5WxFoO%C!+U+aeUL%_WIYTRbzp`N^0YvRmLoL7gU zpjEY-Go!^Wo=1bqe=RfQr8BeLE$1&0|Ni zW&7_Oy&1lD1jP*ovQHma)nmRIwId^p_Kt;tw4;@*IjY{UmbcGHiqG+kQsuH(p7T$C zmO^Nb_ghM4L`-DSdz>&@@7il*Sy}{+BEH6#pYF|C$2V8p?Zb?VgBY1YoaKy=aV$1s zx+{fj!^SW+FNCw+O5q4|olY;KdTGzC=UWM~6=(Qc-Az3=gW`1Dt;BxY3tzJePO|w% zRIL;-oKZKfjm$77{nn#b+a1L~G-@uT(LSDSb!G4S%JFhFX3nq`(_@~NGT;8LUv%<& zFV*(Tf-A*~PQ3f9ficCz(day;H0HF5q=s-$d&S{uXP)5q)BYbP|7tZ)&eBRPjR1M9 zl#jAnR5D%`V2y9D_jf)~jN_}D7z)8^I3|ZGT`rHyB-!+*dInia z&~wq2&wIp6{hphVJH|*pN4XNbvxktg>9w&tYTs2MC15MPSOY5Wt@>93MhmvZ&DpDG zHMRKM1KxWnSAjIR8jM%hiSAbEzrSIK&x_go&ToADsy?EQgU_>KF{5h3{HhP%Q{EH_ zvm4RVOgoBp%qWO9x?GbBURW_Uc%OauI7Yu~?y_#g&hYad_YkaoddXM1RYyfdeKn)K zCBB#`M}4y+T5x~=gjF8o@l)@!Y%avRe7c^Q|MoRkYT1%^&-YF&-Hl;u%=M%|i$D4z zC+Z*OZ~njITCC(KCgA3xxpkE0&SL-MK z(u+^L`WF$=_&luk%G)v%TH_WeRjU29-WZs}&&}#(P_F6EN{ni+8Y{v$t39vISAVT2 zm+7mWDyO>|y~o*mKOx8QgcVf-TJy)-?$plXs$sRpRY%($*g1K|BiLU z`+iretx0;+nrc#otBZ1YF&KHew01$8UH3h%nbqq?&ABri5kF~0#;Vapj4i!ubvbAS z8D3($Ph3{W-&e-##ks9wkDMPih``IipbK9f85`-V69ql2LAQxJrw?}?J$W|F2eDmxAh(0ih+H4j4LI}Oz)rb zy#}Ru5pT@6zok@F#fWO|@-n?5wvIetbV!{w|``R|&!Y>)X=Avd#JM$PixWkc;T z27SN$#1}{7RQ33j74z0zLpIdN`=s*ShLx8?uh}u13tD$b|2kRSH}7)jisT+|6lTio zW;0f@qc)1pb%~f5?;ILtS!LGre=jTN|8oCt-dY0rpZjkLg0H`8v+DF(@JG8h->22f@74RGYqhyA*IR#I zu2=FW{a?zO`vZS0`@{R+dV5>%v#2lkyXoKc^h$b5_j3PO)0h9lvFcf`|JCo+>y`A< z|9{%&wO;(U7yBfC%JfP9#=Z34x>mZ6YsG!}zo|Zr|BL_D?bH9e{XcXs^?&`==Yc=o z==D*ro;5}N<)go2W2I5ByqDLfW##qWeKq`VnZKodWqVuf%USh%IlWY$)M|ep=Wm{s z|0n;qUweIJ^-3?dm->@hAS*z>(?8fsS+8DcU;2OZ=KqwmVk`9F|4a9kx61$M{Gs|v z|1YPv{oiK%*AlA*|9kc2Ex+gSpOIkoN3UitWljH#o4#Bx{*(53t`A#F{H<%P(VG9m z?c?-mStY&gTGL+Zdi&R#`$PA6<$tmHzaHxKhBe>I>!bdEY=SjR{{gG?58X%o(J(?r zSZ+=Gur>Eb@9*6IuhGlt)9x+T+g_hly|O-DU%P!$YoB^)Yp)@g{@>nPd4GIn_2B=1 z{q5gAX>Uw=bG?!1)%rvCQN6RoI=gzWUJ8Qx=35`%ivRHb%(-5EpX7h}eI@>u_R?0p zUa;!)K_A_R`}luj6ug%7t}DINpS7b;(#P+W{K=sZ=-*()eKh7jyk5!QX|Du>u;RYd zD!0;o@V`0ifB07X=kBF?WmuSR1*^Ol_ffdt`QFqj!TlHdXe;i!|44f&JS^+I@i6WK z0!$HL=uHU-;_n<0##X8qTT{}1fczVi5mszXtw8w=)c>SZL?6%)y+DUC{Xf8faqrE5 zu@#sI7JT<&7EJd7`!}D3VE-5Xrmb8Kd|P>(-_#$yUdmd_AF8ib=6_nVVk`97&}W_1 zUaPg#-JPTo))(17F|=CncZ~WXV(me1->m+%La$di2rhyL10MzfLI{Hh1~EbcgA^hA zeajFEgz`7oAXG5eBJ6&HJ;LEPI3XMnP6+4!fD6I};rd^2Ls-H6uiKjYJKv{kE!SIz z6I`3MPDg}240dpPws5O9uoVh~{I^X~*gC77g5P#?f5+DvLuC=Py5-HB<#vLgg@&LM2c!R0I`3`A{AV zxlj%a*-#dg31z^L4y8e~{TJLuKHORn+-eEjdKpv!+fohH!ge*l7Pi2aw!=1e!M1B4E!-3E_mju^Whmt8m=(U>T`S8(@MC67%t6* z{Yivtw!nVthFhAg|wDtONFJ-l55Ga{DN^goYz@!kPz~K#=;gJgziI;u$K|AZ$=~5BF-Y- zAu{0>C`d=-NaO z`mKgg|G8%vCsN;BzM@L1J|r1VykCi~@NGXYx8W9cU9F|}sJOSpMuuIdTgPbZbtynX z6Ss>N(+?t#6H&PBg0&(vaYJ{O!Bd>+;?6vwcJ7Fgua^(Ad5o$rd!r6<{;Cj|MJ?Io zGfhvW6Prm|W|z!lXd%~p&9x^Q$$N5EmVfVOWhLzpcTzjT+W!1{+ON~(RaEwd4UDC*k zy&bWNF3La2PgC}!B{?~caAmbeOsHEjXt2!)>`LR;wg=L=9ujsqwiFLi7f}ezE9`;n zwNzJL435WmEB9le-6A;wbtz2Dl$F!HxK;TLC9hQKs2X&3!K8Gld4w}jl9hq ziERkL#(Wo4Q-9br3$Ejq@w}z(;0?Q2hg+)SyrW>WFvTg}jq0_T&}{yx_&of$=U`1x z!{bVbkZ&5JZ~=2R==e3 zs%)lgr(HL227K(gtKT?&EM0~2aPB8}V^%qDQG&QY%xgjvf4h5+mkIYul0#S}&KSeD zN85$)4J^m8mxX2(K{dOjrsJx&EOBFPa(QCTVv}P;=u2UG{Eyku50kHcx)z?5*6_4A zCMRKQ(FvkQ<^8;)i1)?!hS8iC5K%0o-9^gm|^y6+)h?w*eV@5bH=8KPrqt=nf1OY4fk$WiaW!U ze+=!U?}ipm;Z9oOF^hc3ck_gV5&51kX*I)kjt-Flqmi;mm#z^tlwhH!>(>$E?NeL zH}yF9#C%C`YvaRp&m9`q$8JqN)AFtq?da)h5Z$vAdbN(fK0+VZ>fO(cH~NzIh``r_ z$Mc;By>+E2T^t+iDxK0?d_A`CKMol>CfgU`fk?hn0#-T$$f zIGZyj#b@r&!S-QYV?EzAmSGFOu*|9W4MjbQGzF^<|g zdgRzm9+MniPYD<_)xT=UKa%;LG-U%#?bPYkAaUS1+n-j8yyXSw< ziRdzA*nxof{WWHaNf8?iD_>2O53NZfiYu`QP=~Frd<5t7Na;Vb2QZuk2^GUfymtD-`d% z&JJVyus!FwZtd4K?)6XyVvtjqnzLC->P37yWW0n{)=Z1 zW?y7qv48dK{l(`Kqc+71OlH(_Tf!j~HkS2}naMdb`23Jt&gJqGK7#{Yd>h=xI?Z)@ zIw0I_oanhC*wg4>{|C^YZWiUNN-aB&W~f&GH7v3YYV8jnf_Hb8a?X6&|P&zFtTA zuOD#MHE!_60TGHk>?_tV=>@R7`Fhd3jN#cwBOkt3zZxA}@q%#Oco}%%`8Mtm`bNLiG~j_UR~prPMq)l%06&ojdg51S@M$toR9wgSddv;!7VbH3?w*}t(>sh%I6 zeaLuo;%VFa+m9YUI2XM6rQluK8+_`5xF<0UwO^`ZO*4tB5Tz(@uCvcZPp+cIHq#|x z@Di_q&Z`Gh%Dmi8InU+_o!wj>$ae8h*bAML9X)(f6x-dW_s6?-4;^kd-(Bu`)h5@31e1dYB<4Hw==&5Iw+{xa* zzuLBVaDvwum8ZvcN43vEAC=@SL&VrBSZZ0UE9p3pe7@pgV!>y-*aL4berOENe^?r_ z@df$4|BK5X5B(H<$*sQli&V-p2Wm^G2bpxvO%7M?>%G@;tXq(Mlk$asfRnxBFV7M# zXoGZ^0*^AB<1P77VUp`4oBbYbL!u>F{TLpz9I3->9uZmrAdUeWwK zepxZ&>(dXB5hGrn{P5_x?PJEvCBb7uAHSpg#HZ9nL{#6csc7d=CK@orY3$RoB*zjP zj+?y**>AO1r83-4B?fGxJqiTr%3n5Ja;$Ke{R72a`Pe~rq6(Mcej9B14`aB!veCQ0 zvyX5XE8QU+f&F1xkM*se-C9v%6H3YrZg|O^p0{OB1B3nEc?CPid4_C=pPG9r zZ5=SF@l5w*{Cz^Mw4dO;!a;&^CiGXxG2S2S*U9-lJo!vjimQicgm8t*VB;utcEQWe z$)*pSFSqSCW$-9zyYCv8YqkYGFKm*<+lZU^r*YGD{f*l*g>~V-a=&(`t6p}5Jo`B1 z-jR2$FIzrVzl?jsNWS)cc>%ka^-HUJqHRFzVLYS8ispzj?WWl~y6HW>@#pjpa3(1u z`?vBOWh0!j9Aa)ic-}JYq2{R4eAGN zPFWb^y`$wO?$4Ysif^Nt!Y zTseE7zf-=fR(VX`#BcZx zw+mWa9}d*!m6s~Yt3OIo)`SiEQt)HVlbBCmzh3$BF;x3~!LJ2zXA2HDsHztu=9pcL zc7$sjuFQ_VTv08bZhO=*PxR7ljpIil#jchAPITG%-Mv5=E*Yxo5#1|I>?=E zo0$^%1GhxxTxXF}xlHRnPrgR=*3(h3%iUJBi>DO23490v+9v$Sj(H_38^y7ynQO8) zeAx42+lTfqm!lHit_U3wxhZ}^TunNy!oE<^Vb*R@^N5=$EmVKTB%DvkUjvihjIK*qpHX)OSR>?xyFZv z0~942V87=aP`tA_FYTvtk$BqOw+|C9k`qNv($R`)aV@(+h;y9G9jhGZdXR_rD0J?p zbobeB^RFG`bX-=X_$Vx6wBmM5@9dRmnTi7r`(| ze_@f-)o}~^r$XoUkMx?yb%*_mm;E5SMBB}_W#TH)DK3q?#IzSRSmRQa*fcadKh^jv zKm7T3e#E#BCEt&HqkZ-M-X7zY^gb)Ka9xq2>3Yv_jUIUq_l`{9t`^b61H>9tlyZRb zgiVk5gc7F&ByOVTf)kQ|B({oL-Y4O|j@6Po_MOgp$p`mfSAV-nZnmm+n+vK}!i$Wd z)O7p^AWpxoQC~=@3ChTbGbg0TOh z2*q?HJAsFn#L6ezOi>M&jaAy(70XvieMNk6r<5uCAqwXe$S=wsDYiQnikJ?AJ>qPf z+^CLwZ8KeUs^!unqOIIV5+o-%KUts%0)Ic?n zkC5M*4+CR6yUK6ZV2bThS7knr?f0W0zBByIcS+2^i11`>CX{^l*P(JkV_H|Z`i4FR znN6vukE9nc3F5=brnaRaQ42!%&_B^^5|vZJ2LvAl4U#irj5NaLtf)~GEfxv65(M9uzeC_7 zs8-m^Ja}8+eZoe&F1vVns7;m258E3uweYOqiX?@(2B9_6Eth*VP3J34c&;o`n8xyu9=(|ICy4_EbeV_frmR>oo?9l26?F;RMKr#r$U*d0yiS2uj+TB`5L^Q7-l=w| zkcu-7T}n0o79)k~z#NCQXnZ?z+D13hOONLKNdBB$ndk9qjxR;(;`N=|gag*<&F&@e~6%2;4*ff@-zd8he9Ik&iG5#N?C8!!4I-6}|z zACf)iAcc2C3<+AaN;pw6RCY;Ks&G@?S8Y@sb<^AaQhrgLm5osD61LC-afPTHltFW# zSy1__^i|2m%y&snG11YggmIBeA`+vnCA>`EnpBgut*EH7zI{&H+%B%E1=~O=AU$WS zV}}Ue$m)f3=@MR^xiT)s@1;}R=5CR`(W!gc0NqL|S^dIx=lHnt(8a{4dJFI2uu3L}XYy(4y5 zihb<0cw%BwPF=C?7ox(g{#oZs(K~Ve zR6Q>^kReL-k6V^FO~0ypt;Fsz z=9}1us3q7R23trjNonFYN z%eUF&OZ}Z!+nnLA6-?wg@+pj?SfU|LKOTr{8CZ9;_-*#n0%Ll2;-iGCKNlrVO&FKx zkhnWzP?0#FRDP@aR&%joB+z9zf(Rixa)a30Szl?J!~>L-{Qi;zPKdzWW;_21KR|#N zTod;Q4+`fC2P%u@H0fv+Q+m)&=d@b#R@N$R5?z%YWrq{|P`fZYEv4#XEpeswRTInl zX9+VbaZn;F1sk{V=jr&RnWxf!W<}(`D_+~Wr)_9kj+STYB%GkQky9yQtX%OAp@(Fw z;0Sw|^0`DI^c1gR$B1erDB(N)65%`f8R<}Yv3R}wy~}2WhkTzxDUG(*%N$u9R1w{m z%0rJfG_}obO>Y@p9$7RqYj4W!40PgzSlhVA@zn|FWUq|5=|hXi)ypf#G~aF8txH4i zL{oQLf8~PuMxCeroZjDp#5uCouxpu580Yxj@)x2usi$BXH$d4fnkMWPJ?C8#7fVjS z>j@yuwGqk5HWKM+Wr9nte1!C#c#?3NqFAt$cmXjMdl)rSzo{d!;@_%m)kpJAW{ZN4q|u=Ai-+E=UXc zi{L0foU@097DOrD32P-?-1(dc`6}UUK3~)#3>0UG7fNNKXvH}dB>kkY5kHf&9FBi8_=_ zd5vt0G)Wrc5TP`Q_6q&@Kcq3DbL3=H045B1%b?Ow>rEABt6vp=&0ChaJH;z)RLaog zmL$hCRfb;R72rt_z9v#lSDk`7F-AlL@Y2*)n92QRO745%U%|yXWveJlrlH9JB6F{CFMuP z+$>67MBb~iA5GHc1?}DuqaZ+r@noZG?@TC6Om*CHiq}Il!q}qvP2Xcv7 zhch>)=OzaxvXY-Aho^;Qu(BhI*O#|dootxc@=1H&3|dwrhhXEVRh$t#Do4Yn6x!O7yjl@>_eNt1=k`CFys(!~-n{G9ey*eeE0pGne`B`T`? znqUuiq(CJug~QC53a*styR z?~Bsvk2U+X9_`5NUIFdIYjO72eRu|oEqo$WahNQc=$Vuz+atE;^@w6+Hzh-)D<#QN zjN-cVgmkbX(q_5M3u&UzTbv@RlZ@j~DNl&i#GwQ?sH}Tsn|JGi=A@daLibad9;7 zEze0jQ9Mu54*x^7P*N}XCRs1dm7Z0+lphvFmN;e4HxD@^( z{(80@MbJ1{6Od39@to1S6zFyLRM6n-=6E3r^^F##^v2Ch$n}ynm=4L zQglhYKs1qijdvD)mVM!lrAFZc39iH!7>NPd`QYy@Ev%od()+Ao>6nM zQc#^;{-8LtC?(HOP*&1Ww4rQNmAWdmj@$I2eZO|J-T{!9=*UvSVTvtDO>n0kU|-=J zXWwPmF~)HQb4A>||0~-^YT+X|l)ohrLr9#lQ5 zA)qm_Wp&pCt&j1rd8he0@*MsxWe06A1xt2kgmBMtf>{?BtC)G*Ty770K0Ak_?oUDGNbHzb$Ruu+NX_!+Wu7^&{;I8`g+SaoGtM&o`6dtH8Im!1XdCq{&R)h z#2wF>$({i3S(-RcxS?DE_aLvFo5tD8yTIElK=NZ4rx`k?kkwBAhDX5XK<1+!n)~U7 zb-K5nY-6^JtsPqxRL&`5SEQ7-l&q_GP=2H$u_mi-eG9qsL&u8lMY>YsW8_*i4MoOS zh`VSCMh>loyq120Gk|lKEn&W7EaN0|CcsD8B01~1^Ei*VNZvsBXx3u(3|2Zjot@7H zXkmnAycutU{emb1)6@gilAgD1ubY0>YAew-tEzyqXXOjZ9+i!+I$4!gV`zNU)X{Rd z^J0$_M47vcJc~PeFkv=1n*4$oMgE8B$6CP}$6QSx!n(u0$zI4&vBm64_H#}kcQX8Y zCX`dnUdTPg(R1#x9cb4nxs@5>k zPOUUnwA7rcomV%uIj8ki*EB6civ)5_s}W2r51);_f)ApE(Guw%j0);qdOa(GHHe+Y zL^Boa6n1~kVa|I_Kh6s_o@?Zs{G;NQX9@If~KWpbzudUu$HKOu!b!_drI@>0GOJYk>2cl=S?jJ+A z!O1)VxgOJuFC+p44#`MeL~o^EV+^6I7<|@5Ru3zV708NbO=hRE2f;l*#U8-E!!BgG zv8Ak98k35qoFpaVr(^P=ai%gun4YO=?_AcZYI)uS)koJouR&C&R==)EulZ3ow_!xX z@a6?==8jG3+nPJt$zX;l2#LWc(L@ZE@S2=O9YYgQ22-ap-@?5Wv)04jCUdTF+Bpk2 zEo>$`ndQt*W;?U*(K{&TDR(GsbvV#*Gn61n~%5PJ5F~tcfRP^skJqHFwZqBEJ~yY79eaT^$-pcbE$mBB<4+K z7kxT&5}VFRho?slyBdBm`j%bJ-p9VlTuMJm8%#S-3#Nz&Q!uIM5tvqFk$DubO?zMG zr$zVJbWCkgw!Cba**vUC(fFkvY)EU`(d5voY*)6u>L~51RPO+S0Y~tOvBnaPrsIfM zJtmgmN|Dnhz&(0QB{2<5cXkl#IP($fCTktLf!)Fy#WFJbGu#+E7-Q*;udzN(1=_Iz_X}R7U)$GyoxOr(avUy2! zODnyd*ICt7*0oKYp-ln?m=a81&99(0C=4zbe-|H#-%h$gQPKL-4OAR`C?k%sj#baR z!@SN+XQVR6vraL2jCHh|)bZ4bR9A8g;WUnmQD6q4KnvG28d$IYt{bko*gdra*&fq& zyzNM( zN+iXOhM~)u1bB8anb#RlnIl+IW*39Um`N|DCD5X&hsigI!|;pnb-3ARBjSSji?PQz z9bB(Fq2AN|wd-o}mDxSbpT`+E=l4$Y3?$%thrSaRlM_*dt0dM!NPF42!sqsjA0`NUje zEv^T17&Qkzds%2sG}HnK+O?Wc_2-`ZT}~Y~?F-sQwts88-nzR@+e&J$?s(QYx_f?) zpE^i$Mt|Bc-575AVdf&D(F`mdcMsQz-$hbUwos2z18JveQu;3@1Mahed4+xzj=Y9W zp_fp5;Q5qAb|Tpl4&vD8Iq35!t>vlN!ypC@0~xx>n(pqXuI-)DPE*IA_Wteb+T7d5 zb}a93>Qr_~yL!5}XdHD>x--Be!*OGq{VSHrD8Sade^eMD->Jw@uPD;P(%m2tGRny&uR5I?K9neeWc-#iERD`9Y)MY4a6#NU+_8t zmb8~lp_Wm7XdrzH{W$#*^Clyn@t6@s+f3g~UqO3H;gcn#hlC4+QMf&rIp|%`R_KQ1 zgE7MJO#eXpRl8p6tyXtE?egjD?p)LLrej~{!;Z5ZJzf8FpX^bpn|g#ASA7_m3NACY z7|o_rNC0IrK%vy3*b1zZgTFjmLbUR3vVcXc0BFYnImvUJYvSl;~!_QJlNBi%f8 zmPV?BbTfd3pd)B&UI3rV?L_(@XP`@P_XyXCeBvI`2QrpMq32Qy>APu4S_{L4?o2PE z4W)qO;}j%CK`g^V*vXhz*tMuO`220C@r)_fI1BUu@90iy9%>$IU#i`D*7XeN>gW#Y zDeAi0bFTYw_j5H)GeL{hdFYPoflTjD{|Ey_bmEHw-s zPeF{s^eEa3dOhV9)tPpOGLbwU{^k4}=Z`PI&OuK@uYzJMiy)P$%vcVt(bIr`;hlek zrd9K*=Sq)E!|TDR&-Og;a#6olC#zR!+%@)Ey)GFZjrR;V(-Y$XXd}WE*@ezWFT@_l zA0log)siHX0`hhmivF5!9$dqLN#E7!Y&-vQ8AYhGjC zg1C-2i_FEG$BOZD35$q5#9QQ0YBS|At%Lf25<;IzE2b)FyUF{>3&~4J`w1iQRNN=@ z8O$OS53vA|Y}#zPZW?8f814fa-94RI=cp5DZ)hi~@2fG|5$dZNPmMwiYW8Srv^Kg& zx6mtwj2?}P**fgDEKMPbnLD5126 z)YX(gS}J7|jYYs?y^>;9jD{#`|0QFEkG!sfcp#@ms*11v*b~z>u4XW7N^B`6Ldr> zg-jVw&8JSMOs3URvG5vlh76GIlHL&K5Dw$+VRO+d&@IUC@LBi-bGqq)>9ygJL2lrH zivSkj01)+q^@DX0TAIE=yIg0htJ93sZPs1TwdtJTwkg0Kg8{TLwwWfFmO&ASLC9RR zBjza94X-CSlk_A<$~jUrWiu^;vV=B^Qcb!Plnh!!K zv)r`BbOF2z_JBV?9>@goz!2RL;E~=%?*fpt^K@}~ioRHXO-Iue>zV)sSPqbkH^7s| zJ(g$Y48#!RTBI|^4Z9ydn=phpi8zuZrk0YAQAbeUl7grwDObt2$c+R&-W5LvKLxu8 z{R)mU6`2GrFi*D3HU2WZGs+D+4f72i;1zu)uoA4-3&1MfVO<0esJ8`9>zC_x>)+|U z0khr!EC;Fqf0MwNYid{2;<;k{1~yJ5l3Ek(8HISIP~lnskDQ zCMk(RLNFGDcW``^0JRrBYhP;aG2+Y;<3QtV<2i5&cpWr?weV@$YJjf?043lLBmk#$ zWA&HySl}42Q$I%^t>+msz|WwqX$t&H>oBw!nu8jFo`X4njmI4&GDs81DWvV>Z`2Kx zH`El$N78VLk<_0wk2nWki=BoE$9zIr;BNx5%qJ{DbCPi^Jg?>&h8dQCS3n3%17GQ5 zfs8&H4x5d}SOdlQ3A}E& zVAud&2M8e6kOn*lzXAjF8ej?#1v~+8`o(&sJ`k7zyw`{5BlKB@5JRhBg87|kDddTK zj+~2mh+T*OiJwOV$m=MH)Uohek7(LH>OZsv@PCe;@P6b0AqO`Ly8(S0>59sOK3LwG zHyH<+jE3(9z_1m}1h;~l!8IVxfH&OHX8``71VkIw>xb*vz*-;#NQPH2g}xuq2uuR5 z8YUSw8#$I-a|&V>st%Qh8Gv(zKgroo-cB)*{b*#G8eT^ODF`Z^vYYHk*5U=YJWLNJ z0DThC1HFOQp=eV!T*E{I&hQlc1<(zC1~YgVXad{}Q^BEzukh$<0D^!nphjP*Ytfz7 zIRH)iW<4LU10==?#{H&i@HYr|k!bWbObRZDaDb#IJ|~w_57NHT)2T10t7u8oR;rA= zj95b0ho6o6ioSu0Mc^PNL^7{4tu>Mi3*nVpW+($!gB9RHum;QmHyQRAJ_5F&5Zn&l z0ps+W^*-=Zg$$g~(ePft{)(B1dx5tQO361VXqqQ2jJlN0 zrJtv7prI+rWFJyCaT{Sbb|(5fQjO#x8q5eY$9U10WXJ{`497u7Lm=n{ZZ+VHfyN48 zE*NY$Xjo=UhF8@TAO#2jz4Udu2;CdqAs|{G4I}|1aHi>usnGHY!9hZ38Fm$ZD3MI2 zQ)9?8sRX)?{)(}anoLQcT&1j}Y$6QAjlc$Dg3zlGd!T3LXQt(*7lvB+iSZuX1Db&Q zhLeU%hCo9+NH?O5YNNpD41R-q{luUHW8oESrS3Rz7KqTX^d$XgkZo9HIBuE??+5!K zN21!g&ItINqbKlM==ud#Ibk)SBp`g{y|)^?6E8|hZ|oR zYC(!&Av{MrL6U)NYy|P}E^n%7m#Nw?#}HxA8dHp8!DPT0_zKJd4Z28OjIKbp1Xutd zKnu9u=wv=>8G{T)oyXMSw&1-;{*-0ZR+@k|nf8o6i*c1clNw6iPI^LGK)ivyht?po z;jeoho8yd|jd_L?@EUj#o}(k-o-YSQ##p1$L^h#~hm2jOLepkA8gIe#!jNHYt$ON!dp!pxt6jWNf83Q}YW`@-Gk}Ip z(*UCdcm|Bq-_ZXAvURcABpp@17)m{8fAiwgou^L4@*6Hs+1S2!Lcb0?vk4tTeD490Sjl2*Y|)0{m@` zz!GSBZCqg)VvaD{8{YsOz%Iigz@WXQTc$gy3jn%xFZ6p1j}56NlZAsgfc}al;5HJF zloZMYS`0j^Lh0|||4@o)W=b8&ja){|z@achP(nlpbl)=8P-@s@m z)_WNyfa8G$hFQiJ#;2xu^IX$5O9_OuU=XLwuBKs7lj)Gy#Cz#Gj`-Blf^ zT?;PLX9I1f111IJj_g6L!QCO~2r|-rS|BZ(X3Jbhb))ZOBv8&!`jIm5KX41N(WuuD z7b<~AlCx={{v9|ST%g}*m;}xPla2AFH^y_OMkvHG2k{6xW5Ocypd#}u%VKz?o?$8i zYjyv?>xxD*9WdwyYXb}yfO!UqWxtt^9EVO^ce%a99EKP{0+ zoF&zC4q9$nXk2610esX=1j%5N8qkl|-_W_61Ym&i3A6w*BX6Rea4!hK8|7@~F}drTEbCE}c=*xUiy8SqAb!wW4=?*|;w_=4;8zPfd$AcLQ2BeWFp2t6HZ z#*M@uBhRJ`A~PA;@-!Ioy|7pFk}njU*sn=3(7^kL9-Dc@|@+odA?~_?5Lbhfyo0!pT7g^(xgZuH zo1yoJa@eOGh+&9p=7or-X0>UPWxTNxj{9&!nZ6cK>zUeSV5jc8W)HAK`$6w(I0alb zAWUz}j?i$F5$TSaM@nM$hePKKzf1+ji@;FRIj|0J2Q=C%`UCpw`ght)EucNpGgSS( zr?fj?gH*FLC~dvgpx+Ew3^$EImJGyB6o?s1pb{OZk<^Whfy|Mtft+ymL;ewd34b-~ zC;bb@kH4GEBQ{VcLiOaKlvU=l?Dy>%ngr}%4P=_sozu0x{gisO{#u6)7*M@XPeAlE zoq&AnzV$d-q&0^TC)&q1-_qVMYlqy7Bb%PL4(i<3GTTI^Tr1Dj4u#A{l+#qham;dV z2ED&`F#jR^=B~?D(*J z7S(3pgX%|iM_4A|&O;9wM8|aM631JPCsbEP6*zbIYadQ`TQ&wgpu}sq{D9NrftmaO z=a=AR3tRcEs~zdmG`74Ga`?5r;d9S|%!L(~o5khzwT*Q%>YH2K>*>`SI+nHWZltz` zmigApYSU`BmfX)O>#A(;E|wSFNDI)tHcx6P?HS(qgnWm0QCjA7op{mb;>4Z(=Z>7= z?LJ;HX~)Pnld^q%N7fGoJdUUq*>t&xIfF<~)R$YIV2^jNX06REzRp%w8^2GwJj^lYYMhjZo$>p&q&GnS&}@!99Gp8eJT2SI#0V;pPMUBs;ggV zOmW_7|E?g_cwp9^p%LSz`Um=LU3YadW9IG+HFLq~J0|7%>>UBjDDyK9g}M+q`9AN4 z&`qw-3nC0fcWAG==#Tb9KHAQst-379F_M;?^q7}MQd1x_h-d!6`sV9bK%*7hS|Z*qrb$t#te zxj%e6z&2rCNW-F-AuSIyh;-ND;@4@RGh5tabWyuM`@fo8cD&rRMwy78ZvgaJKDFFG^=uD#~$^(*81XI z@#$~Y#nPGvW{>{4C}DlfYE4Vy(S+)vi78o~^^MLkHFZbIyUOM1DIX@5{gZzwLHO~) z-BB^J#@vdB-#5hTDoe~PnF|?PcM%0a;7h)HdJOcd<$Lc;?9>~7>MU1@f95<|U z$;s6|JBVu?S34Xm+}$?6V_EYC_2PzUNwQfYrvV{0h3vHE33)B;6`k9P5NX?zv!aHl z_p98JF(RWWkCLUW7gxjShMQPs-UUOgDQtt4Or-E z5^M#-K&IiPwzd6O+U&69xjAine~tS-_vhQxCv}f&?`DK%pD7$(T~_3noKktTUX~mf zJv7!W%c= zda&ui%5g#OHrr3#JOwqQa&pMPM9xE;3o}*R57$`COI}nR(3M(Lo6?!!829$qz4GFm zP1%aV%LTg&P8V&d9#P6lzmO-*>z6bxYj%EjQf9=$u;GbsbLVCy{6CJa0=lWKSs#lu zb$5d*P#0QiP#j93xVyVw+}+*X-R~%N_t~>&zWHX( zo|zB3o;H`arw#UX(sH%L$2UQl%A`l9b}Cvjao)tU<7Q0qUAAgwz_7XFDkshxwW8nR zjO9^J+Lgo>2J9jix9(C$$onYwDcY)B-fa96B0Jr%qx|%bmz9&mGnLVTWq;lXaCLRV zv9G1orApVo$scF_C;JxP^!RV)>&H*ierx3q_?K#eB(26N$ZF5~?R+Ai#;nZW-b*v& zpK-AxCX6;r>^gh?_@jdpC#)LWt-vScQ}V;K^TB1#`wW`aY|c2_^15jsxq`_mx7w=8 zV?X{@OcNavF!iEZKZ&P&PW6WdqwKJ@A7cXrLW+I{uc zf9z>KNHn<2WJNL?qrx%|=U?sRmfvIO>Is)7+!_%%N<8Q22$%kQdQa+s_8pe$7as04 z(D}RH8vMT0U6LqiFY;;B3fEMJe>OCJY)Wg!#lQYsX&TkESy`s)UNKP6PO^}H?5C)_ zqioC1M_;DDo%ius!^FxvrO)5lYm-|0x7<=MH5}j~{=Y((B^9Mi&%QnI`@rPkH^+@1 z=P~-huxn#y4QwjD+wW-#mi{oTJiJ3>DC&azCw(vZYV^|%YDjy3yy?4eWMi=8;g9qN zfh?a-HP_UxmMm2@S1qYQL>jSc#rvPx++5keHs3=xOR+aBtGfnu!50)C+Z7@JpN|>^l>KWhjrTyZML%X&c=JDf+Vd^# zUBA)`^?PcgYaC@?WQ)ur(PHjcB0S_+%;v=Qg}=L&_RAZ2XZWyTnPaX`NF5eA=s^F< z&L?|!%}xv+8SBq`>;8{BN1tbKG!;W535ob+2Sxig%BkG~(W{jU^{W^eGxY$g}^lPIq#rMh){{7nj7{jh!$rP}y2A)dTUt@(;DE z8y{DUR&1*(t{Bj=p$eB#@;g;uIrC)kq-`=01g;{T- zC%aDe-(|VrNO4?M91zW`Nt1N0mI;yrV*iWw?RF-|xohLF)Pc4k z#U+Eswi}z$n=^QQ7jyTYg=v9~jzL}_ysy5KEO(6;HGZmrqVeKd+04I2epb``%02oc z_4-;$bE~>kaZCB3I=nf!xus@7)zhY7aMxy9<)QyFs<&1B_j63uf?t)*Llrx94_gOV zM|y8}J{eV%FfsmI_vJbB`z#r}W$^S-Z+m5p7&*9CkFb)of_M^(ps{#<>hdV0eO!P)vB+N-8Syn){7(y{$K|G11D zncB|x2F>c;XFzdL%}}RNCvuAVpYMLX^TYzb(1hq-?VO#4c*Z)Gn2H^lDlgGBu~>V# z?sk2aBC*O#n;;!rbyc%o@LH6mzR=WO{6Vp;W@}Zta&P6S-~J7EKA9@l*X;bJt+H3| z65SA2NJknQs1%n|L>Ipi9sXI( zuQ;{*Te-Ml!^abU>+1IZSy0!m9+Nkz!gQ-_2hk9}$8Pf?t|dN=dDG)ZL3AO1NOa%L z16zBEil>w;N$-}QkXG37M8ai{`#vr1IcP8v+44(uTz5A`jXjo-1Jw%utt zpja$hCO^~|Ebv$TskI6}C^3G6vQ#>y5mkJX+k}I}1DZ-|4#;NIdj3poT3WWW{AJzW z&wt8CHT7sR3c|#7dcI{eyPmVo-4Hb}vVHpOE^e7s14i||)z4lc?iXHsvB%=x^9zq9 zP$@`^HgrPZY}Q!T3MShU+km`S%ik}nQybD7J*AKJ?)GZOOP2RuiGu-XlJT_ z8QL?K*|UQ?`sIa^$%hgqb}i~!*2O+xcHf14UiZl^^zHsFds%iwQs1!g?XCnIalgeq zW&dnbn7M|1O3*3M4oRij(D=J?pW;#TU`3v?2mifzxu(7Nu%K2XsBJGHgmZotG%l%* zuE^y({o3_+lyE9v(Y#Prsv2QTBVu`-oN|KJhq`nK?eZqEFmFlsSKT%h4=gMw+}!JV zw|RO0CFl}7J3J0A2rA(HL%0x=8NBVO`o6-USgO4s$>0}@&xuCMpQwrie}on_E`KNa zSN4hjP5eZ@wx)l>z@~#0F@oI6q2CJ{hSpAQY!Ot~Wl53@K8|@d34X@8%BMqzeo^in zmuBD1itUBZJH#?uAL<;ZmRe>xH_l0xZ2$Kvg$$gK*>c- zWsB8N*mfB;`%VgI@fSvI>^LQBe)q@O!@36bN$$10H$N{TFQMQ`=KW5)+f9oY9?J4_ zavDQ!uoXB7j z>yVlb4ZSOW{!rHI#p%)-$$5!LKLNeY?ZbWI|G2>6;oW10q{rtC%sr65v1eFss(VUze!=3rE7>K<#c|8S=Y@0$ zI_Bz$&$owIv#k$}BUH8g7ve9fN2dr_#tabD_I)&=wA`Kl8 zJuUWG+?eE5xhD&I74*$3=uy-oKksnX)bybAWr=&*^$0lW0ks|OpU}>h+}0(AR|bK8 zx18U&Ks-#9uT7T@;}=R7C_ZZ5%AfNKBnuT<$)AS822^}VP*wZ0e0kL#{z1Xmh8q=< zI*Vpy>umif!*r{s(;e@y;Gz&!sH)TUj1FD6Ih%7&6tKF6bRUtsvGaTU zF0|fT!98Q2Zuv@i($jUn1ly#$wKrRiDfaW{i*_iNt2e1!Ma!l0RYRo#l3Y=x_?&P- z)78dtRYx2A1xEji z@=Llf`5W_2XUIF|#%khQ+ZFrY;_bsua+b1e*1lRdl}?)lcd`PchxqGcjf!3^-Q>T8 z$7LefD|N2Cp!u@6zxbH=U2R1DLXoNdWZjPPzZJ@6sd%)gPvf=bO4BOpA7%@+fpyV$ zWte-sIwmu|UDlBtcD^FdFTXYaP;N_hR$5WYrG&9D!^46+13jxe7I2nQd-T5gO{PY( zT>ZMKhh(ZUtL2UQoZyeRUcN$gO4UzPD7zrrBRSFhz45*{pMSAIS2y|3xW+X?|AxKw zXlIx)^q112zN5<#e-_ruS1RILf@Bv(8)S>5Vnwh>(mY(e zo!?%#sH%P4hvuKPYwB)QUaNU2>#dS2$ICjm{Ghr!jdb70JMTR_GA2GTV_wRbRAt_b zg64d`{L0)DU9M%lOe%|g74O`Ba>!t>Jfbs4!_H+gO_PB@Pu=OSh@^=`M=4iFmTFGOpGk=_5WNxg@?TnbP=0@LHJGu%lsNxxVI$aDBtg zhF!IZ{I!}N2Fj3Xh^0nwZ9aQLeFIJH#w28=dF18g9PDB&i0JljUTdx+`)Hap^-;{n z&`S|^f5ht=r?1_URFF%p?X|ve`>#eB-=dTr7I36DBrDZjmHV1U$ebl>q({WtgD*;?&|$u$m{x0>s@VBL#7SX7P_k7d}}%zgj99EbJrr z+>q8-qGTDJ%!7<9YYifC_X<_`9SxitwYTHs%srhGQlm3(=61*q?_8hxB6(Kor?{0t zXCi8XD}BDZIUEzo2y7gk+qM|)0p3N7PStLOemGXxT3|fVX-o9;J8!_dur# zxWbkEteOP=2EqKM_l?5pyG;sZm3Ff_Q0d;Dly2Et~`?B+PVlHyX%xM{_9buf_HcoLxGGDS+@v3!) zyn}d*bi0_R86l5tJ}((0n$^r}ENg0O{?QoP7*Z{$`7GWf{UhGpyh#?{ddp#F(wH#z zSeK)L=c7gKsOX}E?x}{X9O&TFBg4^USNf=wZYg`?y2odQOFc{dsyrV!Z(?vKvErZ}(;gG+W!=S4oTwfoW@Mwq@y#Mpbn~<3V19j*u>MHRyC$pPkwC;( zHr*6w=`zeuP2JkAkQaEL{d@a6`)5S#jNO!SJ<~I-K7CV{U0EwKu*uQ7l>g~Qw7ST{WziB$-mk?hTPewvG&kRtUzGF`9&YX? ztY|t?hc{joxbn9&a%=ZAY?k?I+N-B4JL%Wjzp=+Vt9cP##Q}fX2Pgd3$(;B#O`iEP zYgDehb6wiL%uNZZ_!%9FBKL-$_x<7&;PQpL8qcEUwwAQKHRiO<)J&3nkyc9=wgjo` z#ZRPnL_zZ9@<36rWMA`L{*xx0&lAjSTGBYYc53ZqzCzSfk|Yk0PFM3R)1i;#IV|1D zCHWa#e7jyZ6jHS)y}VvZraRm&-ZWI z)cj5PPiqR?V0-3pV}*Mn{%-?@gr!AIPY6m+$#{_woHa08(zznrKhrs7Lb4+IOk_;E zGeOV&dbm77&tT422Hj@vr=6?3qWfSpsXj;>#IMB@wK=Ll@d zqp;4WQO>_7c*u{e+h0FaJXzgO`(8C!d(LzOtAeJizgz}-WraP9&*=EH!@Pv`sq-^! zSxjbRYDmVNq`9siB0kKl#mxcIwVaqaE&2`_I$!DF4Z78w#$4YDg@TzjKsm-LFX zw`hj=xS~j66g_M1%Wo%;@N1e53(5s#{m{CfO(L-rbRtGls_139&Fn%(Fl&hdx4*$p zA`V4#Y(FF}AbE0nSw?tTak?$NQ_`iBX^9zew_+_pt-ccfd!Au#(}{HZ9eXKhwG|n; znvwDunvSiDR8I1Fk{_Zqs%46YVn4}x;kaf?bEaUH&|m1+xURmSku9hfEf!@8Tt(*; zMyMlb*Vf%So|)uajlMRZ|;CV5)&*W|SHy=k-4x25Y6eB=fv_O=Y`Jkx#gyE9nKrUilTLGGfZ18Hj}L-LA6s}rV7#KDo4t`ih1JR zvO;N&m=BdQ7n_>+mHYs~IetOI)B3$lNzKQl{bc`1?MheudTTn<2XSC0d9!@hhjfj& z64@4=+i7k}ar%(7O{rVc_N8c(CMFH*SQ@i9VoK1OfcL&^kB!`3NEs=kq8)qM_UoUh zzo^f)RBC1@-%G!Xr^rUiw@OcmHwdo_27q=43u^e!8_D{s4SSn*3%@qI3U4)^lkL(D zGiA0NGoy43JJfxV&mzBjK|dmo#|aXglJ&`#Qkzomr9Dh%r`9EScc_b=(#{fI6uj1F zmg{X`V-xWj`mMF8^|a2^s?bl?bX3$zcgxL+6nUd0RaDlzTC}aXS!fY_Zo1LX*x=hV zgny0CYRYb!EgUW#s7lnV*Pbv$+G>&4+|$lK+$6r%kcm+haT_}fNa&M%E$u?a>GaL1 zisW61c^x~%YoWj0+`wp``|bx_IZicbXL77{jpbq6saCYbq#C38p<1PSuDBsRE7>KP zCwT!;j#BWPf0=)Ze~a&E(lusCvLL|?snG?UX1|-P`mgwA~$Mg%%(VxcuDN#nB1s$k+z8J z@TY+yA8*fVE-RfEbLV1@>BWvDTak5-`K(c;AEt}Y;re)8rDmMkNu8}$sf{X~e2Dyu z?7sA`tb^>mWUFMf#41%Onp7p~3mRNsZrW@AO@%U{1n%_KRq12*D+{E;N zy2G@Kni@4c>RtOc;Xi{H2Il+9y%%^jx@_Yn5lay{@(|8FvO zf+fNH(ePgPON(gVYSo&Nsve4|@+A3o#csv4|Fu?@U{ax6A)g>0q$pR+QWxkt8hRK% zn#Ne4ki}R8w9Do?hq-_DdKi!qbU(NttTyags5W7o~>V^wX2F%vy>|3bd^MYTh>>$Ou9uLtjLw^ zlP!|b@>2CW%|Y#c9cB==owXgNjxw=$JSUVl-s6k+S6?L1C1_n}&&acp-y#zuPe;T= z)dMEn4$Z(<_5S zZ){o9vZJL*Q>W^pYE&2%?NvYJ>tuSFzsw>RD^AM!in+j-v@MAKW^1*n-csNQX1Mr# z)_tc`*Uj$7eZKog1@#Mg7SaJaY7|Abi(DN}gnbP~Lr(>jLUr#uPpR7-7ZcY&sIhj) zVrn_uq1t3tn=Tu$)|q;3%N6Ys%^8(NS)&ptb@EH{`Lbd;tzgM1*#Bb11@$&fs`k6? zSL;pFKV zj`wSim(J(87M2BX#vU?O$3t7a`B>Y$wll^Xt!cVKZ8vR9OPY3=DpA=>p;W9^PgUNK zXDHUn@2P%kD%3IBbK0-Eji#68iPo`Dhcy|gCeAvMJf7P&k8)3~Urk_B(AD7iL9*bC zFt;$5&|X0X|5E=;{=gl6vPz_CRe#mI*Hf(>jDOoC7L}bt&qP(Y2WJv*vvXJX z4&HBk3jO=~M+S@uCPL_$2Um;QWB6zWcm(dPv-^x_;+L+4Y0~3qm^4Jo2V( zs+DZB8EuBrR<~AMM`N2Peva5X5P1@hOwYoE{yN&MVcDA4P zS=3FW7Jtmz#GUCf$?ZSSN!~ZToBiwoGlKpNQUojx+!CY>d=g;yReDeH?(6l{eW?rF zu;MhbcHw?#Abp7pcC5ATHhZ;&K-IufeSmI7%bk{2TA{j8m82px&KjX|k!rB2PQAM2 zjCOuYdwq3lZrfpVq_xg|f?AHe!;Z3oxC@<=-Gpv0JlFfg`=J5L{p+B+#qU5{ASd9D zuh93luh6@vr?Xp>%UtI{P6OB-aSAarhbbN082i^U%$#XTFj)1s^-vSs@?GPr>8FX; z9#P*?%~qXMjnO7)=W9gTS1r3+CmVkn>)WEN1yDiogmJ>StdU%$6Y3&xOY&^?p6T<% z=b@il01-GZV6Wd3zv2En{hEB7yqcj-rOma#`5RZm>JIYuN4RvogRnoht~GBmjWj+p zbZRZpxkB_ar{%VGz1m0pTs2oS4o*H_#IWwSw-~f_I zM^N654C_;~tGU^9%kW7*RkugCO*agxfdp!+dVw}mvr?U@`KcjW4(aae?D`{yIc-6f z<<@y{C-E+HoGQy;^E_IVy2iCNp zrE|-+7M(83u+@0flw(e|j&-c2N@-VgEB=7p0@aNM=ZS7sk2=rAUI)B~`Q-Wy_w(}g z_tpD+_Nnq#dGg&2ZmZlzx@7Q*IDJ?VP#yD{F;G7of9z&!PxDq&Z_^|rrC+4$(Bi6l zs9BK)E7z^r zZI=h*b-<^q_Xe+U?-C!I&n$1KNpk0V)Ve=(b#mUqHE;_#?!*bq%J?&EdYApXm22gj z-GU*IR$-LR!kT*EF4=7AsKuTPxHK(q|dowMH6F8q3Tt;I{j2dm{OmS&9zA z(uq0jBTjF4D&7hgz1w`x4xU#$CV2Mo(t0oQ-r{NTIPN*sQw-HAH=JXfDP9V>Lr!dCAFiI?pS0Sp=1K=iD#2i#*G`Rzgi{nCBR%vs~&C=H}#D z?#gkAjyc!K-Eb`dk z5$WaYImlh(7Vo~$?Yzr$=V+%d+<5RCDtsdF7Ybiin5ml*LYCcw3!o zUAww{bsOw{((S7U*Yk|~YxhX#h{kpQ4a*%rg`Y-`(R1J?{~G&u z@YY*RuS}y%Jq@DPc>QtR!qx^|y6%PUo^C&!u)4L%@Wm8m=2&#rYAa?-RpyNJ`78v_@#&$>W;h|3AL0Umm{$?h#~zunwD8r%w8H@Zk%crH0SB~vW^_9dy(5FIsK-(NJ5+H-@yWGVd|( zwq!V(9CGp;Oa~Z&#SwnQRMstaIhS<0#QVs53RwEbHOp;}+b7op?uG7LH;e07*EH8% zP?NpX={~oVTf}b9O2!RHJCtA+kvzv5+ivSAsQgMb<{E>Ii(6N<;`&3&`z5*#=!l(*|&Th0mGB0WKYCC1jF}Agi z(^r7^D$!f@cUW zXy+_mobzAThc0H9DwjN{=iTla>N3@Nm-7ne!Mx*6*V)6_4z>apVh@qm^i*a9wa9+K zidnra1I&%4m#x9Zt;T7s;f9g=YW;cRN5fgeaATust+~pgwLG_+c2ts~R4|iFN1=A8 z%N$Eo67$)C+&|pA+*RBl=gH11on2iv^A5u9BVBx5hB!ZQe(kL15l-JZE!=F*FV;Zf z06G=xhIXa*koERn_7yg7Ysa?Pri8XE;~rx-!#jPx@rWVG;B0g;zBIM9O@w>&>#g_f zukB{?AbA?9!&?y&ZN@s|eAYL1J$yX4Q=A?;{orkIO5t^O32;8=ywN$*nQ)l~H;@lI zeQ+A+L~~L&_lc9lazcidA!q4WN=43aIILb4uH|`~TiZrcu3@0D*f`jbVeDz>Yy4_@ zVw&2v(Hv?$W$k76c4+L~XlIyxk%m4*WY}v$MjT=tW7V)RuAX~>JDt0b7si_kU6Y)6 znNVH&jyHkl!YkpOauRU|adxsR*dpQ$9)N|x43{VLdbq8B+&0i!Y>}JynH0ueP+j-P zSZsC$C(_a%WVWy40N~Yti2qgs8Li7{P;=&+E-A;EA15o#MDD+&)mLlEx~-{=-OA!|bGs$#BOVsBT(qDQ)wG zvtDm98TT9BL!IARW3nm4^bEL2xTV74VT*E%cZkU6)LUv9l7Q|;?_%GvETS{(9ZSZN zv#jjroVO6kI5@|gdOKZ!s;FWof;W=)#!1a(L%nA-cRXi4%Se!fFa8_-$jqWM=xgLV zyUChi4Y8z{oz1bPHe--!fw7zEn=#8Y7^)3loBLQMSTEVC?NY}*$8%~aOn*VqdPI$u zLG{&cVh}N#wT7)_`*Ys2DNZqWD3{_+=5}-{b4qdYbn4_Z#py41C-)iWEc+4r66-9{ z9#2Obk&jG&Sosy)@BU-$XWeKHZX4Bh%5=!o*>uEs5Rh$jTU*;mv$xe~y=m)eKkS%D zeWI-NI>ry7(S4XdJ`JzOtBEw0h2_dlU<=qO93!WiQ^r}vUCFKD`Z-}vY#8tKlv~Yt z%9+X8%znZOCC1|qLAxD{nR-t09jon$ws#hr*~h%NExB!pX`(6HG{|Hym9z~to6JWn zmDcMvwY}A0AQjYDTFxktt!NRJhb7|a#0#RCI7-ZArLdo}zq7ZqCv(nmL{R^x=3Ljx{9b&HkFp2nWQ zdB^tPSUJ}?jqnU&PBSN(yOYzxN@HW}WL7v)iuOf~h>96VpLbM4?UCKK(z@DQW^Qk8 zg-Xgs)91ES^Hg&O%VtX$RJcvE4|JFvmte>JU_QLRocKXJBAke; zu%jy0C)Pz)3HvU)6UUpwurYl!Wct)s){m`l#1vgp3dLgXe)u-SnI zVGppLxF@c`7UH*X6QHRFkxPgOH8GO)mUWwz$lk_Yz%FI8Ie*x<*xlK$zyc?;ya^2N zhYdlmAx}%xz{nobeo}HaliL2z6*myTI|7oJC3LtuRaI zD56Imq4%+ASO!*#VR(O-m@^me3SBQ^hyg?yv4Kz!hgr*5&g?DhA?&s6f7p@i&g=sA zG}cujj>yA5Vlp%b@nt&EiBO@?-|@rdWjh8{FfS})EfwYr%XZ5l%WP}7ZLMuFRG@Ll z0_bc}LU)H*Oq-F9=tuMpnveB{3g98Q2|I>m;^W~2Tk(hZ8>mR0Kzt%5!yKL0EE}tl zl>j=gV{K(EXB{J^5#GdB>^%A(vY)A;Us1itM8_=q2^*|qU2VBxxoR0`p)4s-%iGb0 z*>~F4IQEh<@*DMtj$}3<&B$A17kUfz!+v2qup;a^?EC~sJ_fJF?%-ST-}ogWkJwJQ zu)ymn z2QMIkh&x0vXw@9n4c0!^CYFO0##&13B>LfO{0+uI-H-&vn;u2&CVM!#*eh+WHnX+L zS_g=E9%`Z&*y?Tmc9uf`^`>*EPxMoIITH@^nNC1&jge>_`Y#%R{lSJ|x1fr4IEKN> z*D)U~6=&f&cs_vcKwjQ>@_8Pl~BNo(c@0>+Go0~Quy{YB3qBSvMQ@^$&>hSnx`kpx)w0YX zvXiz5d$?_*O$gQE|Je|Gd;3a<6B$T8CEe*xw3Z%^lrwLTuIOXL72{y1P&M?ud5MO= ze!bCms1xRc{)2VLK0x)%F02`|<2)jONG8@1>xp)({lqi;FwqVFhQ;E2Q6(}8`ASF7 zUnvE-9_lF<0Gb^GG+l2a?4xaa?RmAdvI|<}1<}nS_2tO-Lj91*Tq! z(MRYqbRA5;`heU)y)hBO!hXY^((s?q-*pk_x(@oa>?a)fd3cJU_;{iTBsBorj7&ll z%zAnk=tih-NE+`=ysQn2Q|co_B; z{f0Utk08ctp6~Y4CLky^GYKYv9>rSOsQ*y2mT{PTY*=5DB;qAB;~2 z>wkvEq6TCs6U5x18>oNDHKfYHx9@j6u`jmwb4;+OIa(dHj>{xUZJ|=>VazCCVPa%4 z5`osB8srwN(Ggt@S{{miLJY`Akh8*u! zv=_P+ZI5Zd&PQSW@DNx#9AAbWz!sqA5e%(^X>(S3Ha(BpO&z5&Ne_ln8Nn}q$@|N(6^xB%h7L$2|0yKMVgr@FuiUKGYLszJ~Ee)m&^nB z`#s1e(0?=7{W`1wJA~!o8uSP@2P;CoFcx6e52fGG7;})h%3Otcc}qdJZAc)TawC$9QpiQqnj~TtP^$%dxK8HTmW;nqnBaAW(_lhDX06=d7x)AsjZZS+z($S zQbgUNGUUby+XH_(f$E{3>M-OlbBgK3?59KMvGi@~B}g5m znyEKbBh?F7%PV>`=)yZD4kigMLPny6U{j}Hx1OM1uR)iFz@)%h<`GOH>?UZn8#Wq!f~KN(Kqj8Z4)|9cnX7bXx+i^*l29h< zGF3opsRWoTsHG>t^gq}M^Bf|KYaruJs0&OE)FD!+RNRb=2QDdr>4)n<7v4c%78k~Y znaQZ>ZNOAHNI#H?5TTG|=ygCmKDr9fUPHUl{lU)U z)M~I|E|b9&G9Lgpx5Mvff}SmH;HxqKJ-b4*QHJb*Q;tGDGpAvn35+XKMe~?fbOW6Y z|Lz#bpphvAO)$f}#Uykh`VXM&Jah><2cA9vJ&Fu~8IW(7SqukGsEj@i*82~=g}zMx zg6eA*=*@5yW>F47mLne#FSIk-2RKC@SW^s&qSujaNH#)(tV@_IhRrxJA3!gxASsHD zXO!S$#9-sSkX3Lh)yO=wC*bBvu#O@)sq4shWGu|Ftbr+$3jj$cK&*F`4rVUWJsBSJ zlitmIVv=BTWE}jI4B3qSKzhME#xmqIX!sMbssEkw7v^7P9uo9!a8Q?QY#1&$~diYZ__<%dm8PxRuQ#bp;^vGS%6QM8EeZHb+ zFz0~(*x_bHcaXIiVWZyQmHt;-y98Au-O$_cxBH>nkNE%RV7fB@(%(T=ujmOd2l6_6 zIPmNedLqpDOoaKI1CSRmIg*XGAR@#WEk&vT`If?ONJH$PMPHc{%u>+wBCwAupwU{G z{)xk!%>7_ZBCzIQU=v$G)`@5Y%0=zS2|(=jfFq}&@|q1#eUV;B_oMsJUzkOf^=D)Os zZlzse(&i(W51k4>I}|w$>;H$mhX1}njv(XU_m2c!iGV4i0>%rbl^U2pnCjXEzIPtE zg8V@oNF3~UENIwlbP8zMAh-nn8YYGIf$6%x0XNfuvz&v8n)ARi!vGm9R2n@6s=e33 z^XTc`Fk|yDXwv`mWe!MqF#OK@$VLRP9VEU4)=q%mGzq@j1Id7l#u?-|av3QH$#}vp zC%}4((L><%FQEg#Qze169YW@UoL+(F4q?jZQhGP!1Jc3UtEePe2|g}?E}$>a)i7(+ zm-&Y|1oKkkU^-|TAW|`M5IKfy0txI!*1`Yx!}QV>@JvI2&wqyf%8)w1-Xg%D0l+-= z01jM3cY(B*11qoqJ}d`66_4D4UO@$njot&N980UIIJ%0eqG;+HL?%7x?r`Rh!NLQX zZ7`4Z4C9K(nSNkrImj-Uu(}?s{VaHwBgiAb+Q*0(IftMS>GF}WDCDqUuUo+uE~9^e z?Op-=N&@XBK)3&eIjvl<$jOWsXz@)t1u&3BKLHFpMm?tV)FX-wtTl(;3|^-WbbTdE zf_()0mN1#1UHxF?W3bz+fE15muQZ&z9niKOnF^WRT#)WGbRRGv1@P+I=r8mbp#Mh5 zLUut#|MP<&?WfFE(CugRLwY041%C6C+Cw4q3&^g%qRzuO^dfp7Jp^W|{-!+um8LO! zV76>E(*w@8JM8#2oc1e(L0S+JNkJ=+?%+?l!Y((#eo62yAAnJd;Y$Mkb}a0?1b!w6 z-H9ZFZX5?+m%+&C#q=UNiarJVd`vB(-cv=?K47hXKmsf}5xy%0d#|Gp!0g%U%>OK+ z0AxNGxdb-W40aNaVqiVD0OuY7vd;p~qJ?NC1Z#smyI`5vbMzkUI~ydP3_KTnAg~b` zGZq-E4DgmmccFu!Q$!s#jmn3!o(t1tZ&6pk2ZhtU!4KC0I!oyH%s4>wB#7WA{J)mH zg2>nt)?J1UggLfiupvDfhn1oOFi)s}9|Ccs3)TXa@b`dK3V{I*L#3c!M*x9d02603 zUqO#{fNrrsw|h|4)I6#ib(QJ~-Dz%8@2Dn<09)?^nBD-WevKIpQ+>OG?JfuVx`Q+z zX@HKM;WzI9xemnsLvyi#7=>=c24T6_P;4~j1uVV*+YaY`5IATQ`V8g{=OCw;IZPL( z5qQm5=)=2};!{tlXzBtLM@0a?8bmFic2N&O)59P)pA5*-0PB~7rnfKy0eeq@7qtUU zO@otN15x5nlnuO?Mt4ADJ_x&s%?A!Q3)_f=Vp22`8wXhS5A+pi0M;x6?>vQx2Hidf zryLE^o(h_^0@%e7DgoA>Pj!ZF9mgP2`3%#0hl9oQz1$s$8hyQ~@;_`l&sm9#Ea=>5zS30dq+JbbiAmgGHZ2+K^)OFyI9n zbI1I!m$1((m=#vkV0>8lC3Xp0h4qBB&EVhPqRzk$8UbIsBBfv{`AiF-b`j|CGg#p& z6;6$#I#M`QNYSJ#?7cHJ7#Q9kIRAV=*nfb($3P!HA!B1EgH2%&BW(b_)B_xi(ip_xu$!n-j zWIxJ@`a-s+YDqO2Lrnm^+XT6Xlhgu;S*Fr6nK0<@(Hs2x6+pu@@J0@_7i5F^*a2)m zwgUf(h2bOdPgo>GL=D(&>ja_IeUd+MWIc_UjI@ z{yORjIe@AG{_F?2bRFpdQfLnt<3nGfcG5@SWR5d)q2Jj&@E6s{P+*1EA@8#SqTC$3 z6;om-aUIqRUxoYPX^`V6!!}@#AuIF|^}(W191Ta4U`DhKeB=nmhxv~l2@LK7$hrY` z+n-9Ju9C4-75Rd+lC`7|Vu(dlAL<6>3O{oixR#i{&m3V0(C!)}8{GzoL83>oACUDJ zhKImivB-k{Aj5CJ!X zv_F&g$$R7*vYAwoKGb4rD*Qwu-2wPzJZPB*=;&o67UDDk#DB*@my==DVfYh>yU*j_ z@%13_DR?Ys+ehpm=7AyDMv%2XAoqCS>$_kIc@R@c@1zIND)3T&0K1~7PSkyn@egt% zd4W7Zo`s0Go@^tN0nJy#WbX06QWGIUy$ihd3E~gghbG7}9mSx96}|}H1nbK2fAP2Y zCwwjBx_aYY_!F?RVmRrypkroW-0grpeg+gv2Bx|Ua3YSb1RY*S`BMaCCy$V~$4#5lJ=+Ahc+;Eg^^{r~eJf0|Dn1REMkaVR1A3-)@HTo2t5 zm;e9jH%Jla`XuT;C4+anujmfUZHPpc0G2F3YteMrvj*!5QT|K32C_SL_=xZi_$7QL zo)0VE!xqA=-H%{ZG*XHz0Ji>%Sq9OO4q}p#v=89eQ)(IbDsMRDQt}454Az}XPKBLs zg6AkDc~m6GTMqu~G3Zz^;P*UWKVuya8FwXq;`i~rVA}}Z z1WzUaZ?*|y${=(!Vg-)6ml?`XfMR{YBXPk;Ux#zHk!jR9@*13P9yx)WO-_QfZ;_YD z=VVW+AE0<4a2P2)8D`n907epk-av{l>{F8HN+=*G)~_I&E5(-6Ov+b1?+Sa*@GNJP9aB=L&@Xh zN0Nc8^>VQKNx+iE0jKc<7MlaHUL>{~D+g)c#!DfC7)tm+_BMg=C3N@$d<~w0i?Acu z2+RuSoCOi>S%{xy%ud*I7HC;0Z{0k{x&vVA(bBoLb;Z zBY=UPMl!*oA45bK3e0yac#Tkqe-Be7;3r73fxJYn208a23&|8R6LvnC+)dsgX>t;E z8~ST4pjGr<#tHZ?iEKvQVb5kj&(mO0!NeG1C{aQTA<~FI$l_groT&%?6dMBC{S4v_ z204HXN4y{wodfZh0Ql!Tx;=E)yGyNvv-F@okXOi6&}Fb683*rwV#qYu_Z;$HQc326 z{}xh1c=*#sV{$3>G~G`w3a3JNOSgi0DO3A-2HRFd~Ow6W;*My5Tag_;_GTyU}(K z6)u99sFpd-bO*NokX}J&(}8p;V989_b1PWUQgSBQ9rheS`oYR^Pyth|6&44=)Aq3#E}Zp07bJ#ibh9rE~MbOl<7en!?n zY+l8j1nkC{kMv=B1dY;Sz^SoRSIDQ9!AXw=osK1W&@axL^dXZ#{s&13`0owiw>HD= zg?(_+r@=>mLbKtmP!iyV4o@Mv5*vv%#47me5ASD`_+`LQD}28@c=2s$AiN7d4BS`3 zYywXi1D3TH_^mH3qDsLxBmgUUP2L3^%Y*eiNH%G8xWic&k$Yg*?rGTY-N)!@;5T^mdGM6^(3Or&9R-{2PezgLNECMMM79SV9}S*_0B`yiGJU`4DG)O+ zg?#ZQSosO&ginV4xg25+NO}dVy&BHCH-QrG@nwK-cOaV?3vYmyLiS)CG9O~uQf3Ln zI9mD($UA{X=+7X_wUj4S4PD%3lQTfad`M5y;b0snXxSK$H$jC!98gG~f$07LlLK+a zCX@@vOkzVJGn`JO5<7_F@O6clPILn#{fjTbBXKD(gm#e8O9ck{45IHO4)E>=Z-*E5L7TfJo~qAm~?g zKD>X)$KS(!f+BdwyqtJIoFJAFi--<@Ca3Umu;*RaV2FM8qH*XghzL7@jy-_{o^2llt0oDbR^04p0DdPlRv1+;7w`3dl3D8zi-fsxIGT=GGPB|oEC z*cY&?1GpDx)qLVCu?ODv?gBi^Aq?Oxivh>Z0zbfjgLZ|JUIqDV5oD77r((5btaTt^~s8ZRKnd6_O^z#!??Hr-9dDAq=ZZ3y7VP~!wmAl}jfbKciZge@z|253+5IaK2ZeUwA!2jpNp8JA!pM}iPdwBcv9qkHr<5NMKIufzqGoBMG z;ReoHq6;ACACN^f@ZXuxYg_{PsThc_{+CVCK(28q%ui?nW_OzIM!N!rE`(1aRRJeG zkL*QefR44odkTj`>qvpW*$bS!gnCZRhnRW=WW%;1@FobD$y=-seilx87-04$u;&$E zStEg;G~tI}&ws(L;(>P`fmq`wMDPg+3%LjJY&OGUz5}D32r-rhe8MO|Z!55Y)8t4p z8SKg7P&hOWH_{h8^?C9yITp?&2eJu083D5%@r7JWDsb*#{35OfPq`d?#$Mta;Mpi5 z8dg4v55U_X^6G=tK@8Xx{S9_ig0upHZew~wo~RKx|1`)*X<_9ZV9`EQ8My=WI}L6i zBL6RO1h6y@^sb3afjtKSYwZD9gh`O80fYcv9RzqK!TrHg?gCG_4G^>+5k<5B_b-Nb z*yrG!&5)Zb1m3+98HjiwpCG3_9Bvm0=@*dk%>~PP1-yP7#iJy^1ZMzxw<8Jg8Ya*) zU$PIpC#oT#e=0Ipl19!y73S;#eA+ia*8ufDJ8zcgGuH@8jXNfDy256pjKj z8G+gV_vRno^*sXZjz)x#t(^`wZ3Qm453(XYke@#aemV{K?@MwkIS@RS6P&ccK|4Hv zA+3P8Cj=0*g<4NDkY(-wdGP)aYfQnau@Yc?W;~l14cfgLtZEQ&!4`<;8XozRz%zl?_Wt9Kzw-sS&K`Y2ddEXezR8LCW(-@ky5_`lN51H6jjTi~~ZKp>P* zLy;Ch5u{6zB1jQwf{Ic_QTYpkNC#07K~#z&pol0@MpL=iAAPVVMjeesIk3Dcmj5 z{qe}qa#S!ZCNfaRa2b1v4t2!eIEHsK0KHKe3&B8R756-YMfN*#te^LUw~G;puDnC8 zsRYug8u_Jitn_Jj38dyA^m7la!Ri4S?%mui={`loCe#&646CH`Dm$XHqZpRpSL9Bs z61&*NN;knKI}E*t;Lj#O@6l+-I_PMprw~UIs6E}2=IMp6(+O!`-xov1Ez|KenY7Wa zY-&|RW7P)s`|Pf^0d-Bgd$hYIe*6rwU%QFQ)pW+Q($5nEjW<`ikuTlYh~DjqpZtLM z!~k|yisv96){CA-SO$gQ@hZsiUicpuJssih;$$CcV)MUD1f(x=Yz$fZmsr&c?y!K? z@O5AQmJ7(jyBmYWA4k-rp(}yt@AGJlMcAMf@HW;H(QS?&9|Pn;XpUquUt@_51yR*I z8{6+WAeIJpC^)}`u1xSWAop2cgmYBy4_*4pFc zEF}_Ih?w96M`MStr%3-3q&)5=Sj&F~t+< zDGyC@N+(L<{|EvNFCTTJS+z*{8T_#K+%9ZPZ}r7#X3{;oR@l^Nxzj!DK!n(UTJoXzN~3a&%cbu@7%Vn+=kw{Vwu z&1C%Xisb0lkZWve1QNfPCjZaJn}|l9j%V2w4cW-k7R@;spSFPaHN5G1MDiqpP!C`F z1!_i`x>jTVzJn(h<@QmVQY63~kO<^y?yl}2_fFRczQsQdYpj_wn~cC1>P@nI2YfSq zZ<6Os$2yr}bT>kc^TfE{gWh+c_X1)FZ+Kn+b{FRG4R->7ywO_?EmMMgSvfq~YGjVX zh##brEuZGz&suxov~>2^-&{ZM&gb4^?yGn*44s^T&Is~-?Hxf>+AJax+7MsonDGOV z=Ne}GVk{nB5OKM+o-e@lHT;Y|o?)J^uzh`=SCO$H%+fF><84eQ|2Z9vHNGfc?rLAiz~$aCOox`7*Qg} zek$-j1#$`Z5j?wRiGr^quM|ekWidJB3RvD-d|#L`gqy}XeEBv+PNLb zydv)7c(a|+l-r0CRVT}|0v>BbX5$c9vEIJNec8r7vQ<6sY7>ZU5ArraQ^$BVvby83 z6NY2=e#<(iVS&!`UgtZDk71b{L`&~*G&ifjCSvu6;m6H$uW^TSY=+;)Q-4>IDA63^ z76tHfq_*TXa%>#2!~(>}7LXTi?hEimVv)TGy;F%)P2zjW%Mkb72G(Bydkk{qCms&6f)jJ z9M_4=g*%^eO`-Cr6e~L#?JKkEa{uhO89oak!x+hN+0lj zN6zpme4U-dVY`!`P4uq8+i8zSolcF+2K>uKp5Nf~Q^>HU#5PWOpG4X^kh}?a85%n$sUt!sQnPN^UN@uw3UQf?^9K{@JB{uA3GzFSyE=UJ zI{5bIvlf!%BGK1T*uGiRgMA6)yx9ET;!Rh^7L?4LTIrRnbO$_+3&_xs_!Jef{1bq^ zkC=NDo@_C4E8~&eC9$8*8P721GFZ{~slVIm48eO_M=Z3ITVl^W`F+m)BpTy9cSljp zo!_;OXiaDGNHN$Z{fUHTkO}*SJzdq8My&r!{H$_Vz?<1UJ%~yMdGC1QJSROTn8AJG z{B41}8@ps6v5wkk*&yQ17aXn7(+1he?~tMfahoRY)=<5%yNU^UjcYvdsY2|lIpk;` zqb_ebb#pCBOTea)5nvp}w;V-w_;L2uJ+fHIMEC=-R=NUtuQ$KZn;2qA>H^}) zW36PRhmsEtb-m%5PaSORZZ@)89zU zrXzYI0X;pMJzbP&{~BQQf}_e1t;(jJ(*?iP_I76#!pU3rpk)vd)MNP4a~&B-@8#sr zTEb<2xz3O;EJ!w^xI2JM)(O{IstjL3N8fb*jeXD(d+Y>x#sO4}rBOjUgPeE;@}Ga< zH@%2OmW*}s4Hif{VsvG^!9Xue45zWTKQ^FHyE|C{7xwxVeChGnzm17+omAaMl7MUHx#(l;q3R=Z%2XL4f`pBs-M-q0_6S<;!O`GADs_Q`WR`s2OB(@c*1q? z-;X5WKf<|c6N%jK{Lb0SSrv`-7ZIDbc$Tqb^#|hLxv&t#tF3|bK1Wph3o`QUuoG%{ zD+9MV@uML`rw)6Iumk2ADd?+3RCv8gM%>MazhU>+aP=X2@&|9AoFh9MOa1It*8PI#sMQf7rpM*#M7m@szmgQyw0Kz&IQIBEoY z>09>LOm_2da)r%ZW!YB|_;!Pw^~r5Wtf;T4H`YP9R#Zfat{jFmDNc^#IJBLMb{a>{ zY8-3+9oNT*$5o;JVYYFa3Zs5_l_|bw$mm2;J=Yn1xsU&ZQNh&`*u&uINgN+S{a1nf z7?lrESid8wktjqZ=r4|UskV|js?U5K$m(1`MvOyaRl?Uf2K+3T%eI z98?(fM#7&kiXlrDU>`h5+4X94_MRGs*Op#iw1575W-`I>5N>jldFJg!Guj?@yc`AW{;94)4#dI!*5E zMQTOQ;%)Q?gLp>;cJzK8`fmLfoVs z)qkhR>GVZcrearqMMkMED}9@|$W}OfG5TXMQT;7kr+5pK-5bQ3oG=O@?We=t1@TR1 zP`j6(daco59m(zrb=4&^DOEMSk)0jj?aD-)6Ua%=!rQ1vcK85RXm@OrSR#`D!79o& z!lC2d_(>%>Gh z6Y+Plu1^EIGx&B!sy1|0;%$W_>VD=?McSMw)hV(-uT!0P4LLTFh@{JRoan^|_!||; zaaB^sbriK+r-|^lrOIv#^$T^83%@$z@Uz;1^%A17H<02L zs5fW<)OM~`SYlz=B~n$j4C|?bvji4`)O*x*q+=C*gaj=>_3IMiRGrZqd5Mz6kVV)9 z^nGZGIC36^sDXJIpLYcj(EMoi(Ns+&`NH_0g0s*ZUMgf?XC?n|p5#AP1%s#5Iy7aU z)n=W8UFmT5YP^l^_*g0E4Y5KUjy+JW1GT;vjMYfcmysq#pmzdR?jerj$m<(auazeM z(31?v8Z5E=cqhZrHW$dzz5u;9kktt#3N#FgZga*uGuYb|T~7kN4u30B@sr6rF&p5* ze#H4R96L=bv@n&CbKvgBkf5Ta`yx$BL+_i^ZAEjOBR822Usqw32T;Md0qe66RfWUZ z(Wmfen-cd~LKUQgF<*5~KsW58F8P78psO5q{^RK9a#V(A@-ECaeCdJYr30NuktVMq z$IfAoy+QWwwiyc@0E9}=`z~vnKrZ&GHG<+&6-NhO93pQisWTWL+vbhZ;$gQv`cGeh%*)|^etkcSIBq1hnCK! zD)UEptOJ!KMW_o;Co`DrP4jxmO;#qR(GyAYGrs2yqP{KYF&FDK4cnwMdC%ow?W5+T zD|K_t9_|%_ywtQ7JI>P z2C>G4*i~81tML8rJnKQM=r)pXyrVf$*d4x!zBbf$$H2K0z^y6tEz&S zSZ!}2O|pqqPeG35rG|ehv7+L>i)6AU!(%OpJ{QNk%)@#Yq=xP(td*Wvr*q)JXd}xg z&n|oyKVd!b10U}IcgC)p&uB@Gf>^h&QjPZm8u>UBP6uxn_K-k6i48i3c|C(SeGE_M z72ZHNjZfTz`iE;+)6>wE?ct}gMhH*~VvmLy^^qa{@u_~MvL%iPdnIJpKUqe4-dVAKeH5ypFyrgRs1B;-d{Vx%(gzbTnGH8T?%V zyvrle8o*t>xjPFfa)kF03gUHl0`gRRln8Q$fz(&FL+4Iqt&bBmDg{0JIzMI){Y~ZR zMet7JNMaUy(bAup`Lh(H&uF5s8RSOBA(sNFQe0|$ijUO>3O`PrWOeA?oErOg;JhDM zi)i9f#js(zVGm6sVzQS?K`)+VBkJZp#ID?q40oan+q1K$BB^)qX3r&HC&Sz4@Pt+& zVc*1$&m_k48SgH*kf4)TJE{6ui(mOJu$mDMtihd70XJ8km>CR)?Y!x!;Tc{VV?%G(d_IsL^Pt2B3FSQ1LU^0mMt2!;izD_EMrPr?86hQqkUw zs`@e5Wm|~H`W%(1l^g({E#f`Cqrkif^s_)-$NImAA76l~;~(e|>WCr7!2j+=g6^hH zaV$HlBYDeaMhp1-HE1w_yMGbaNI_dX!Jg`a?D+=BTc}XHhMq2gWz~Tw-xR8p4mxf) z3Onm!<&MT0SOs52LFqU+{UFt#KfvA3qbuXEX5QjWkwoPG066CXcr6Cb+eF--M}oIO zTK7SsPC>eDG0w2cMe)PhBj?9rsV_yA9PwT9-SY)AMssi&O$G8sur{b0R9G*j&gl@@ zhqFXBj$k|dM0I;tEbr^^<51>%i+ubr-W73UgDxi?IT(MnGx&C3KlR5Rn2y}s%_?UZ zp~U1nGt2Qv&t-7tL3HIE>LW{&spyE0IvK1Zz`77NuB`POu-*a1qmd+sfxHMyum?Pt zhAlG=OE8Pse}ulzPu<*FvUkIIXM_Ldi_Gi+t;e(WtEuh2h6no?_RGuYi;28ZAdn-l z1h4x%?CPpu-HWyU&cxd12t#K42Wwy<@+=(Lc^s>I3p;u;d#;u<3z;{W3~>M%%g;?O z{xWjvJE{Q&U=6+o+5L>YoHtz@6%HN~oHlnXiL+`un>e5&^?U81a zu#XPH;YE?8-KpZ4gB0Hm>_b4_%8s7G`#lY)vR zjQ{-xzfHqV+GLzG(v1*!p$&1J5yXn;BS|)q-HihBEx4?xxz?{C!@q};F=RVLf4qn9 zGasJb3G97v*kZ2V#S+X<1mSyVRL-=y2>iVeDgTxmmcrDw5t<^pd7bRNnKKBL}e4SM+sx}q#x_!8RUV|3&~qPAO*V#mlC zUV_WK*xyyKj=F>Ocf^4%AkD(~u7H8~kqe;sU+~u-=`u&-C( z0i-!fI-6pXj&;steb-{Etb^Lq@r61%%Tp(~0gtw+BOCi*5;kaQEWy2KrI}zo7Pu4P z^d;mlqo~y?;H!=l>1D3+kMP%eWY~UiK7kCqj4U&Jg|Xn9A;ZTap|!t` z>HL|<>uPq`5@s=ydsXmrM0U3}?SmO6kKF~owea^8V1Ekkv(O?tOkGh3$nEgA$0D-@ z@^3{M%iY3tTVO0r?KhY z@YUqaplGPO09>aaQ@&!&SHkbHCTG`X=M8|rzBX5RGa6$*nTZoXj-k5eHfvo3-?#<# z=qKp@aNf)bK|gdivHlT?|Aq{kfz8r`c#RXSF`HVd(!>OpV&gR8ow+!)(sKA}8f!e0 zIjo2N!r7&G+J$`6Cs^peBJ~9F0pD@xeG9>6<-3$RS8!QHE&V+(ZU4LEr%d^MZnN3`QQqX+h~6fFFKOr7lUE)?ac=0&*|t5JLRmFT8|SWEhUIOW(wo4#EmOg1r2l)n3Ere!RFeqc9S#Ej&05 zE%hrAnjK(#3|ym3*irD=DPUh=U*~s}M+^0a%a-t->2*_EcfleZjTJV9%9;;=+#FeQ z6Dd0psTV>`=t8tmb1d)M(07ONCwQ+1as;;Secpho!Jc}X6`2j>zrgs23G)QlMuM}f zcQjZhv#*OGo4T4@wh7D3&F*alv|+qG^9jF0p+HN#oZCbrr?W1SnO=z;d!F^Y&n}8! zeg9;C?O=Z=8V`V63!8HQb$Q<-X}7{%{C`hY_cV}?0{7@cieCcjg2>~RNaZhhyYM7c z=cU+NFQYSuBhSWyb8jr7a>PLozzaRGrIL{i6WKRmL=di^Id&2m+y<>r8kfvHRiDU= zK>iVly$xCmN1x+Yt@IISaD=&Bq(alhE^UY``u`1%_LSJ!Q_xZZ`4F^?1#UFQN$`z8svPA^ zI9%+}I@p#&$o?vNmuDY!$0{65?DcJGtlOYXGI@(&Azo!;EEB0{8h~FJ;!DFrKgB-Y z2d&Q|vjd6CHblA(<-N}N=!$TY(lNZz8_RJDES2%4+D80d4iMB`z8{l1i8Ro(0eX?bj=Y+4Q3N`R6i{JKD?XO6lw>M zNs>3i+rixz8FdtP){{hR?!eJ!*-sZ(LNU@fw&xJmC(WD<9SwVQoZ6Rg|A;+A(-n>rCU5UFA#Z2RFPikO zZ1VSXbkZR#LI?D&1I*`;opqr=9{lLtP~`Q8-2Dd;?{4t%W9(`V{!E%l>r(8g_E38= ztMR*Oab5)1TUZ0>%piu9KF0bU^ldvyYO1cn6H{vetLN@c=6&c3C(YbT>Qv63>E=$t}QX zGW0&q`(ZAs^Gf2|)PU~AS?8;8{qIPzwq}0e7)ggCCEq4o(R|3={MU=}f~f|oa>niFaK6pL#U?^RqU79PsG169E| zg#6V_Y_*?w+JPLFmw7J*vp%eXgLT;wu+kfWb{^H-Zu*cav`#_33y;j(c^s_m59qlvukGH z!L&wmgu&CLslX7aL~74v&mLe;$(!~Wyk(aS=IQX$N$9YjoxT??6A$1xbGb+U zvLf||1F!&Ak}*1m9qdDfmm~)tvdLw+c39QBIxq&Sz9#22wH*?^!<9OCS zR=Y4{e;2Mlg2R?GayZn!0j6$h z!d+MtUMP?Zw?}a7G%fMXU>yM!QiL|=;iJst0HI~2dh?#d)un8drMyRp<}@J$iz zSrrHN!8Rn-NOo^0ux^bEX-SNt7dU?eXa5ZU96@s2Gx^I2#sSoFdZ4W6jRQQ}0admz zhP1i?y+epZcSLhcVQ+0>WiKGZuLCz8Ydjpt-|~$&9l<#de$G}h44>kmzYeBtf!x|$ z`rNI}`ghAJu(%$tTO6z65on*jL+`+g5YMqtM+MczI&^_aPQE8#w#H zIu@vxkSwRbS*Se>8C!>u65-LMrVM=p`{a4{Qy2E~o7mzXV{88oPRCg1WM~{n>mac9 zfpZ%CeHJ`rPwxP-_#mRQ(y{z2!n@t6W0`>z-H7!c1>|_7=@I;&737^p5-qO7dosz4 zyb4d^L+tuq$kDFwS`T*FaQ2jVR9hH70Z1O+M(2z4=@E$RONZX)*hz;?&9NEC63Mt= zdi15(SzY0y&#-b=5sx~CZ+#lv58}PAgxUi6Y4lYZ)Lf4q{tQ@eA~U-Kxi_>Qg)jF# zJ8nDA1@Z%SYcNsM{6H3*Z*lh|v)Tp5+d0ImKMf_kSSRw08-3xj*+l9#v$_Y7rIB#< zX0B%dxeuDMl;Z(%a0_^NXwq9GifGGdG-L$S z-p20^&Q2p?9uUQ-f~VDs^_)t^Xa#ZE2=><@R(S*TpN0L|3(Zo7F)lIh->{3uAw>s3 z=QprM1_F6Ha%3$d9->7mxa5V_K|l{S_ttIG7K~)i?FI5SB-sIQ5N#QVHP{$>55{+# z3FHk}!uz1~R=mkyv4_Us<1}aAJK?c|aME1l$p}34chMgM*kvCBc_A~}2~8#LmxcT) zKoqYq^bWxP7EK)kma?b!K$%^fMY8j6nQ>9Ry$qU;MPe<0!~cNJ+xcC?)pV#WQY;LM z{~BDmk)1W29Xt#=4@P>8!XBQ=y+0UNFu7@RSTJx4nk(%y#f_kgo_y3x$$9vo2~N!AJoADDaV2k`!lHI{GOnvAFY2KI3cw8lO9A0`H~7|HQD zcF4!TodgBGM?3$`n9*?TO>ho`(nax#3a~=?u-4O9=`$S1!S?{|_JY5}g45agMXAJl zj&B`#A6YhueLWwji;39H;A#{*t^+t1HF@j^+_@Akn-1*H@Lj(`PyGbWe=(=yCeB$% zC)rg+=ur?J^I-`lAx|V;bI9cEaC1i|nDVX|9Ml~C?nQ0Ir@)(s6#pKKzrkJ^j?LW~ zSyC1aa0{;7#txc~ESrvon!?{Xrsw<@W1a?~-QA1kI9SQp^T$<9iJ_hOlucv<^7B%*O1N@TOrrGeZW`8@!SegZr` z4Sc6U>krXmz0p=rBTMqb-RGF^My&dU$kEy68vlUgTx06UQ{bEmM+KUk9Rl7VtaAX| zeVg^a03SrL&f;B4Y*PFUktW6YzM|%)6dlCMeg@uhOhk?hg!9@F->A&K%3y5qh}Qt^ zXY|BeFkXoNu^yg308SFoHn75kw~Ik-ktS|dN%nLiJSOoA!CT@N601sOPZ!0ut&e^3 z60w`1!2SfxKOqx60-vr29QHJj3o-j^&~z`Hyb{<8f&Md}zhifbog=(`8)^ptPxwn9 zD~)xZRY*jNN*v)RoGtcf40j~*UV!iXssYw7@?9DOfISjAj|J{vcKNF&&K04ZkF|+~ zOSS@Q1u%aB-{0^q{s!`K#uB-m31r3FLhWGa?PKSrGOIZHO2p_m&?P>XXmSPr_MV;9 z0BQ9y)E&hB8p`f|A6mb`>bK^}B{o?$kdwhVhC`w!Q6{xT)`~PK2KUwj>(1=0zR-9O_`V0my`e)ps9hCVB2qXJ zN=2}@HUM!I#~-F$xt|eZSj{`gL^o1Xud_&Rz0%^#pFxI1GQw$aP5`o4=SA4DHHiAR z0a8!2#DAdr+wA2YT(^e9!hoC?$XDT{gY2zM!2ScOZ{SQc-$|$`UeyDW!wLdhvYR$c8xh?+$xZa8@!*AO}Hj2l2dAD3Soh&zi7REX=Ps7Q3fxQ)ra~rxM9G-~=auPUavd#iI#KgG} zIE%*0GFSR4yHY5A7ThJ~E|4D}d5RG8uMRIY2X<%n*DD;|m_<9*K)yMx6r*@)dzqdR zSKI^4T~J!&;c>>h2v)+|3UXn_6P$yA?1abeGUp`d9S7`KR`~+wNpPgJFN7ouLpL;n zy6uo#orzI)!rN?xwNx9MDU?xstjslfod(N;&|IPc5%9H0hD*q2WdQ~<$3j3Z!e7}} zE@qs;oNvJW3if#@muN!HK+flf-W6ESx>$VA!fDTQv_pqAgR^TgCxI;AN_UgqF|4b| zBawthS@&o#N#scylEi`4nIC$KMWi5$#u7a(SYI|*J01!N>3*19qjuLYhB*u@R` zTL+A*GNY0}&cg_I!R?C4r7^%1PK`BJ?K;#@9$f&?WTgv%OE8oVG*>#4o_D}H3D^Q% zAg6Foe1p8SEr|rL430Iyv<~p=aP|aiP!4(sWP_Et4c4Lo6qm-ruM%aw$+-9b!MT8m zwNN`hd;^vTEQ#i4k4 z@UDXNs0P$3aMxqZrzqnDn!IusD&1h-mw_&ard&EWXPX!($ozZDhmhT@v=_SGH({rk z&;@QP*BRL8PN-P`4lfRd<=A1B%%dW3OG9h1CId~Wa38ES)fC-i#ZsYWCZjl@Z6G*^ zB`Ee#KCbf6&tN4oX_HRNJ3zh7-&EFF;%7o{;YxvB3g~5!I8MKnfs5IA4q$ssyzc}3o_XAV*h?%{7wv*f zO3AJ$!W`tc;KTJ2Juk5UHFuf495PNOtC+LwUe=x*DsWkb&_A9Szhi7r|@bFBk-lyz*2n$ra+bc1guS*WsE%LNCH=)RIZ18 z1#@2FHv7n@9MaSGu&>Npda5IUE9viKchJOyE%VRjR}SeZPzA1nta`{8s;7di<}Gb0 zWHkG!-m1Sqm$BtX#*)$0k6f#}>WOY4&;*|Rx=ifkum2%Wq=i1zNHT(~ftrWHLeWG1 zD$Il;a>$QxgMwjUCbSW%Xv(S=cqadIbaF0pUD36z?e5!Z`v{qT{Lj-r-mB0|H(6NTj3e8IeyS9}AkC zuOx;*Rinu4?Hnv~7HX-zE^=Rc4Is?tXyo%5OV zcc9H= TqZkKuAW2^_}td)F|o?$UEu(fZif6M6QbeQw&)thw_6&kITFt&{E=-BHeHCSN9be zC9+2BTIpHxp_G%Ei}j&(lGqIq_@}(Nzh7q$I(|=Ew@=Mk7<7tW|pB)TPRkZ z&rq&qrNt8x-c~CuoFY7u!nN?H=y6#s(I>*+vadv!iM^%eg4QUqtF>Mcts;6tcD9oA zmb3;SKO$?Cwvy3heA%m_-_?$jeoEGeq)~_Ld9f|LChv-zP(Gc+EJU}#2mb=O7$*#ExMPwBp09z!DhAewl;ud08 zh%F_Sg}@d`Bho?+nYG9nu}lQE*pFh(h_VYry9fo9CR5b2 zG?vz4TH7e;o}2y>Eo#@K>WX3cQMyj)D>b$nPia(Td&pWV8^F@w(o<|D(c4O6XuTo# zWEP@jWId&)aE9=OvH}FYf^3bTtp}Nd>MM4K>aBRnvO5%POKWM{MQK;vTj@-zueLq3 zh89Sol?AelCD(f1O5ZE^@>g`bnzhhHQ%m{?wyLM1ykM@*&n*6 z>=1KNOjvrwgjVWD_f|8IS*X#}U)@*W+Yf=NILKl|NTd|(k&istMBMf8DF34UKUh+ zN8={fR%>;pul1+Z#!f}Kt8tNY{iE)Qt)uVQKkaw*xn2!BJl)5CM*6P*oUMLx_Gk~8 zOU{|Ak*uE9$oh#qd$gB*b#+S((>{jAO`fSytUKyVue&v8dw+dy_0o6Lh}Kn3=z4V3 zBKQ7+nP8&lU>_~#c}^Nx^Odm`m8?EF;o12>)6N=EwYIKhJk`p6&l){~s;9b>6HogX_VY()r$_c*X$@EU=03JH6V*mTvGIhuKhgcc>mkAUTGOc;QbrM0>hu) zGIDM+)Fa9BoHMhqx1OjoeJ!8*Tt={dteGmfImgny^yj0P=qEC!JoU%N8qW^XI=9a3 zY>^`~`5x6+?h3w1}&#=5WX=pOPZ_j2Bs z9#${=bL+0^WACY+`QKMGuzJav?ql7x=A-Y~S5V(mt@M?&(C6|*U0HWD6g{@}#F~Me zS@)%fb*09%{>n2wru5RQYfi&&CK{rGBhPg|>xmjyL(`x3nOXPrT@6ucHKM?@-&1|ZRvtLD6UGQ(xI(_+w(f(&xGLvX5Y$+gqx0J%*l{{_iv@)%SgJN^pJD==>FHbrTpT9fq@{Y|e_wh(Izmnu_CD%dRzN5(S5|*@mb10>+?<5@ qM@xuDv6IL45v*%Hj{elGtU2f>CJ4Vj(nh!Ud+>j{&^^`oa`it@_o+Gn diff --git a/selfdrive/controls/lib/events.py b/selfdrive/controls/lib/events.py index 95ccb7b7ecd105..5fdf2ba29ffef8 100644 --- a/selfdrive/controls/lib/events.py +++ b/selfdrive/controls/lib/events.py @@ -439,7 +439,7 @@ def joystick_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, "Steering Temporarily Unavailable", "", AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.), + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 1.8), }, EventName.preDriverDistracted: { From af1c8e5412d3d50d67a5748c496e0461421342ce Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 21:15:33 -0700 Subject: [PATCH 389/436] Subaru Forester: 2022 requires new harness (#25229) * Revert to 2021 * update docs --- docs/CARS.md | 2 +- selfdrive/car/subaru/values.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index eb4dfd884769ff..73211a8d2199e3 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -134,7 +134,7 @@ A supported vehicle is one that just works when you install a comma device. Ever |Subaru|Ascent 2019-21|All||||| |Subaru|Crosstrek 2018-19|EyeSight||||| |Subaru|Crosstrek 2020-21|EyeSight||||| -|Subaru|Forester 2019-22|All||||| +|Subaru|Forester 2019-21|All||||| |Subaru|Impreza 2017-19|EyeSight||||| |Subaru|Impreza 2020-22|EyeSight||||| |Subaru|XV 2018-19|EyeSight||||| diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index 8fac934285debf..ae471a56e7b3f9 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -52,7 +52,7 @@ class SubaruCarInfo(CarInfo): SubaruCarInfo("Subaru Crosstrek 2020-21"), SubaruCarInfo("Subaru XV 2020-21"), ], - CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-22", "All"), + CAR.FORESTER: SubaruCarInfo("Subaru Forester 2019-21", "All"), CAR.FORESTER_PREGLOBAL: SubaruCarInfo("Subaru Forester 2017-18"), CAR.LEGACY_PREGLOBAL: SubaruCarInfo("Subaru Legacy 2015-18"), CAR.OUTBACK_PREGLOBAL: SubaruCarInfo("Subaru Outback 2015-17"), From 444de299b6c0dd7081bc23b16b465f80a83d2d70 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 20 Jul 2022 22:02:22 -0700 Subject: [PATCH 390/436] FW versions dump (#25203) * hyundai dump * dump * optima (good, it doesn't get rid of previous segment values) * add these comments back for now * add another from release3 today --- selfdrive/car/honda/values.py | 3 + selfdrive/car/hyundai/values.py | 28 ++++++--- selfdrive/car/toyota/values.py | 1 + selfdrive/car/volkswagen/values.py | 97 +++++++++++++++--------------- 4 files changed, 73 insertions(+), 56 deletions(-) diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 3ce467e2989956..5749b53476d2e5 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -1303,6 +1303,7 @@ class HondaCarInfo(CarInfo): ], (Ecu.fwdRadar, 0x18dab0f1, None): [ b'36802-TXM-A070\x00\x00', + b'36802-TXM-A080\x00\x00', ], (Ecu.fwdCamera, 0x18dab5f1, None): [ b'36161-TXM-A050\x00\x00', @@ -1341,6 +1342,7 @@ class HondaCarInfo(CarInfo): b'36161-T7A-A140\x00\x00', b'36161-T7A-A240\x00\x00', b'36161-T7A-C440\x00\x00', + b'36161-T7A-A040\x00\x00', ], (Ecu.srs, 0x18da53f1, None): [ b'77959-T7A-A230\x00\x00', @@ -1351,6 +1353,7 @@ class HondaCarInfo(CarInfo): b'78109-THX-A210\x00\x00', b'78109-THX-A220\x00\x00', b'78109-THX-C220\x00\x00', + b'78109-THW-A110\x00\x00', ], }, CAR.ACURA_ILX: { diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 303558b5a136ec..9c3d852eda3b33 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -383,7 +383,7 @@ class Buttons: b'\xf1\x8799110L0000\xf1\x00DN8_ SCC FHCUP 1.00 1.00 99110-L0000 ', ], (Ecu.esp, 0x7d1, None): [ - b'\xf1\x00DN ESC \a 106 \a\x01 58910-L0100', + b'\xf1\x00DN ESC \x07 106 \x07\x01 58910-L0100', b'\xf1\x00DN ESC \x01 102\x19\x04\x13 58910-L1300', b'\xf1\x00DN ESC \x03 100 \x08\x01 58910-L0300', b'\xf1\x00DN ESC \x06 104\x19\x08\x01 58910-L0100', @@ -413,8 +413,8 @@ class Buttons: b'\xf1\x8739110-2S278\xf1\x82DNDVD5GMCCXXXL5B', ], (Ecu.eps, 0x7d4, None): [ - b'\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware - b'\xf1\x8756310L0010\x00\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware + b'\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware + b'\xf1\x8756310L0010\x00\xf1\x00DN8 MDPS C 1,00 1,01 56310L0010\x00 4DNAC101', # modified firmware b'\xf1\x00DN8 MDPS C 1.00 1.01 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 4DNAC101', b'\xf1\x00DN8 MDPS C 1.00 1.01 56310-L0010 4DNAC101', b'\xf1\x00DN8 MDPS C 1.00 1.01 56310L0010\x00 4DNAC101', @@ -493,6 +493,9 @@ class Buttons: b'\xf1\x87SAMFBA9283024GJ2wwwwEUuWwwgwwwwwwwww\x87/\xfb\xff\x98w\x8f\xff<\xd3\xf1\x81U913\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U913\x00\x00\x00\x00\x00\x00SDN8T16NB2\n\xdd^\xbc', b'\xf1\x87SAMFBA9708354GJ2wwwwVf\x86h\x88wx\x87xww\x87\x88\x88\x88\x88w/\xfa\xff\x97w\x8f\xff\x86\xa0\xf1\x81U913\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U913\x00\x00\x00\x00\x00\x00SDN8T16NB2\n\xdd^\xbc', b'\xf1\x87SANDB45316691GC6\x99\x99\x99\x99\x88\x88\xa8\x8avfwfwwww\x87wxwT\x9f\xfd\xff\x88wo\xff\x1c\xfa\xf1\x89HT6WAD10A1\xf1\x82SDN8G25NB3\x00\x00\x00\x00\x00\x00', + b'\xf1\x87SALFBA7460044GJ2gx\x87\x88Vf\x86hx\x88\x87\x88wwwwgw\x86wd?\xfa\xff\x86U_\xff\xaf\x1f\xf1\x81U913\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U913\x00\x00\x00\x00\x00\x00SDN8T16NB2\n\xdd^\xbc', + b'\xf1\x87SAMFBA8105254GJ2wx\x87\x88Vf\x86hx\x88\x87\x88wwwwwwww\x86O\xfa\xff\x99\x88\x7f\xffZG\xf1\x81U913\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U913\x00\x00\x00\x00\x00\x00SDN8T16NB2\n\xdd^\xbc', + b'\xf1\x87SANFB45889451GC7wx\x87\x88gw\x87x\x88\x88x\x88\x87wxw\x87wxw\x87\x8f\xfc\xffeU\x8f\xff+Q\xf1\x81U913\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U913\x00\x00\x00\x00\x00\x00SDN8T16NB2\n\xdd^\xbc', ], }, CAR.SONATA_LF: { @@ -1052,18 +1055,27 @@ class Buttons: ], }, CAR.KIA_OPTIMA: { - (Ecu.fwdRadar, 0x7d0, None): [b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4110 '], - (Ecu.esp, 0x7d1, None): [b'\xf1\x00JF ESC \v 11 \x18\x030 58920-D5180',], + (Ecu.fwdRadar, 0x7d0, None): [ + b'\xf1\x00JF__ SCC F-CUP 1.00 1.00 96400-D4110 ', + ], + (Ecu.esp, 0x7d1, None): [ + b'\xf1\x00JF ESC \x0b 11 \x18\x030 58920-D5180', + ], (Ecu.engine, 0x7e0, None): [ - b'\x01TJFAJNU06F201H03', b'\xf1\x89F1JF600AISEIU702\xf1\x82F1JF600AISEIU702', ], - (Ecu.eps, 0x7d4, None): [b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8409'], + (Ecu.eps, 0x7d4, None): [ + b'\xf1\x00TM MDPS C 1.00 1.00 56340-S2000 8409', + ], (Ecu.fwdCamera, 0x7c4, None): [ b'\xf1\x00JFA LKAS AT USA LHD 1.00 1.00 95895-D5001 h32', b'\xf1\x00JFA LKAS AT USA LHD 1.00 1.02 95895-D5000 h31', ], - (Ecu.transmission, 0x7e1, None): [b'\xf1\x816U2V8051\x00\x00\xf1\x006U2V0_C2\x00\x006U2V8051\x00\x00DJF0T16NL0\t\xd2GW'], + (Ecu.transmission, 0x7e1, None): [ + b'\xf1\x816U2V8051\x00\x00\xf1\x006U2V0_C2\x00\x006U2V8051\x00\x00DJF0T16NL0\t\xd2GW', + b'\xf1\x816U2VA051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VA051\x00\x00DJF0T16NL1\xca3\xeb.', + b'\xf1\x816U2VA051\x00\x00\xf1\x006U2V0_C2\x00\x006U2VA051\x00\x00DJF0T16NL1\x00\x00\x00\x00', + ], }, CAR.ELANTRA_2021: { (Ecu.fwdRadar, 0x7d0, None): [ diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 46b43ba25ce70b..48caef565a783c 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1863,6 +1863,7 @@ class ToyotaCarInfo(CarInfo): (Ecu.fwdCamera, 0x750, 0x6d): [ b'\x028646F4707000\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', b'\x028646F4710000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', + b'\x028646F4712000\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', ], }, CAR.MIRAI: { diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 91297843507209..34eb75e53904fa 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -300,6 +300,7 @@ class VWCarInfo(CarInfo): b'\xf1\x878V0906264L \xf1\x890002', b'\xf1\x878V0906264M \xf1\x890001', b'\xf1\x878V09C0BB01 \xf1\x890001', + b'\xf1\x8704E906024K \xf1\x899970', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x8709G927749AP\xf1\x892943', @@ -335,76 +336,76 @@ class VWCarInfo(CarInfo): b'\xf1\x870GC300043T \xf1\x899999', ], (Ecu.srs, 0x715, None): [ - b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\0211413001113120043114317121C111C9113', - b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\0211413001113120053114317121C111C9113', - b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\0211413001113120043114317121C111C9113', - b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\0211413001113120043114417121411149113', - b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\0211413001113120053114317121C111C9113', - b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\02314160011123300314211012230229333463100', + b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\x111413001113120043114317121C111C9113', + b'\xf1\x875Q0959655AA\xf1\x890386\xf1\x82\x111413001113120053114317121C111C9113', + b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120043114317121C111C9113', + b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120043114417121411149113', + b'\xf1\x875Q0959655AA\xf1\x890388\xf1\x82\x111413001113120053114317121C111C9113', + b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\x1314160011123300314211012230229333463100', b'\xf1\x875Q0959655BS\xf1\x890403\xf1\x82\x1314160011123300314240012250229333463100', - b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\023141600111233003142404A2252229333463100', - b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\023141600111233003142405A2252229333463100', - b'\xf1\x875Q0959655C \xf1\x890361\xf1\x82\0211413001112120004110415121610169112', - b'\xf1\x875Q0959655D \xf1\x890388\xf1\x82\0211413001113120006110417121A101A9113', - b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\023271112111312--071104171825102591131211', - b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\023271212111312--071104171838103891131211', - b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\023341512112212--071104172328102891131211', + b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142404A2252229333463100', + b'\xf1\x875Q0959655BT\xf1\x890403\xf1\x82\x13141600111233003142405A2252229333463100', + b'\xf1\x875Q0959655C \xf1\x890361\xf1\x82\x111413001112120004110415121610169112', + b'\xf1\x875Q0959655D \xf1\x890388\xf1\x82\x111413001113120006110417121A101A9113', + b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13271112111312--071104171825102591131211', + b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13271212111312--071104171838103891131211', + b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13341512112212--071104172328102891131211', b'\xf1\x875Q0959655J \xf1\x890830\xf1\x82\x13272512111312--07110417182C102C91131211', - b'\xf1\x875Q0959655M \xf1\x890361\xf1\x82\0211413001112120041114115121611169112', - b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\02315120011211200621143171717111791132111', - b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\02324230011211200061104171724102491132111', - b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\02324230011211200621143171724112491132111', + b'\xf1\x875Q0959655M \xf1\x890361\xf1\x82\x111413001112120041114115121611169112', + b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1315120011211200621143171717111791132111', + b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200061104171724102491132111', + b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200621143171724112491132111', b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1315120011211200061104171717101791132111', b'\xf1\x875Q0959655S \xf1\x890870\xf1\x82\x1324230011211200631143171724122491132111', - b'\xf1\x875Q0959655T \xf1\x890825\xf1\x82\023271200111312--071104171837103791132111', + b'\xf1\x875Q0959655T \xf1\x890825\xf1\x82\x13271200111312--071104171837103791132111', b'\xf1\x875Q0959655T \xf1\x890830\xf1\x82\x13271100111312--071104171826102691131211', b'\xf1\x875QD959655 \xf1\x890388\xf1\x82\x111413001113120006110417121D101D9112', ], (Ecu.eps, 0x712, None): [ - b'\xf1\x873Q0909144F \xf1\x895043\xf1\x82\00561A01612A0', - b'\xf1\x873Q0909144H \xf1\x895061\xf1\x82\00566A0J612A1', - b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\00566A00514A1', - b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\00566A0J712A1', - b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\00571A0J714A1', + b'\xf1\x873Q0909144F \xf1\x895043\xf1\x82\x0561A01612A0', + b'\xf1\x873Q0909144H \xf1\x895061\xf1\x82\x0566A0J612A1', + b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A00514A1', + b'\xf1\x873Q0909144J \xf1\x895063\xf1\x82\x0566A0J712A1', + b'\xf1\x873Q0909144K \xf1\x895072\xf1\x82\x0571A0J714A1', b'\xf1\x873Q0909144L \xf1\x895081\xf1\x82\x0571A0JA15A1', - b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\00571A01A18A1', - b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\00571A0JA16A1', + b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A01A18A1', + b'\xf1\x873Q0909144M \xf1\x895082\xf1\x82\x0571A0JA16A1', b'\xf1\x875Q0909143K \xf1\x892033\xf1\x820519A9040203', - b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\00521A00441A1', + b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00441A1', b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00608A1', b'\xf1\x875Q0909144AA\xf1\x891081\xf1\x82\x0521A00641A1', - b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521A00442A1', - b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521A00642A1', - b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\00521A07B05A1', - b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\00521A00602A0', - b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\00522A00402A0', + b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A00442A1', + b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A00642A1', + b'\xf1\x875Q0909144AB\xf1\x891082\xf1\x82\x0521A07B05A1', + b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0521A00602A0', + b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0522A00402A0', b'\xf1\x875Q0909144L \xf1\x891021\xf1\x82\x0521A00502A0', - b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\00511A00403A0', - b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\00516A00604A1', + b'\xf1\x875Q0909144P \xf1\x891043\xf1\x82\x0511A00403A0', + b'\xf1\x875Q0909144R \xf1\x891061\xf1\x82\x0516A00604A1', b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00404A1', - b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\00516A00604A1', - b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\00516A07A02A1', - b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\00521A00507A1', + b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A00604A1', + b'\xf1\x875Q0909144S \xf1\x891063\xf1\x82\x0516A07A02A1', + b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A00507A1', b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A07B04A1', - b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\00521A20B03A1', + b'\xf1\x875Q0909144T \xf1\x891072\xf1\x82\x0521A20B03A1', b'\xf1\x875QD909144B \xf1\x891072\xf1\x82\x0521A00507A1', b'\xf1\x875QM909144A \xf1\x891072\xf1\x82\x0521A20B03A1', - b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\00521A00442A1', - b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\00571A01A16A1', - b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\00571A01A18A1', + b'\xf1\x875QM909144B \xf1\x891081\xf1\x82\x0521A00442A1', + b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A16A1', + b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A18A1', b'\xf1\x875QN909144A \xf1\x895081\xf1\x82\x0571A01A17A1', - b'\xf1\x875QN909144B \xf1\x895082\xf1\x82\00571A01A18A1', + b'\xf1\x875QN909144B \xf1\x895082\xf1\x82\x0571A01A18A1', b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567A2000400', ], (Ecu.fwdRadar, 0x757, None): [ - b'\xf1\x875Q0907567G \xf1\x890390\xf1\x82\00101', + b'\xf1\x875Q0907567G \xf1\x890390\xf1\x82\x0101', b'\xf1\x875Q0907567J \xf1\x890396\xf1\x82\x0101', - b'\xf1\x875Q0907572A \xf1\x890141\xf1\x82\00101', - b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\00101', - b'\xf1\x875Q0907572C \xf1\x890210\xf1\x82\00101', - b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\00101', - b'\xf1\x875Q0907572E \xf1\x89X310\xf1\x82\00101', - b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\00101', + b'\xf1\x875Q0907572A \xf1\x890141\xf1\x82\x0101', + b'\xf1\x875Q0907572B \xf1\x890200\xf1\x82\x0101', + b'\xf1\x875Q0907572C \xf1\x890210\xf1\x82\x0101', + b'\xf1\x875Q0907572D \xf1\x890304\xf1\x82\x0101', + b'\xf1\x875Q0907572E \xf1\x89X310\xf1\x82\x0101', + b'\xf1\x875Q0907572F \xf1\x890400\xf1\x82\x0101', b'\xf1\x875Q0907572G \xf1\x890571', b'\xf1\x875Q0907572H \xf1\x890620', b'\xf1\x875Q0907572J \xf1\x890654', From c3e95d5e194be9d445f54661d0ae0f0612db6a62 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 21 Jul 2022 14:48:08 +0200 Subject: [PATCH 391/436] prereq for matching speeds shown on cars dash (#25222) * prereq for matching speeds shown on cars dash * also handle fallback in the ui for replay * update translations * update ref --- selfdrive/car/interfaces.py | 6 ++++++ selfdrive/controls/controlsd.py | 6 ++++++ selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/ui/qt/onroad.cc | 8 ++++++-- selfdrive/ui/translations/main_ko.ts | 16 ++++++++-------- selfdrive/ui/translations/main_zh-CHS.ts | 16 ++++++++-------- selfdrive/ui/translations/main_zh-CHT.ts | 16 ++++++++-------- 7 files changed, 43 insertions(+), 27 deletions(-) diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index 4c7ea97dffd4c2..a451ef2ae14373 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -162,6 +162,12 @@ def update(self, c: car.CarControl, can_strings: List[bytes]) -> car.CarState: ret.canValid = all(cp.can_valid for cp in self.can_parsers if cp is not None) ret.canTimeout = any(cp.bus_timeout for cp in self.can_parsers if cp is not None) + if ret.vEgoCluster == 0.0: + ret.vEgoCluster = ret.vEgo + + if ret.cruiseState.speedCluster == 0: + ret.cruiseState.speedCluster = ret.cruiseState.speed + # copy back for next iteration reader = ret.as_reader() if self.CS is not None: diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index a20a3a9f37ba51..b6f323d376b5cb 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -163,6 +163,7 @@ def __init__(self, sm=None, pm=None, can_sock=None, CI=None): self.can_rcv_error = False self.soft_disable_timer = 0 self.v_cruise_kph = 255 + self.v_cruise_cluster_kph = 255 self.v_cruise_kph_last = 0 self.mismatch_counter = 0 self.cruise_mismatch_counter = 0 @@ -454,11 +455,14 @@ def state_transition(self, CS): if not self.CP.pcmCruise: self.v_cruise_kph = update_v_cruise(self.v_cruise_kph, CS.vEgo, CS.gasPressed, CS.buttonEvents, self.button_timers, self.enabled, self.is_metric) + self.v_cruise_cluster_kph = self.v_cruise_kph else: if CS.cruiseState.available: self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH + self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH else: self.v_cruise_kph = 0 + self.v_cruise_cluster_kph = 0 # decrement the soft disable timer at every step, as it's reset on # entrance in SOFT_DISABLING state @@ -538,6 +542,7 @@ def state_transition(self, CS): self.current_alert_types.append(ET.ENABLE) if not self.CP.pcmCruise: self.v_cruise_kph = initialize_v_cruise(CS.vEgo, CS.buttonEvents, self.v_cruise_kph_last) + self.v_cruise_cluster_kph = self.v_cruise_kph # Check if openpilot is engaged and actuators are enabled self.enabled = self.state in ENABLED_STATES @@ -752,6 +757,7 @@ def publish_logs(self, CS, start_time, CC, lac_log): controlsState.longControlState = self.LoC.long_control_state controlsState.vPid = float(self.LoC.v_pid) controlsState.vCruise = float(self.v_cruise_kph) + controlsState.vCruiseCluster = float(self.v_cruise_cluster_kph) controlsState.upAccelCmd = float(self.LoC.pid.p) controlsState.uiAccelCmd = float(self.LoC.pid.i) controlsState.ufAccelCmd = float(self.LoC.pid.f) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 00bf28ed8317ed..a9339bc4ff773b 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -7c1168af0311d2fef67b82812cd863a0e97c030e \ No newline at end of file +c428493b2a46cb996a7565ea4395d7ad6cd79786 diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index fe39cd0cfc579c..fbfa7e646d31a9 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -184,13 +184,17 @@ void NvgWindow::updateState(const UIState &s) { const auto cs = sm["controlsState"].getControlsState(); - float set_speed = cs_alive ? cs.getVCruise() : SET_SPEED_NA; + // Handle older routes where vCruiseCluster is not set + float v_cruise = cs.getVCruiseCluster() == 0.0 ? cs.getVCruise() : cs.getVCruiseCluster(); + float set_speed = cs_alive ? v_cruise : SET_SPEED_NA; bool cruise_set = set_speed > 0 && (int)set_speed != SET_SPEED_NA; if (cruise_set && !s.scene.is_metric) { set_speed *= KM_TO_MILE; } - float cur_speed = cs_alive ? std::max(0.0, sm["carState"].getCarState().getVEgo()) : 0.0; + // Handle older routes where vEgoCluster is not set + float v_ego = sm["carState"].getCarState().getVEgoCluster() == 0.0 ? sm["carState"].getCarState().getVEgo() : sm["carState"].getCarState().getVEgoCluster(); + float cur_speed = cs_alive ? std::max(0.0, v_ego) : 0.0; cur_speed *= s.scene.is_metric ? MS_TO_KPH : MS_TO_MPH; auto speed_limit_sign = sm["navInstruction"].getNavInstruction().getSpeedLimitSign(); diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index ec8f3a52d6abc3..acff1de2b10f14 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -488,30 +488,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX MAX - - + + SPEED SPEED - - + + LIMIT LIMIT diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 9485e9449add74..438ce65d70ccd9 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -486,30 +486,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX 最高定速 - - + + SPEED SPEED - - + + LIMIT LIMIT diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 9a3b800417a451..ade999f2559054 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -488,30 +488,30 @@ location set NvgWindow - + km/h km/h - + mph mph - - + + MAX 最高 - - + + SPEED 速度 - - + + LIMIT 速限 From 75473daf31c8fd2b1d4ecc6a60fe232f344c15ac Mon Sep 17 00:00:00 2001 From: Jafar Al-Gharaibeh Date: Thu, 21 Jul 2022 12:20:58 -0500 Subject: [PATCH 392/436] Mazda: update supported models info (#25230) * Mazda: update supported models info Signed-off-by: Jafar Al-Gharaibeh * correct format Co-authored-by: Shane Smiskol --- selfdrive/car/mazda/values.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/selfdrive/car/mazda/values.py b/selfdrive/car/mazda/values.py index e1d6907991372f..7db401e1617779 100644 --- a/selfdrive/car/mazda/values.py +++ b/selfdrive/car/mazda/values.py @@ -36,10 +36,10 @@ class MazdaCarInfo(CarInfo): CAR_INFO: Dict[str, Union[MazdaCarInfo, List[MazdaCarInfo]]] = { - CAR.CX5: MazdaCarInfo("Mazda CX-5 2017, 2019"), # TODO: verify years and torque for first 4 - CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-17"), - CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017"), - CAR.MAZDA6: MazdaCarInfo("Mazda 6 2017"), + CAR.CX5: MazdaCarInfo("Mazda CX-5 2017-21"), + CAR.CX9: MazdaCarInfo("Mazda CX-9 2016-20"), + CAR.MAZDA3: MazdaCarInfo("Mazda 3 2017-18"), + CAR.MAZDA6: MazdaCarInfo("Mazda 6 2017-20"), CAR.CX9_2021: MazdaCarInfo("Mazda CX-9 2021-22"), CAR.CX5_2022: MazdaCarInfo("Mazda CX-5 2022"), } From e66145f9e0ec561a2e630e03e2c94d760874fc48 Mon Sep 17 00:00:00 2001 From: Lee Jong Mun <43285072+crwusiz@users.noreply.github.com> Date: Fri, 22 Jul 2022 02:24:40 +0900 Subject: [PATCH 393/436] Multilanguage: Korean improvements/translations (#25231) * Update util.cc * Revert "Update util.cc" This reverts commit d7d2f4f6c16785084375b8b8e3ae63dda414c872. * Multilanguage: kor translations update --- selfdrive/ui/translations/main_ko.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index acff1de2b10f14..9928a57e1407af 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -638,17 +638,17 @@ location set %1 minute%2 ago - %1 분%2 전 + %1 분전 %1 hour%2 ago - %1 시간%2 전 + %1 시간전 %1 day%2 ago - %1 일%2 전 + %1 일전 @@ -1017,23 +1017,23 @@ location set Switch Branch - + 브랜치 변경 ENTER - + 입력하세요 The new branch will be pulled the next time the updater runs. - + 다음 업데이트 프로그램이 실행될 때 새 브랜치가 적용됩니다. Enter branch name - + 브랜치명 입력 @@ -1043,7 +1043,7 @@ location set Uninstall %1 - 제거 %1 + %1 제거 @@ -1217,12 +1217,12 @@ location set Show Map on Left Side of UI - + UI 왼쪽에 지도 표시 Show map on left side when in split screen view. - + 분할 화면 보기에서 지도를 왼쪽에 표시합니다. From 289239710d48ae1450e8c42c04373f7ee5e79d8e Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 21 Jul 2022 11:02:59 -0700 Subject: [PATCH 394/436] better star descriptions (#25236) * better star descriptions * most --- docs/CARS.md | 10 +++++----- selfdrive/car/docs_definitions.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 73211a8d2199e3..49f2f6e85d2be4 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -5,16 +5,16 @@ A supported vehicle is one that just works when you install a comma device. Ever ## How We Rate The Cars ### Stop and Go -- - Adaptive Cruise Control (ACC) operates down to 0 mph. -- - Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed. +- - openpilot operates down to 0 mph. +- - openpilot operates only above a minimum speed. See your car's manual for the minimum speed. ### Steer to 0 - - openpilot can control the steering wheel down to 0 mph. -- - No steering control below certain speeds. +- - No steering control below certain speeds. See your car's manual for the minimum speed. ### Steering Torque -- - Car has enough steering torque to take tight turns. -- - Limited ability to make turns. +- - Car has enough steering torque to comfortably take most highway turns. +- - Limited ability to make tighter turns. # 196 Supported Cars diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 3c58b30436cc0a..01473e4cc327c3 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -172,18 +172,18 @@ class Harness(Enum): STAR_DESCRIPTIONS = { "Gas & Brakes": { # icon and row name Column.FSR_LONGITUDINAL.value: [ - [Star.FULL.value, "Adaptive Cruise Control (ACC) operates down to 0 mph."], - [Star.EMPTY.value, "Adaptive Cruise Control (ACC) available only above certain speeds. See your car's manual for the minimum speed."], + [Star.FULL.value, "openpilot operates down to 0 mph."], + [Star.EMPTY.value, "openpilot operates only above a minimum speed. See your car's manual for the minimum speed."], ], }, "Steering": { Column.FSR_STEERING.value: [ [Star.FULL.value, "openpilot can control the steering wheel down to 0 mph."], - [Star.EMPTY.value, "No steering control below certain speeds."], + [Star.EMPTY.value, "No steering control below certain speeds. See your car's manual for the minimum speed."], ], Column.STEERING_TORQUE.value: [ - [Star.FULL.value, "Car has enough steering torque to take tight turns."], - [Star.EMPTY.value, "Limited ability to make turns."], + [Star.FULL.value, "Car has enough steering torque to comfortably take most highway turns."], + [Star.EMPTY.value, "Limited ability to make tighter turns."], ], }, } From 8e3595185dc2cf0d3ee35fa135384c4acc6bb5ef Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 21 Jul 2022 11:03:43 -0700 Subject: [PATCH 395/436] Kia Niro EV 2019: add missing FW versions (#25223) * Add missing FW versions * sort --- selfdrive/car/hyundai/values.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 9c3d852eda3b33..1391bf09021aaa 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -966,6 +966,7 @@ class Buttons: CAR.KIA_NIRO_EV: { (Ecu.fwdRadar, 0x7D0, None): [ b'\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4000 ', + b'\xf1\x00DEev SCC F-CUP 1.00 1.02 96400-Q4000 ', b'\xf1\x00DEev SCC F-CUP 1.00 1.02 96400-Q4100 ', b'\xf1\x00DEev SCC F-CUP 1.00 1.03 96400-Q4100 ', b'\xf1\x8799110Q4000\xf1\x00DEev SCC F-CUP 1.00 1.00 99110-Q4000 ', @@ -978,6 +979,7 @@ class Buttons: (Ecu.eps, 0x7D4, None): [ b'\xf1\x00DE MDPS C 1.00 1.05 56310Q4000\x00 4DEEC105', b'\xf1\x00DE MDPS C 1.00 1.05 56310Q4100\x00 4DEEC105', + b'\xf1\x00DE MDPS C 1.00 1.04 56310Q4100\x00 4DEEC104', ], (Ecu.fwdCamera, 0x7C4, None): [ b'\xf1\x00DEE MFC AT EUR LHD 1.00 1.00 99211-Q4100 200706', @@ -985,6 +987,7 @@ class Buttons: b'\xf1\x00DEE MFC AT USA LHD 1.00 1.00 99211-Q4000 191211', b'\xf1\x00DEE MFC AT USA LHD 1.00 1.03 95740-Q4000 180821', b'\xf1\x00DEE MFC AT USA LHD 1.00 1.01 99211-Q4500 210428', + b'\xf1\x00DEE MFC AT EUR LHD 1.00 1.03 95740-Q4000 180821', ], }, CAR.KIA_NIRO_HEV: { From cb73f9dbc38fff4ab249d8379a788a6b6d7770e3 Mon Sep 17 00:00:00 2001 From: eFini Date: Fri, 22 Jul 2022 04:50:04 +0800 Subject: [PATCH 396/436] Chinese (Traditional): Add/improve translations (#25228) fixed readme shortened ETA & revert unit --- selfdrive/ui/translations/README.md | 2 +- selfdrive/ui/translations/main_zh-CHT.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/translations/README.md b/selfdrive/ui/translations/README.md index a6f2f665ebdd86..96d251c95318d1 100644 --- a/selfdrive/ui/translations/README.md +++ b/selfdrive/ui/translations/README.md @@ -8,7 +8,7 @@ Before getting started, make sure you have set up the openpilot Ubuntu developme openpilot provides a few tools to help contributors manage their translations and to ensure quality. To get started: -1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Simplified Chinese is `中文(繁體)`). +1. Add your new language to [languages.json](/selfdrive/ui/translations/languages.json) with the appropriate [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) and the localized language name (Traditional Chinese is `中文(繁體)`). 2. Generate the XML translation file (`*.ts`): ```shell selfdrive/ui/update_translations.py diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index ade999f2559054..85d40c60a34ef5 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -135,7 +135,7 @@ Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。僅在熄火時可用。 + 預覽駕駛員監控鏡頭畫面,以確保其具有良好視野。(僅在熄火時可用) @@ -340,7 +340,7 @@ eta - 埃塔 + 抵達 @@ -440,7 +440,7 @@ location set Map Loading - 地圖加載 + 地圖加載中 @@ -1020,23 +1020,23 @@ location set Switch Branch - + 切換分支 ENTER - + 切換 The new branch will be pulled the next time the updater runs. - + 新的分支將會在下次檢查更新時切換過去。 Enter branch name - + 輸入分支名稱 @@ -1220,12 +1220,12 @@ location set Show Map on Left Side of UI - + 將地圖顯示在畫面的左側 Show map on left side when in split screen view. - + 進入分割畫面後,地圖將會顯示在畫面的左側。 From 063cd137ef022d9eefd56d9ca6f8d4bf4a59e615 Mon Sep 17 00:00:00 2001 From: chengyu-k <59636949+chengyu-k@users.noreply.github.com> Date: Fri, 22 Jul 2022 06:05:02 +0900 Subject: [PATCH 397/436] Impreza 2017: add missing eps FW (#24892) * Update carFw for Impreza 2017 * Update selfdrive/car/subaru/values.py Co-authored-by: Shane Smiskol --- selfdrive/car/subaru/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/subaru/values.py b/selfdrive/car/subaru/values.py index ae471a56e7b3f9..362e9ccd6af5f7 100644 --- a/selfdrive/car/subaru/values.py +++ b/selfdrive/car/subaru/values.py @@ -114,6 +114,7 @@ class SubaruCarInfo(CarInfo): b'z\xc0\x04\x00', b'z\xc0\x00\x00', b'\x8a\xc0\x10\x00', + b'z\xc0\n\x00', ], (Ecu.fwdCamera, 0x787, None): [ b'\x00\x00\x64\xb5\x1f\x40\x20\x0e', From aec9b2cb8a65b2631b385de3a7a09dcc2342d584 Mon Sep 17 00:00:00 2001 From: Christian Gribneau Date: Thu, 21 Jul 2022 17:11:04 -0400 Subject: [PATCH 398/436] Hyundai: add Genesis G90 2017 FW versions (#25183) * v2 fingerprint 2017 Genesis G90 Ultimate 5.0 AWD * remove Genesis G90 from 255 steering torque blocklist * Update selfdrive/car/hyundai/values.py Co-authored-by: Shane Smiskol --- selfdrive/car/hyundai/values.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 1391bf09021aaa..791d84305f7472 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -858,6 +858,12 @@ class Buttons: b'\xf1\x81640H0051\x00\x00\x00\x00\x00\x00\x00\x00', ], }, + CAR.GENESIS_G90: { + (Ecu.transmission, 0x7e1, None): [b'\xf1\x87VDGMD15866192DD3x\x88x\x89wuFvvfUf\x88vWwgwwwvfVgx\x87o\xff\xbc^\xf1\x81E14\x00\x00\x00\x00\x00\x00\x00\xf1\x00bcshcm49 E14\x00\x00\x00\x00\x00\x00\x00SHI0G50NB1tc5\xb7'], + (Ecu.fwdRadar, 0x7d0, None): [b'\xf1\x00HI__ SCC F-CUP 1.00 1.01 96400-D2100 '], + (Ecu.fwdCamera, 0x7c4, None): [b'\xf1\x00HI LKAS AT USA LHD 1.00 1.00 95895-D2020 160302'], + (Ecu.engine, 0x7e0, None): [b'\xf1\x810000000000\x00'], + }, CAR.KONA: { (Ecu.fwdRadar, 0x7d0, None): [b'\xf1\x00OS__ SCC F-CUP 1.00 1.00 95655-J9200 ', ], (Ecu.esp, 0x7d1, None): [b'\xf1\x816V5RAK00018.ELF\xf1\x00\x00\x00\x00\x00\x00\x00', ], From e6e8607306b14ca1d869846007d91f240bfe2c7a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 21 Jul 2022 14:14:24 -0700 Subject: [PATCH 399/436] implement string representation for Bootlog --- tools/lib/bootlog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/lib/bootlog.py b/tools/lib/bootlog.py index 4e711204002bc9..35153708238f28 100644 --- a/tools/lib/bootlog.py +++ b/tools/lib/bootlog.py @@ -35,6 +35,9 @@ def timestamp(self) -> str: def datetime(self) -> datetime.datetime: return timestamp_to_datetime(self._timestamp) + def __str__(self): + return f"{self._dongle_id}|{self._timestamp}" + def __eq__(self, b) -> bool: if not isinstance(b, Bootlog): return False From 937013e48867862bccf0153064f104778e356f5d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 21 Jul 2022 16:35:47 -0700 Subject: [PATCH 400/436] FPv2: log all responses (#25239) * should be all that's required * try using rx_addr (should be the same) * log bus * bump cereal * remove debugging * bump cereal to master * rm line * add printing to offline debug script --- cereal | 2 +- selfdrive/car/fw_versions.py | 36 ++++++++++------------ selfdrive/debug/test_fw_query_on_routes.py | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/cereal b/cereal index 7870a1123d4e8d..bc999518e73c2b 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit 7870a1123d4e8dfe5c39e86224c2f851382f3fed +Subproject commit bc999518e73c2b04fee3b8207b3c28a8644233ea diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index ee2c0f31d45114..9c6f6e44f03e53 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -452,7 +452,8 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, addrs.insert(0, parallel_addrs) - fw_versions = {} + # Get versions and build capnp list to put into CarParams + car_fw = [] requests = [r for r in REQUESTS if query_brand is None or r.brand == query_brand] for addr in tqdm(addrs, disable=not progress): for addr_chunk in chunks(addr): @@ -463,26 +464,23 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, if addrs: query = IsoTpParallelQuery(sendcan, logcan, r.bus, addrs, r.request, r.response, r.rx_offset, debug=debug) - fw_versions.update({(r.brand, addr): (version, r) for (addr, _), version in query.get_data(timeout).items()}) - except Exception: - cloudlog.warning(f"FW query exception: {traceback.format_exc()}") + for (addr, rx_addr), version in query.get_data(timeout).items(): + f = car.CarParams.CarFw.new_message() - # Build capnp list to put into CarParams - car_fw = [] - for (brand, addr), (version, request) in fw_versions.items(): - f = car.CarParams.CarFw.new_message() + f.ecu = ecu_types[(r.brand, addr[0], addr[1])] + f.fwVersion = version + f.address = addr[0] + f.responseAddress = rx_addr + f.request = r.request + f.brand = r.brand + f.bus = r.bus - f.ecu = ecu_types[(brand, addr[0], addr[1])] - f.fwVersion = version - f.address = addr[0] - f.responseAddress = uds.get_rx_addr_for_tx_addr(addr[0], request.rx_offset) - f.request = request.request - f.brand = brand + if addr[1] is not None: + f.subAddress = addr[1] - if addr[1] is not None: - f.subAddress = addr[1] - - car_fw.append(f) + car_fw.append(f) + except Exception: + cloudlog.warning(f"FW query exception: {traceback.format_exc()}") return car_fw @@ -531,7 +529,7 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, padding = max([len(fw.brand) for fw in fw_vers] or [0]) for version in fw_vers: subaddr = None if version.subAddress == 0 else hex(version.subAddress) - print(f" Brand: {version.brand:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") + print(f" Brand: {version.brand:{padding}}, bus: {version.bus} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}]") print("}") print() diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 191411f45a77ae..0cc674cfc8220f 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -115,7 +115,7 @@ padding = max([len(fw.brand or UNKNOWN_BRAND) for fw in car_fw]) for version in sorted(car_fw, key=lambda fw: fw.brand): subaddr = None if version.subAddress == 0 else hex(version.subAddress) - print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") + print(f" Brand: {version.brand or UNKNOWN_BRAND:{padding}}, bus: {version.bus} - (Ecu.{version.ecu}, {hex(version.address)}, {subaddr}): [{version.fwVersion}],") print("Mismatches") found = False From 0ca62bf7df0fe81829c744fd8689c9b97cb6a36b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 21 Jul 2022 20:54:53 -0700 Subject: [PATCH 401/436] let can packer handle counter (#25240) * let can packer handle counter * diff is expected * update refs * clean that up * bump opendbc * fix sim --- opendbc | 2 +- panda | 2 +- selfdrive/car/body/bodycan.py | 6 +-- selfdrive/car/body/carcontroller.py | 2 +- selfdrive/car/chrysler/carcontroller.py | 3 +- selfdrive/car/chrysler/chryslercan.py | 7 +-- selfdrive/car/honda/carcontroller.py | 20 ++++----- selfdrive/car/honda/hondacan.py | 36 ++++++++-------- selfdrive/car/hyundai/carcontroller.py | 4 +- selfdrive/car/hyundai/hda2can.py | 11 ++--- selfdrive/car/nissan/carcontroller.py | 4 +- selfdrive/car/nissan/nissancan.py | 7 ++- selfdrive/car/subaru/carcontroller.py | 8 ++-- selfdrive/car/subaru/subarucan.py | 17 +++----- selfdrive/car/toyota/carcontroller.py | 2 +- selfdrive/car/toyota/toyotacan.py | 3 +- selfdrive/car/volkswagen/carcontroller.py | 4 +- selfdrive/car/volkswagen/volkswagencan.py | 9 ++-- selfdrive/test/process_replay/ref_commit | 2 +- tools/sim/lib/can.py | 52 +++++++++++------------ 20 files changed, 94 insertions(+), 107 deletions(-) diff --git a/opendbc b/opendbc index e2465cc701e878..4195e8f4c9998c 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit e2465cc701e878fdae5b4e363496c2fa5d2f7c08 +Subproject commit 4195e8f4c9998c0a1a6084c3cc71499307bbd81e diff --git a/panda b/panda index 1dfee5973bc288..a1686ca3ca55b0 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 1dfee5973bc2884b61e55b7614f2fe90d0e2a1f3 +Subproject commit a1686ca3ca55b004179f10bfc45af5fbd701ca64 diff --git a/selfdrive/car/body/bodycan.py b/selfdrive/car/body/bodycan.py index cc448d53d1a89c..580e5025ade783 100644 --- a/selfdrive/car/body/bodycan.py +++ b/selfdrive/car/body/bodycan.py @@ -1,9 +1,7 @@ -def create_control(packer, torque_l, torque_r, idx): - can_bus = 0 - +def create_control(packer, torque_l, torque_r): values = { "TORQUE_L": torque_l, "TORQUE_R": torque_r, } - return packer.make_can_msg("TORQUE_CMD", can_bus, values, idx) + return packer.make_can_msg("TORQUE_CMD", 0, values) diff --git a/selfdrive/car/body/carcontroller.py b/selfdrive/car/body/carcontroller.py index c13acebacb78b3..7705cd86cbf9c0 100644 --- a/selfdrive/car/body/carcontroller.py +++ b/selfdrive/car/body/carcontroller.py @@ -80,7 +80,7 @@ def update(self, CC, CS): torque_l = int(np.clip(self.torque_l_filtered, -MAX_TORQUE, MAX_TORQUE)) can_sends = [] - can_sends.append(bodycan.create_control(self.packer, torque_l, torque_r, self.frame // 2)) + can_sends.append(bodycan.create_control(self.packer, torque_l, torque_r)) new_actuators = CC.actuators.copy() new_actuators.accel = torque_l diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 6d158e7cd0c949..344a2f1623cb7c 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -70,8 +70,7 @@ def update(self, CC, CS): self.steer_rate_limited = new_steer != apply_steer self.apply_steer_last = apply_steer - idx = self.frame // 2 - can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_control_bit, idx)) + can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_control_bit)) self.frame += 1 if not lkas_control_bit and self.lkas_control_bit_prev: diff --git a/selfdrive/car/chrysler/chryslercan.py b/selfdrive/car/chrysler/chryslercan.py index 1e26a6d275634c..10ed73e9f2bcfe 100644 --- a/selfdrive/car/chrysler/chryslercan.py +++ b/selfdrive/car/chrysler/chryslercan.py @@ -52,19 +52,20 @@ def create_lkas_hud(packer, CP, lkas_active, hud_alert, hud_count, car_model, au return packer.make_can_msg("DAS_6", 0, values) -def create_lkas_command(packer, CP, apply_steer, lkas_control_bit, frame): +def create_lkas_command(packer, CP, apply_steer, lkas_control_bit): # LKAS_COMMAND Lane-keeping signal to turn the wheel enabled_val = 2 if CP.carFingerprint in RAM_CARS else 1 values = { "STEERING_TORQUE": apply_steer, "LKAS_CONTROL_BIT": enabled_val if lkas_control_bit else 0, } - return packer.make_can_msg("LKAS_COMMAND", 0, values, frame % 0x10) + return packer.make_can_msg("LKAS_COMMAND", 0, values) def create_cruise_buttons(packer, frame, bus, cancel=False, resume=False): values = { "ACC_Cancel": cancel, "ACC_Resume": resume, + "COUNTER": frame % 0x10, } - return packer.make_can_msg("CRUISE_BUTTONS", bus, values, frame % 0x10) + return packer.make_can_msg("CRUISE_BUTTONS", bus, values) diff --git a/selfdrive/car/honda/carcontroller.py b/selfdrive/car/honda/carcontroller.py index d47caaa9ad1664..a5f25e3a2eb417 100644 --- a/selfdrive/car/honda/carcontroller.py +++ b/selfdrive/car/honda/carcontroller.py @@ -155,9 +155,8 @@ def update(self, CC, CS): can_sends.append((0x18DAB0F1, 0, b"\x02\x3E\x80\x00\x00\x00\x00\x00", 1)) # Send steering command. - idx = self.frame % 4 can_sends.append(hondacan.create_steering_control(self.packer, apply_steer, CC.latActive, self.CP.carFingerprint, - idx, CS.CP.openpilotLongitudinalControl)) + CS.CP.openpilotLongitudinalControl)) # wind brake from air resistance decel at high speed wind_brake = interp(CS.out.vEgo, [0.0, 2.3, 35.0], [0.001, 0.002, 0.15]) @@ -190,18 +189,16 @@ def update(self, CC, CS): if not self.CP.openpilotLongitudinalControl: if self.frame % 2 == 0 and self.CP.carFingerprint not in HONDA_BOSCH_RADARLESS: # radarless cars don't have supplemental message - idx = self.frame // 2 - can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CP.carFingerprint, idx)) + can_sends.append(hondacan.create_bosch_supplemental_1(self.packer, self.CP.carFingerprint)) # If using stock ACC, spam cancel command to kill gas when OP disengages. if pcm_cancel_cmd: - can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.CANCEL, idx, self.CP.carFingerprint)) + can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.CANCEL, self.CP.carFingerprint)) elif CC.cruiseControl.resume: - can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.RES_ACCEL, idx, self.CP.carFingerprint)) + can_sends.append(hondacan.spam_buttons_command(self.packer, CruiseButtons.RES_ACCEL, self.CP.carFingerprint)) else: # Send gas and brake commands. if self.frame % 2 == 0: - idx = self.frame // 2 ts = self.frame * DT_CTRL if self.CP.carFingerprint in HONDA_BOSCH: @@ -210,7 +207,7 @@ def update(self, CC, CS): stopping = actuators.longControlState == LongCtrlState.stopping can_sends.extend(hondacan.create_acc_commands(self.packer, CC.enabled, CC.longActive, self.accel, self.gas, - idx, stopping, self.CP.carFingerprint)) + stopping, self.CP.carFingerprint)) else: apply_brake = clip(self.brake_last - wind_brake, 0.0, 1.0) apply_brake = int(clip(apply_brake * self.params.NIDEC_BRAKE_MAX, 0, self.params.NIDEC_BRAKE_MAX - 1)) @@ -218,7 +215,7 @@ def update(self, CC, CS): pcm_override = True can_sends.append(hondacan.create_brake_command(self.packer, apply_brake, pump_on, - pcm_override, pcm_cancel_cmd, fcw_display, idx, + pcm_override, pcm_cancel_cmd, fcw_display, self.CP.carFingerprint, CS.stock_brake)) self.apply_brake_last = apply_brake self.brake = apply_brake / self.params.NIDEC_BRAKE_MAX @@ -234,14 +231,13 @@ def update(self, CC, CS): self.gas = clip(gas_mult * (gas - brake + wind_brake * 3 / 4), 0., 1.) else: self.gas = 0.0 - can_sends.append(create_gas_interceptor_command(self.packer, self.gas, idx)) + can_sends.append(create_gas_interceptor_command(self.packer, self.gas, self.frame // 2)) # Send dashboard UI commands. if self.frame % 10 == 0: - idx = (self.frame // 10) % 4 hud = HUDData(int(pcm_accel), int(round(hud_v_cruise)), hud_control.leadVisible, hud_control.lanesVisible, fcw_display, acc_alert, steer_required) - can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, idx, CS.stock_hud)) + can_sends.extend(hondacan.create_ui_commands(self.packer, self.CP, CC.enabled, pcm_speed, hud, CS.is_metric, CS.stock_hud)) if self.CP.openpilotLongitudinalControl and self.CP.carFingerprint not in HONDA_BOSCH: self.speed = pcm_speed diff --git a/selfdrive/car/honda/hondacan.py b/selfdrive/car/honda/hondacan.py index 7246b98686bd47..1f5f75426b7f4a 100644 --- a/selfdrive/car/honda/hondacan.py +++ b/selfdrive/car/honda/hondacan.py @@ -20,7 +20,7 @@ def get_lkas_cmd_bus(car_fingerprint, radar_disabled=False): return 0 -def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, idx, car_fingerprint, stock_brake): +def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_cmd, fcw, car_fingerprint, stock_brake): # TODO: do we loose pressure if we keep pump off for long? brakelights = apply_brake > 0 brake_rq = apply_brake > 0 @@ -42,10 +42,10 @@ def create_brake_command(packer, apply_brake, pump_on, pcm_override, pcm_cancel_ "AEB_STATUS": 0, } bus = get_pt_bus(car_fingerprint) - return packer.make_can_msg("BRAKE_COMMAND", bus, values, idx) + return packer.make_can_msg("BRAKE_COMMAND", bus, values) -def create_acc_commands(packer, enabled, active, accel, gas, idx, stopping, car_fingerprint): +def create_acc_commands(packer, enabled, active, accel, gas, stopping, car_fingerprint): commands = [] bus = get_pt_bus(car_fingerprint) min_gas_accel = CarControllerParams.BOSCH_GAS_LOOKUP_BP[0] @@ -67,7 +67,7 @@ def create_acc_commands(packer, enabled, active, accel, gas, idx, stopping, car_ "STANDSTILL": standstill, "STANDSTILL_RELEASE": standstill_release, } - commands.append(packer.make_can_msg("ACC_CONTROL", bus, acc_control_values, idx)) + commands.append(packer.make_can_msg("ACC_CONTROL", bus, acc_control_values)) acc_control_on_values = { "SET_TO_3": 0x03, @@ -76,21 +76,21 @@ def create_acc_commands(packer, enabled, active, accel, gas, idx, stopping, car_ "SET_TO_75": 0x75, "SET_TO_30": 0x30, } - commands.append(packer.make_can_msg("ACC_CONTROL_ON", bus, acc_control_on_values, idx)) + commands.append(packer.make_can_msg("ACC_CONTROL_ON", bus, acc_control_on_values)) return commands -def create_steering_control(packer, apply_steer, lkas_active, car_fingerprint, idx, radar_disabled): +def create_steering_control(packer, apply_steer, lkas_active, car_fingerprint, radar_disabled): values = { "STEER_TORQUE": apply_steer if lkas_active else 0, "STEER_TORQUE_REQUEST": lkas_active, } bus = get_lkas_cmd_bus(car_fingerprint, radar_disabled) - return packer.make_can_msg("STEERING_CONTROL", bus, values, idx) + return packer.make_can_msg("STEERING_CONTROL", bus, values) -def create_bosch_supplemental_1(packer, car_fingerprint, idx): +def create_bosch_supplemental_1(packer, car_fingerprint): # non-active params values = { "SET_ME_X04": 0x04, @@ -98,10 +98,10 @@ def create_bosch_supplemental_1(packer, car_fingerprint, idx): "SET_ME_X10": 0x10, } bus = get_lkas_cmd_bus(car_fingerprint) - return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values, idx) + return packer.make_can_msg("BOSCH_SUPPLEMENTAL_1", bus, values) -def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stock_hud): +def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, stock_hud): commands = [] bus_pt = get_pt_bus(CP.carFingerprint) radar_disabled = CP.carFingerprint in HONDA_BOSCH and CP.openpilotLongitudinalControl @@ -129,7 +129,7 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc acc_hud_values['FCM_OFF_2'] = stock_hud['FCM_OFF_2'] acc_hud_values['FCM_PROBLEM'] = stock_hud['FCM_PROBLEM'] acc_hud_values['ICONS'] = stock_hud['ICONS'] - commands.append(packer.make_can_msg("ACC_HUD", bus_pt, acc_hud_values, idx)) + commands.append(packer.make_can_msg("ACC_HUD", bus_pt, acc_hud_values)) lkas_hud_values = { 'SET_ME_X41': 0x41, @@ -146,29 +146,29 @@ def create_ui_commands(packer, CP, enabled, pcm_speed, hud, is_metric, idx, stoc lkas_hud_values['SET_ME_X48'] = 0x48 if CP.flags & HondaFlags.BOSCH_EXT_HUD and not CP.openpilotLongitudinalControl: - commands.append(packer.make_can_msg('LKAS_HUD_A', bus_lkas, lkas_hud_values, idx)) - commands.append(packer.make_can_msg('LKAS_HUD_B', bus_lkas, lkas_hud_values, idx)) + commands.append(packer.make_can_msg('LKAS_HUD_A', bus_lkas, lkas_hud_values)) + commands.append(packer.make_can_msg('LKAS_HUD_B', bus_lkas, lkas_hud_values)) else: - commands.append(packer.make_can_msg('LKAS_HUD', bus_lkas, lkas_hud_values, idx)) + commands.append(packer.make_can_msg('LKAS_HUD', bus_lkas, lkas_hud_values)) if radar_disabled and CP.carFingerprint in HONDA_BOSCH: radar_hud_values = { 'CMBS_OFF': 0x01, 'SET_TO_1': 0x01, } - commands.append(packer.make_can_msg('RADAR_HUD', bus_pt, radar_hud_values, idx)) + commands.append(packer.make_can_msg('RADAR_HUD', bus_pt, radar_hud_values)) if CP.carFingerprint == CAR.CIVIC_BOSCH: - commands.append(packer.make_can_msg("LEGACY_BRAKE_COMMAND", bus_pt, {}, idx)) + commands.append(packer.make_can_msg("LEGACY_BRAKE_COMMAND", bus_pt, {})) return commands -def spam_buttons_command(packer, button_val, idx, car_fingerprint): +def spam_buttons_command(packer, button_val, car_fingerprint): values = { 'CRUISE_BUTTONS': button_val, 'CRUISE_SETTING': 0, } # send buttons to camera on radarless cars bus = 2 if car_fingerprint in HONDA_BOSCH_RADARLESS else get_pt_bus(car_fingerprint) - return packer.make_can_msg("SCM_BUTTONS", bus, values, idx) + return packer.make_can_msg("SCM_BUTTONS", bus, values) diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index d0d9c4083981c9..981a3309d28edf 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -73,10 +73,10 @@ def update(self, CC, CS): if self.CP.carFingerprint in HDA2_CAR: # steering control - can_sends.append(hda2can.create_lkas(self.packer, CC.enabled, self.frame, CC.latActive, apply_steer)) + can_sends.append(hda2can.create_lkas(self.packer, CC.enabled, CC.latActive, apply_steer)) if self.frame % 5 == 0: - can_sends.append(hda2can.create_cam_0x2a4(self.packer, self.frame, CS.cam_0x2a4)) + can_sends.append(hda2can.create_cam_0x2a4(self.packer, CS.cam_0x2a4)) # cruise cancel if (self.frame - self.last_button_frame) * DT_CTRL > 0.25: diff --git a/selfdrive/car/hyundai/hda2can.py b/selfdrive/car/hyundai/hda2can.py index 9a9e477cf50dbb..36207aa113280d 100644 --- a/selfdrive/car/hyundai/hda2can.py +++ b/selfdrive/car/hyundai/hda2can.py @@ -1,4 +1,4 @@ -def create_lkas(packer, enabled, frame, lat_active, apply_steer): +def create_lkas(packer, enabled, lat_active, apply_steer): values = { "LKA_MODE": 2, "LKA_ICON": 2 if enabled else 1, @@ -10,17 +10,18 @@ def create_lkas(packer, enabled, frame, lat_active, apply_steer): "NEW_SIGNAL_1": 0, "NEW_SIGNAL_2": 0, } - return packer.make_can_msg("LKAS", 4, values, frame % 255) + return packer.make_can_msg("LKAS", 4, values) -def create_cam_0x2a4(packer, frame, camera_values): +def create_cam_0x2a4(packer, camera_values): camera_values.update({ "BYTE7": 0, }) - return packer.make_can_msg("CAM_0x2a4", 4, camera_values, frame % 255) + return packer.make_can_msg("CAM_0x2a4", 4, camera_values) def create_buttons(packer, cnt, btn): values = { + "COUNTER": cnt, "SET_ME_1": 1, "CRUISE_BUTTONS": btn, } - return packer.make_can_msg("CRUISE_BUTTONS", 5, values, cnt % 0xf) + return packer.make_can_msg("CRUISE_BUTTONS", 5, values) diff --git a/selfdrive/car/nissan/carcontroller.py b/selfdrive/car/nissan/carcontroller.py index 7d9dad6693afce..dbc2b33c6ba6a8 100644 --- a/selfdrive/car/nissan/carcontroller.py +++ b/selfdrive/car/nissan/carcontroller.py @@ -33,7 +33,7 @@ def update(self, CC, CS): steer_hud_alert = 1 if hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw) else 0 if CC.latActive: - # # windup slower + # windup slower if self.last_angle * apply_angle > 0. and abs(apply_angle) > abs(self.last_angle): angle_rate_lim = interp(CS.out.vEgo, CarControllerParams.ANGLE_DELTA_BP, CarControllerParams.ANGLE_DELTA_V) else: @@ -60,7 +60,7 @@ def update(self, CC, CS): self.last_angle = apply_angle if self.CP.carFingerprint in (CAR.ROGUE, CAR.XTRAIL, CAR.ALTIMA) and pcm_cancel_cmd: - can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg, self.frame)) + can_sends.append(nissancan.create_acc_cancel_cmd(self.packer, self.car_fingerprint, CS.cruise_throttle_msg)) # TODO: Find better way to cancel! # For some reason spamming the cancel button is unreliable on the Leaf diff --git a/selfdrive/car/nissan/nissancan.py b/selfdrive/car/nissan/nissancan.py index ceace5088ad8c8..f59714b8d431a0 100644 --- a/selfdrive/car/nissan/nissancan.py +++ b/selfdrive/car/nissan/nissancan.py @@ -2,17 +2,17 @@ import crcmod from selfdrive.car.nissan.values import CAR +# TODO: add this checksum to the CANPacker nissan_checksum = crcmod.mkCrcFun(0x11d, initCrc=0x00, rev=False, xorOut=0xff) def create_steering_control(packer, apply_steer, frame, steer_on, lkas_max_torque): - idx = (frame % 16) values = { + "COUNTER": frame % 0x10, "DESIRED_ANGLE": apply_steer, "SET_0x80_2": 0x80, "SET_0x80": 0x80, "MAX_TORQUE": lkas_max_torque if steer_on else 0, - "COUNTER": idx, "LKA_ACTIVE": steer_on, } @@ -22,7 +22,7 @@ def create_steering_control(packer, apply_steer, frame, steer_on, lkas_max_torqu return packer.make_can_msg("LKAS", 0, values) -def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg, frame): +def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg): values = copy.copy(cruise_throttle_msg) can_bus = 2 @@ -35,7 +35,6 @@ def create_acc_cancel_cmd(packer, car_fingerprint, cruise_throttle_msg, frame): values["SET_BUTTON"] = 0 values["RES_BUTTON"] = 0 values["FOLLOW_DISTANCE_BUTTON"] = 0 - values["COUNTER"] = (frame % 4) return packer.make_can_msg("CRUISE_THROTTLE", can_bus, values) diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index 3fb4bc8553c246..6cb754ac6fa107 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -39,9 +39,9 @@ def update(self, CC, CS): apply_steer = 0 if self.CP.carFingerprint in PREGLOBAL_CARS: - can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer, self.frame, self.p.STEER_STEP)) + can_sends.append(subarucan.create_preglobal_steering_control(self.packer, apply_steer)) else: - can_sends.append(subarucan.create_steering_control(self.packer, apply_steer, self.frame, self.p.STEER_STEP)) + can_sends.append(subarucan.create_steering_control(self.packer, apply_steer)) self.apply_steer_last = apply_steer @@ -74,7 +74,9 @@ def update(self, CC, CS): self.es_distance_cnt = CS.es_distance_msg["COUNTER"] if self.es_lkas_cnt != CS.es_lkas_msg["COUNTER"]: - can_sends.append(subarucan.create_es_lkas(self.packer, CS.es_lkas_msg, CC.enabled, hud_control.visualAlert, hud_control.leftLaneVisible, hud_control.rightLaneVisible, hud_control.leftLaneDepart, hud_control.rightLaneDepart)) + can_sends.append(subarucan.create_es_lkas(self.packer, CS.es_lkas_msg, CC.enabled, hud_control.visualAlert, + hud_control.leftLaneVisible, hud_control.rightLaneVisible, + hud_control.leftLaneDepart, hud_control.rightLaneDepart)) self.es_lkas_cnt = CS.es_lkas_msg["COUNTER"] new_actuators = actuators.copy() diff --git a/selfdrive/car/subaru/subarucan.py b/selfdrive/car/subaru/subarucan.py index e6efe7aa7b79a9..5f2233be9debe4 100644 --- a/selfdrive/car/subaru/subarucan.py +++ b/selfdrive/car/subaru/subarucan.py @@ -3,19 +3,15 @@ VisualAlert = car.CarControl.HUDControl.VisualAlert -def create_steering_control(packer, apply_steer, frame, steer_step): - - idx = (frame / steer_step) % 16 - +def create_steering_control(packer, apply_steer): values = { "LKAS_Output": apply_steer, "LKAS_Request": 1 if apply_steer != 0 else 0, "SET_1": 1 } + return packer.make_can_msg("ES_LKAS", 0, values) - return packer.make_can_msg("ES_LKAS", 0, values, idx) - -def create_steering_status(packer, apply_steer, frame, steer_step): +def create_steering_status(packer): return packer.make_can_msg("ES_LKAS_State", 0, {}) def create_es_distance(packer, es_distance_msg, pcm_cancel_cmd): @@ -66,17 +62,14 @@ def subaru_preglobal_checksum(packer, values, addr): dat = packer.make_can_msg(addr, 0, values)[2] return (sum(dat[:7])) % 256 -def create_preglobal_steering_control(packer, apply_steer, frame, steer_step): - - idx = (frame / steer_step) % 8 - +def create_preglobal_steering_control(packer, apply_steer): values = { "LKAS_Command": apply_steer, "LKAS_Active": 1 if apply_steer != 0 else 0 } values["Checksum"] = subaru_preglobal_checksum(packer, values, "ES_LKAS") - return packer.make_can_msg("ES_LKAS", 0, values, idx) + return packer.make_can_msg("ES_LKAS", 0, values) def create_preglobal_es_distance(packer, cruise_button, es_distance_msg): diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index 61a41b9c51e550..390e5f81702caa 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -83,7 +83,7 @@ def update(self, CC, CS): # toyota can trace shows this message at 42Hz, with counter adding alternatively 1 and 2; # sending it at 100Hz seem to allow a higher rate limit, as the rate limit seems imposed # on consecutive messages - can_sends.append(create_steer_command(self.packer, apply_steer, apply_steer_req, self.frame)) + can_sends.append(create_steer_command(self.packer, apply_steer, apply_steer_req)) if self.frame % 2 == 0 and self.CP.carFingerprint in TSS2_CAR: can_sends.append(create_lta_steer_command(self.packer, 0, 0, self.frame // 2)) diff --git a/selfdrive/car/toyota/toyotacan.py b/selfdrive/car/toyota/toyotacan.py index d57131dcb913d9..7ab3ab3e78a5e0 100644 --- a/selfdrive/car/toyota/toyotacan.py +++ b/selfdrive/car/toyota/toyotacan.py @@ -1,10 +1,9 @@ -def create_steer_command(packer, steer, steer_req, raw_cnt): +def create_steer_command(packer, steer, steer_req): """Creates a CAN message for the Toyota Steer Command.""" values = { "STEER_REQUEST": steer_req, "STEER_TORQUE_CMD": steer, - "COUNTER": raw_cnt, "SET_ME_1": 1, } return packer.make_can_msg("STEERING_LKA", 0, values) diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 1643fbe9b6d1b0..e38cd9705fb6f2 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -69,9 +69,7 @@ def update(self, CC, CS, ext_bus): apply_steer = 0 self.apply_steer_last = apply_steer - idx = (self.frame / P.HCA_STEP) % 16 - can_sends.append(volkswagencan.create_mqb_steering_control(self.packer_pt, CANBUS.pt, apply_steer, - idx, hcaEnabled)) + can_sends.append(volkswagencan.create_mqb_steering_control(self.packer_pt, CANBUS.pt, apply_steer, hcaEnabled)) # **** HUD Controls ***************************************************** # diff --git a/selfdrive/car/volkswagen/volkswagencan.py b/selfdrive/car/volkswagen/volkswagencan.py index 10e0054c79472c..1b42d1a405a626 100644 --- a/selfdrive/car/volkswagen/volkswagencan.py +++ b/selfdrive/car/volkswagen/volkswagencan.py @@ -1,7 +1,7 @@ -# CAN controls for MQB platform Volkswagen, Audi, Skoda and SEAT. +# CAN controls for MQB platform Volkswagen, Audi, Skoda, and SEAT. # PQ35/PQ46/NMS, and any future MLB, to come later. -def create_mqb_steering_control(packer, bus, apply_steer, idx, lkas_enabled): +def create_mqb_steering_control(packer, bus, apply_steer, lkas_enabled): values = { "SET_ME_0X3": 0x3, "Assist_Torque": abs(apply_steer), @@ -13,7 +13,7 @@ def create_mqb_steering_control(packer, bus, apply_steer, idx, lkas_enabled): "SET_ME_0XFE": 0xFE, "SET_ME_0X07": 0x07, } - return packer.make_can_msg("HCA_01", bus, values, idx) + return packer.make_can_msg("HCA_01", bus, values) def create_mqb_hud_control(packer, bus, enabled, steering_pressed, hud_alert, left_lane_visible, right_lane_visible, ldw_stock_values, left_lane_depart, right_lane_depart): @@ -34,6 +34,7 @@ def create_mqb_hud_control(packer, bus, enabled, steering_pressed, hud_alert, le def create_mqb_acc_buttons_control(packer, bus, buttonStatesToSend, CS, idx): values = { + "COUNTER": idx, "GRA_Hauptschalter": CS.graHauptschalter, "GRA_Abbrechen": buttonStatesToSend["cancel"], "GRA_Tip_Setzen": buttonStatesToSend["setCruise"], @@ -46,4 +47,4 @@ def create_mqb_acc_buttons_control(packer, bus, buttonStatesToSend, CS, idx): "GRA_Tip_Stufe_2": CS.graTipStufe2, "GRA_ButtonTypeInfo": CS.graButtonTypeInfo } - return packer.make_can_msg("GRA_ACC_01", bus, values, idx) + return packer.make_can_msg("GRA_ACC_01", bus, values) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index a9339bc4ff773b..41d683602a9840 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -c428493b2a46cb996a7565ea4395d7ad6cd79786 +658b1097663a8deea2c4d77c4c00a514739980bb \ No newline at end of file diff --git a/tools/sim/lib/can.py b/tools/sim/lib/can.py index 939d081a809183..af4339b2e49d0f 100755 --- a/tools/sim/lib/can.py +++ b/tools/sim/lib/can.py @@ -33,48 +33,48 @@ def can_function(pm, speed, angle, idx, cruise_button, is_engaged): # *** powertrain bus *** speed = speed * 3.6 # convert m/s to kph - msg.append(packer.make_can_msg("ENGINE_DATA", 0, {"XMISSION_SPEED": speed}, idx)) + msg.append(packer.make_can_msg("ENGINE_DATA", 0, {"XMISSION_SPEED": speed})) msg.append(packer.make_can_msg("WHEEL_SPEEDS", 0, { "WHEEL_SPEED_FL": speed, "WHEEL_SPEED_FR": speed, "WHEEL_SPEED_RL": speed, "WHEEL_SPEED_RR": speed - }, -1)) + })) - msg.append(packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": cruise_button}, idx)) + msg.append(packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": cruise_button})) values = {"COUNTER_PEDAL": idx & 0xF} - checksum = crc8_pedal(packer.make_can_msg("GAS_SENSOR", 0, {"COUNTER_PEDAL": idx & 0xF}, -1)[2][:-1]) + checksum = crc8_pedal(packer.make_can_msg("GAS_SENSOR", 0, {"COUNTER_PEDAL": idx & 0xF})[2][:-1]) values["CHECKSUM_PEDAL"] = checksum - msg.append(packer.make_can_msg("GAS_SENSOR", 0, values, -1)) - - msg.append(packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8}, idx)) - msg.append(packer.make_can_msg("GAS_PEDAL_2", 0, {}, idx)) - msg.append(packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1}, idx)) - msg.append(packer.make_can_msg("STEER_STATUS", 0, {}, idx)) - msg.append(packer.make_can_msg("STEERING_SENSORS", 0, {"STEER_ANGLE": angle}, idx)) - msg.append(packer.make_can_msg("VSA_STATUS", 0, {}, idx)) - msg.append(packer.make_can_msg("STANDSTILL", 0, {"WHEELS_MOVING": 1 if speed >= 1.0 else 0}, idx)) - msg.append(packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {}, idx)) - msg.append(packer.make_can_msg("EPB_STATUS", 0, {}, idx)) - msg.append(packer.make_can_msg("DOORS_STATUS", 0, {}, idx)) - msg.append(packer.make_can_msg("CRUISE_PARAMS", 0, {}, idx)) - msg.append(packer.make_can_msg("CRUISE", 0, {}, idx)) - msg.append(packer.make_can_msg("SCM_FEEDBACK", 0, {"MAIN_ON": 1}, idx)) - msg.append(packer.make_can_msg("POWERTRAIN_DATA", 0, {"ACC_STATUS": int(is_engaged)}, idx)) + msg.append(packer.make_can_msg("GAS_SENSOR", 0, values)) + + msg.append(packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) + msg.append(packer.make_can_msg("GAS_PEDAL_2", 0, {})) + msg.append(packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) + msg.append(packer.make_can_msg("STEER_STATUS", 0, {})) + msg.append(packer.make_can_msg("STEERING_SENSORS", 0, {"STEER_ANGLE": angle})) + msg.append(packer.make_can_msg("VSA_STATUS", 0, {})) + msg.append(packer.make_can_msg("STANDSTILL", 0, {"WHEELS_MOVING": 1 if speed >= 1.0 else 0})) + msg.append(packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) + msg.append(packer.make_can_msg("EPB_STATUS", 0, {})) + msg.append(packer.make_can_msg("DOORS_STATUS", 0, {})) + msg.append(packer.make_can_msg("CRUISE_PARAMS", 0, {})) + msg.append(packer.make_can_msg("CRUISE", 0, {})) + msg.append(packer.make_can_msg("SCM_FEEDBACK", 0, {"MAIN_ON": 1})) + msg.append(packer.make_can_msg("POWERTRAIN_DATA", 0, {"ACC_STATUS": int(is_engaged)})) msg.append(packer.make_can_msg("HUD_SETTING", 0, {})) - msg.append(packer.make_can_msg("CAR_SPEED", 0, {}, idx)) + msg.append(packer.make_can_msg("CAR_SPEED", 0, {})) # *** cam bus *** - msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {}, idx)) - msg.append(packer.make_can_msg("ACC_HUD", 2, {}, idx)) - msg.append(packer.make_can_msg("BRAKE_COMMAND", 2, {}, idx)) + msg.append(packer.make_can_msg("STEERING_CONTROL", 2, {})) + msg.append(packer.make_can_msg("ACC_HUD", 2, {})) + msg.append(packer.make_can_msg("BRAKE_COMMAND", 2, {})) # *** radar bus *** if idx % 5 == 0: - msg.append(rpacker.make_can_msg("RADAR_DIAGNOSTIC", 1, {"RADAR_STATE": 0x79}, -1)) + msg.append(rpacker.make_can_msg("RADAR_DIAGNOSTIC", 1, {"RADAR_STATE": 0x79})) for i in range(16): - msg.append(rpacker.make_can_msg("TRACK_%d" % i, 1, {"LONG_DIST": 255.5}, -1)) + msg.append(rpacker.make_can_msg("TRACK_%d" % i, 1, {"LONG_DIST": 255.5})) pm.send('can', can_list_to_can_capnp(msg)) From 2f808985c086fd54f1fbc13adeccabb78a07db21 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 21 Jul 2022 21:04:31 -0700 Subject: [PATCH 402/436] body: fix integrator freezing in turn loop (#25243) --- selfdrive/car/body/carcontroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/body/carcontroller.py b/selfdrive/car/body/carcontroller.py index 7705cd86cbf9c0..0d5d780bd313c3 100644 --- a/selfdrive/car/body/carcontroller.py +++ b/selfdrive/car/body/carcontroller.py @@ -61,8 +61,8 @@ def update(self, CC, CS): speed_diff_measured = SPEED_FROM_RPM * (CS.out.wheelSpeeds.fl - CS.out.wheelSpeeds.fr) turn_error = speed_diff_measured - speed_diff_desired - freeze_integrator = ((turn_error < 0 and self.turn_pid.error_integral <= -MAX_POS_INTEGRATOR) or - (turn_error > 0 and self.turn_pid.error_integral >= MAX_POS_INTEGRATOR)) + freeze_integrator = ((turn_error < 0 and self.turn_pid.error_integral <= -MAX_TURN_INTEGRATOR) or + (turn_error > 0 and self.turn_pid.error_integral >= MAX_TURN_INTEGRATOR)) torque_diff = self.turn_pid.update(turn_error, freeze_integrator=freeze_integrator) # Combine 2 PIDs outputs From 0c5668d96e714334a7d6c79678ee8577e6f6739b Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 21 Jul 2022 21:05:52 -0700 Subject: [PATCH 403/436] Revert "remove casync from agnos manifest for now" This reverts commit 8ea982264ec4ba7aa47a3228236f943e76a911c5. --- system/hardware/tici/agnos.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 7ccea95ee78380..853d3ab434a07b 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -47,6 +47,8 @@ "size": 10737418240, "sparse": true, "full_check": false, - "has_ab": true + "has_ab": true, + "casync_caibx": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13.caibx", + "casync_store": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13" } ] From f300a8df2b5dbc457cbb132ee4085c07e381a7fb Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 22 Jul 2022 13:21:53 +0200 Subject: [PATCH 404/436] use v_cruise_cluster_kph for hudControl.setSpeed (#25245) --- selfdrive/controls/controlsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index b6f323d376b5cb..f0927b9c743b49 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -674,7 +674,7 @@ def publish_logs(self, CS, start_time, CC, lac_log): CC.cruiseControl.resume = self.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 hudControl = CC.hudControl - hudControl.setSpeed = float(self.v_cruise_kph * CV.KPH_TO_MS) + hudControl.setSpeed = float(self.v_cruise_cluster_kph * CV.KPH_TO_MS) hudControl.speedVisible = self.enabled hudControl.lanesVisible = self.enabled hudControl.leadVisible = self.sm['longitudinalPlan'].hasLead From f0d494b7522bb1aa398ff1665a1a878bcd03b315 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Fri, 22 Jul 2022 16:25:06 +0200 Subject: [PATCH 405/436] Nissan: cleanup set speed (#25233) * Nissan: cleanup set speed * update ref --- selfdrive/car/nissan/carstate.py | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/nissan/carstate.py b/selfdrive/car/nissan/carstate.py index 3c5d7dc24b0bc7..a5ca2371103935 100644 --- a/selfdrive/car/nissan/carstate.py +++ b/selfdrive/car/nissan/carstate.py @@ -74,8 +74,8 @@ def update(self, cp, cp_adas, cp_cam): conversion = CV.MPH_TO_MS if cp.vl["HUD_SETTINGS"]["SPEED_MPH"] else CV.KPH_TO_MS else: conversion = CV.MPH_TO_MS if cp.vl["HUD"]["SPEED_MPH"] else CV.KPH_TO_MS - speed -= 1 # Speed on HUD is always 1 lower than actually sent on can bus ret.cruiseState.speed = speed * conversion + ret.cruiseState.speedCluster = (speed - 1) * conversion # Speed on HUD is always 1 lower than actually sent on can bus if self.CP.carFingerprint == CAR.ALTIMA: ret.steeringTorque = cp_cam.vl["STEER_TORQUE_SENSOR"]["STEER_TORQUE_DRIVER"] diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 41d683602a9840..b144852714c843 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -658b1097663a8deea2c4d77c4c00a514739980bb \ No newline at end of file +2bfa96aad8b951988cf9d0dd95d3c9d9019a1c0e \ No newline at end of file From 15846435fe16505fbb5a1e397dd87b47527ad33d Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Fri, 22 Jul 2022 10:01:08 -0700 Subject: [PATCH 406/436] Revert "Revert "remove casync from agnos manifest for now"" This reverts commit 0c5668d96e714334a7d6c79678ee8577e6f6739b. --- system/hardware/tici/agnos.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 853d3ab434a07b..7ccea95ee78380 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -47,8 +47,6 @@ "size": 10737418240, "sparse": true, "full_check": false, - "has_ab": true, - "casync_caibx": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13.caibx", - "casync_store": "https://commadist.azureedge.net/agnosupdate/system-59622eddd068d49f2e9df69ef5115e3f205ad369539690a5b240c8c93796dd13" + "has_ab": true } ] From daded8ebebb47d79fd0847b9a802d255df4eb02c Mon Sep 17 00:00:00 2001 From: cydia2020 <12470297+cydia2020@users.noreply.github.com> Date: Sat, 23 Jul 2022 03:18:45 +1000 Subject: [PATCH 407/436] Multilang: finish unfinished Simplified Chinese translations (#25247) * Finish unfinished translation * Update main_zh-CHS.ts --- selfdrive/ui/translations/main_zh-CHS.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 438ce65d70ccd9..da0841c3b511cc 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -1015,23 +1015,23 @@ location set Switch Branch - + 切换分支 ENTER - + 输入 The new branch will be pulled the next time the updater runs. - + 分支将在更新服务下次启动时自动切换。 Enter branch name - + 输入分支名称 @@ -1215,12 +1215,12 @@ location set Show Map on Left Side of UI - + 在介面左侧显示地图 Show map on left side when in split screen view. - + 在分屏模式中,将地图置于屏幕左侧。 From 3117c069d8ce28ca7c4f19afaae8016158992b9a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 22 Jul 2022 13:51:08 -0700 Subject: [PATCH 408/436] Car docs diff: fix new platform detection (#25252) * Fix new platform detection * add some helpful comments and clean up * slightly better --- selfdrive/debug/print_docs_diff.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py index c02649e3fab4e7..44c821a4fced15 100755 --- a/selfdrive/debug/print_docs_diff.py +++ b/selfdrive/debug/print_docs_diff.py @@ -20,17 +20,18 @@ def load_base_car_info(path): def match_cars(base_cars, new_cars): - """Matches CarInfo by name similarity and finds additions and removals""" changes = [] additions = [] for new in new_cars: - closest_match = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.)[0] - - if closest_match not in [c[1].name for c in changes]: - changes.append((new, next(car for car in base_cars if car.name == closest_match))) - else: + # Addition if no close matches or close match already used + # Change if close match and not already used + matches = difflib.get_close_matches(new.name, [b.name for b in base_cars], cutoff=0.) + if not len(matches) or matches[0] in [c[1].name for c in changes]: additions.append(new) + else: + changes.append((new, next(car for car in base_cars if car.name == matches[0]))) + # Removal if base car not in changes removals = [b for b in base_cars if b.name not in [c[1].name for c in changes]] return changes, additions, removals @@ -62,6 +63,9 @@ def print_car_info_diff(path): for car in get_all_car_info(): new_car_info[car.car_fingerprint].append(car) + # Add new platforms to base cars so we can detect additions and removals in one pass + base_car_info.update({car: [] for car in new_car_info if car not in base_car_info}) + changes = defaultdict(list) for base_car_model, base_cars in base_car_info.items(): # Match car info changes, and get additions and removals From 9ab1c492dd051276a422fbac38f754c4e6633f32 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 22 Jul 2022 14:07:54 -0700 Subject: [PATCH 409/436] Multilanguage badges (#25244) * Test badge * fix? * Test * debug * debug * debug * debug * debug * debug * debug * try this * need to build * a minute for what * download and commit test badge * get dynamically * fix * Add to readme * force push * should work * one step * Update badge * draft * clean up * remove these * one line is fine * Adding badges will have to be manual, but that should be fine * cause error * continue on error * hope this doesn't delete the badges * ugh, allow-failures would be so nice * whoops * yep need this * do this * now try a push * clean up * rm line * need this * see if this works * orange * does this work? * ? * do dis * needs to be global? * cool, this works cool, this works * run only on master * add back workflows * remove that * sorting * sorting * print badge markdown * it is bytes though? * run once more * revert * looks nicer * strange * no decimals * run again run once more * nice workflow_dispatch * only run on a schedule and remove error handling * make links absolute * adjust badge text --- .github/workflows/badges.yaml | 53 ++++++++++++++++++++++ selfdrive/ui/translations/README.md | 4 ++ selfdrive/ui/translations/create_badges.py | 43 ++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 .github/workflows/badges.yaml create mode 100755 selfdrive/ui/translations/create_badges.py diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml new file mode 100644 index 00000000000000..68a13398f6eaeb --- /dev/null +++ b/.github/workflows/badges.yaml @@ -0,0 +1,53 @@ +name: badges +on: + schedule: + - cron: '0 * * * *' + workflow_dispatch: + +env: + BASE_IMAGE: openpilot-base + DOCKER_REGISTRY: ghcr.io/commaai + + BUILD: | + docker pull $(grep -iohP '(?<=^from)\s+\S+' Dockerfile.openpilot_base) || true + docker pull $DOCKER_REGISTRY/$BASE_IMAGE:latest || true + docker build --cache-from $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $DOCKER_REGISTRY/$BASE_IMAGE:latest -t $BASE_IMAGE:latest -f Dockerfile.openpilot_base . + RUN: docker run --shm-size 1G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v /tmp/scons_cache:/tmp/scons_cache -v /tmp/comma_download_cache:/tmp/comma_download_cache -v /tmp/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/sh -c + +jobs: + badges: + name: create badges + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + + - name: Cache scons + id: scons-cache + # TODO: Change the version to the released version when https://github.com/actions/cache/pull/489 (or 571) is merged. + uses: actions/cache@03e00da99d75a2204924908e1cca7902cafce66b + env: + CACHE_SKIP_SAVE: true + with: + path: /tmp/scons_cache + key: scons-${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }}- + restore-keys: | + scons-${{ hashFiles('.github/workflows/selfdrive_tests.yaml') }}- + scons- + + - name: Build Docker image + run: eval "$BUILD" + + - name: Push badges + run: | + ${{ env.RUN }} "scons -j$(nproc) && python selfdrive/ui/translations/create_badges.py" + + git checkout --orphan badges + git rm -rf --cached . + git config user.email "badge-researcher@comma.ai" + git config user.name "Badge Researcher" + + git add translation_badge_*.svg + git commit -m "Add/Update badges" + git push -f origin HEAD diff --git a/selfdrive/ui/translations/README.md b/selfdrive/ui/translations/README.md index 96d251c95318d1..1ed73a0484b1aa 100644 --- a/selfdrive/ui/translations/README.md +++ b/selfdrive/ui/translations/README.md @@ -1,5 +1,9 @@ # Multilanguage +[![language](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge_main_zh-CHT.svg)](https://github.com/commaai/openpilot/blob/master/selfdrive/ui/translations/main_zh-CHT.ts) +[![language](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge_main_zh-CHS.svg)](https://github.com/commaai/openpilot/blob/master/selfdrive/ui/translations/main_zh-CHS.ts) +[![language](https://raw.githubusercontent.com/commaai/openpilot/badges/translation_badge_main_ko.svg)](https://github.com/commaai/openpilot/blob/master/selfdrive/ui/translations/main_ko.ts) + ## Contributing Before getting started, make sure you have set up the openpilot Ubuntu development environment by reading the [tools README.md](/tools/README.md). diff --git a/selfdrive/ui/translations/create_badges.py b/selfdrive/ui/translations/create_badges.py new file mode 100755 index 00000000000000..58b587462ff788 --- /dev/null +++ b/selfdrive/ui/translations/create_badges.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import json +import os +import requests + +from common.basedir import BASEDIR +from selfdrive.ui.update_translations import LANGUAGES_FILE, TRANSLATIONS_DIR + +TRANSLATION_TAG = "= 70 else "red" + + r = requests.get(f"https://img.shields.io/badge/LANGUAGE {name}-{percent_finished}%25 complete-{color}") + assert r.status_code == 200, "Error downloading badge" + + with open(os.path.join(BASEDIR, TRANSLATION_BADGE.format(file)), "wb") as badge_f: + badge_f.write(r.content) From f79a21d29ef29dafb958d60d3d7087fe27b4a01e Mon Sep 17 00:00:00 2001 From: alancyau <59986772+alancyau@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:30:51 -0500 Subject: [PATCH 410/436] Update incompatible Toyota Security cars (#25249) * Update CARS.md Update Toyota security cars. Toyota US doesn't sell the Yaris anymore. * add autogen comment * correct US model name Co-authored-by: Shane Smiskol --- docs/CARS.md | 11 +++++++---- selfdrive/car/CARS_template.md | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 49f2f6e85d2be4..12e93c850e0581 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -1,3 +1,5 @@ + + # Supported Cars A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. @@ -260,11 +262,12 @@ All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wi Specific new Toyota models are shipping with a new message authentication method that openpilot does not yet support. So far, this list includes: -* Toyota Rav4 Prime 2022+ +* Toyota RAV4 Prime 2021+ * Toyota Sienna 2021+ * Toyota Venza 2021+ * Toyota Sequoia 2023+ * Toyota Tundra 2022+ -* Toyota Yaris 2022+ -* Toyota Corolla Cross (only US model) -* Lexus NX 2021+ \ No newline at end of file +* Toyota Corolla Cross 2022+ (only US model) +* Lexus NX 2022+ +* Toyota bZ4x 2023+ +* Subaru Solterra 2023+ \ No newline at end of file diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index ba95eeccb8f3a1..db05c793775759 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -1,6 +1,8 @@ {% set footnote_tag = '[{}](#footnotes)' -%} {% set star_icon = '' -%} + + # Supported Cars A supported vehicle is one that just works when you install a comma device. Every car performs differently with openpilot, but all supported cars should provide a better experience than any stock system. @@ -63,11 +65,12 @@ All the cars that openpilot supports use a [CAN bus](https://en.wikipedia.org/wi Specific new Toyota models are shipping with a new message authentication method that openpilot does not yet support. So far, this list includes: -* Toyota Rav4 Prime 2022+ +* Toyota RAV4 Prime 2021+ * Toyota Sienna 2021+ * Toyota Venza 2021+ * Toyota Sequoia 2023+ * Toyota Tundra 2022+ -* Toyota Yaris 2022+ -* Toyota Corolla Cross (only US model) -* Lexus NX 2021+ +* Toyota Corolla Cross 2022+ (only US model) +* Lexus NX 2022+ +* Toyota bZ4x 2023+ +* Subaru Solterra 2023+ From 57aa085e183dea0e1b470682ecae4c443ab5909d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 22 Jul 2022 22:19:14 -0700 Subject: [PATCH 411/436] Fix footnote superscripts on website (#25260) * Fix footnotes on website * Revert "Fix footnotes on website" This reverts commit c2a11fa34e204e9360ac78e0986d7db2bc053cc6. * fix now, clean up later * Fix * revert --- selfdrive/car/docs.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/selfdrive/car/docs.py b/selfdrive/car/docs.py index 26f8949e8f5be8..3a81788c68017a 100755 --- a/selfdrive/car/docs.py +++ b/selfdrive/car/docs.py @@ -12,20 +12,21 @@ from selfdrive.car.hyundai.radar_interface import RADAR_START_ADDR as HKG_RADAR_START_ADDR -def get_all_footnotes() -> Dict[Enum, int]: +def get_all_footnotes(only_tier_cols: bool = False) -> Dict[Enum, int]: all_footnotes = [] + hide_cols = set(StarColumns) - set(TierColumns) if only_tier_cols else [] for footnotes in get_interface_attr("Footnote", ignore_none=True).values(): - all_footnotes += footnotes + all_footnotes.extend([fn for fn in footnotes if fn.value.column not in hide_cols]) return {fn: idx + 1 for idx, fn in enumerate(all_footnotes)} -ALL_FOOTNOTES: Dict[Enum, int] = get_all_footnotes() CARS_MD_OUT = os.path.join(BASEDIR, "docs", "CARS.md") CARS_MD_TEMPLATE = os.path.join(BASEDIR, "selfdrive", "car", "CARS_template.md") -def get_all_car_info() -> List[CarInfo]: +def get_all_car_info(only_tier_cols: bool = False) -> List[CarInfo]: all_car_info: List[CarInfo] = [] + footnotes = get_all_footnotes(only_tier_cols) for model, car_info in get_interface_attr("CAR_INFO", combine_brands=True).items(): # Hyundai exception: those with radar have openpilot longitudinal fingerprint = {0: {}, 1: {HKG_RADAR_START_ADDR: 8}, 2: {}, 3: {}} @@ -39,7 +40,7 @@ def get_all_car_info() -> List[CarInfo]: car_info = (car_info,) for _car_info in car_info: - all_car_info.append(_car_info.init(CP, ALL_FOOTNOTES)) + all_car_info.append(_car_info.init(CP, footnotes)) # Sort cars by make and model + year sorted_cars: List[CarInfo] = natsorted(all_car_info, key=lambda car: car.name.lower()) @@ -58,7 +59,7 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str, only_tier_co for c in hide_cols: del car.row[c] - footnotes = [fn.value.text for fn in ALL_FOOTNOTES if fn.value.column in cols] + footnotes = [fn.value.text for fn in get_all_footnotes(only_tier_cols)] cars_md: str = template.render(all_car_info=all_car_info, footnotes=footnotes, Star=Star, Column=cols, star_descriptions=STAR_DESCRIPTIONS) return cars_md @@ -74,5 +75,5 @@ def generate_cars_md(all_car_info: List[CarInfo], template_fn: str, only_tier_co args = parser.parse_args() with open(args.out, 'w') as f: - f.write(generate_cars_md(get_all_car_info(), args.template, args.tier_columns)) + f.write(generate_cars_md(get_all_car_info(args.tier_columns), args.template, args.tier_columns)) print(f"Generated and written to {args.out}") From fd5819e9ec79e7cb1b714a5302e90e10e04e36fe Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Sat, 23 Jul 2022 08:00:17 +0100 Subject: [PATCH 412/436] Fix for multiple footnotes in car docs (#25256) * Fix for multiple footnotes in car docs * Split footnotes with comma * oops * one link tag * fix empty footnotes * sort footnotes * Fix footnotes on website * Revert "Fix footnotes on website" This reverts commit 187686b0d7b591cf81a8d59ea6d45f7ba94b5fae. * fix sorting if > 9 * try both * it pushes star a bit too much and float: center doesn't work * try on gh try on gh * doesn't look too good * revert Co-authored-by: Shane Smiskol --- docs/CARS.md | 12 ++++++------ selfdrive/car/docs_definitions.py | 27 ++++++++++++--------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 12e93c850e0581..35cd0353116cea 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -188,14 +188,14 @@ A supported vehicle is one that just works when you install a comma device. Ever |Toyota|RAV4 Hybrid 2019-21|All||||| |Toyota|RAV4 Hybrid 2022|All||||| |Toyota|Sienna 2018-20|All|[3](#footnotes)|||| -|Volkswagen|Arteon 2018-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Arteon eHybrid 2020-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Arteon R 2020-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Arteon 2018-22[7,8](#footnotes)|Driver Assistance||||| +|Volkswagen|Arteon eHybrid 2020-22[7,8](#footnotes)|Driver Assistance||||| +|Volkswagen|Arteon R 2020-22[7,8](#footnotes)|Driver Assistance||||| |Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance||||| |Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance||||| |Volkswagen|California 2021[7](#footnotes)|Driver Assistance||||| |Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance||||| -|Volkswagen|CC 2018-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|CC 2018-22[7,8](#footnotes)|Driver Assistance||||| |Volkswagen|e-Golf 2014-20|Driver Assistance||||| |Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance||||| |Volkswagen|Golf Alltrack 2015-19|Driver Assistance||||| @@ -206,9 +206,9 @@ A supported vehicle is one that just works when you install a comma device. Ever |Volkswagen|Golf SportsVan 2015-20|Driver Assistance||||| |Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance||||| |Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Passat 2015-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Passat 2015-22[6,7,8](#footnotes)|Driver Assistance||||| |Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Passat GTE 2015-22[7](#footnotes)|Driver Assistance||||| +|Volkswagen|Passat GTE 2015-22[7,8](#footnotes)|Driver Assistance||||| |Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance||||| |Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance||||| |Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance||||| diff --git a/selfdrive/car/docs_definitions.py b/selfdrive/car/docs_definitions.py index 01473e4cc327c3..015646972d36f9 100644 --- a/selfdrive/car/docs_definitions.py +++ b/selfdrive/car/docs_definitions.py @@ -2,7 +2,7 @@ from cereal import car from collections import namedtuple -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum from typing import Dict, List, Optional, Tuple, Union, no_type_check @@ -37,13 +37,9 @@ class Star(Enum): CarFootnote = namedtuple("CarFootnote", ["text", "column", "star"], defaults=[None]) -def get_footnote(footnotes: Optional[List[Enum]], column: Column) -> Optional[Enum]: - # Returns applicable footnote given current column - if footnotes is not None: - for fn in footnotes: - if fn.value.column == column: - return fn - return None +def get_footnotes(footnotes: List[Enum], column: Column) -> List[Enum]: + # Returns applicable footnotes given current column + return [fn for fn in footnotes if fn.value.column == column] def split_name(name: str) -> Tuple[str, str, str]: @@ -61,7 +57,7 @@ class CarInfo: name: str package: str video_link: Optional[str] = None - footnotes: Optional[List[Enum]] = None + footnotes: List[Enum] = field(default_factory=list) min_steer_speed: Optional[float] = None min_enable_speed: Optional[float] = None harness: Optional[Enum] = None @@ -106,9 +102,9 @@ def init(self, CP: car.CarParams, all_footnotes: Dict[Enum, int]): self.all_footnotes = all_footnotes for column in StarColumns: # Demote if footnote specifies a star - footnote = get_footnote(self.footnotes, column) - if footnote is not None and footnote.value.star is not None: - self.row[column] = footnote.value.star + for fn in get_footnotes(self.footnotes, column): + if fn.value.star is not None: + self.row[column] = fn.value.star # openpilot ACC star doesn't count for tiers full_stars = [s for col, s in self.row.items() if col in TierColumns].count(Star.FULL) @@ -129,9 +125,10 @@ def get_column(self, column: Column, star_icon: str, footnote_tag: str) -> str: elif column == Column.MODEL and len(self.years): item += f" {self.years}" - footnote = get_footnote(self.footnotes, column) - if footnote is not None: - item += footnote_tag.format(self.all_footnotes[footnote]) + footnotes = get_footnotes(self.footnotes, column) + if len(footnotes): + sups = sorted([self.all_footnotes[fn] for fn in footnotes]) + item += footnote_tag.format(f'{",".join(map(str, sups))}') return item From e0cd4bd7ede6d4f536cf68b6856f414d81f9554b Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Sat, 23 Jul 2022 18:05:31 -0500 Subject: [PATCH 413/436] Add missing RAV4H_TSS2_2022 engine f/w (#25263) `@atran913#6118` 2022 RAV4 Hybrid DongleID/route d8117083fe3d623d|2022-07-23--19-42-53 --- selfdrive/car/toyota/values.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 48caef565a783c..0f29ae1ee2d2ef 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1401,6 +1401,7 @@ class ToyotaCarInfo(CarInfo): b'\x01896634A08000\x00\x00\x00\x00', b'\x01896634A61000\x00\x00\x00\x00', b'\x01896634A62000\x00\x00\x00\x00', + b'\x01896634A62100\x00\x00\x00\x00', b'\x01896634A63000\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ From 551d2fe511ac2801befaa93d525b5c8bdb608629 Mon Sep 17 00:00:00 2001 From: pcjx <109846435+pcjx@users.noreply.github.com> Date: Sun, 24 Jul 2022 01:49:41 +0200 Subject: [PATCH 414/436] Make ubuntu_setup.sh use codenames to support Ubuntu based distros (#25261) Using codenames allows the ubuntu_setup.sh to install properly on systems like Pop!_OS or Linux Mint. --- tools/ubuntu_setup.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/ubuntu_setup.sh b/tools/ubuntu_setup.sh index 5bc3630766f843..bd12b878f1f7e4 100755 --- a/tools/ubuntu_setup.sh +++ b/tools/ubuntu_setup.sh @@ -69,7 +69,7 @@ function install_ubuntu_common_requirements() { } # Install Ubuntu 22.04 LTS packages -function install_ubuntu_latest_requirements() { +function install_ubuntu_jammy_requirements() { install_ubuntu_common_requirements sudo apt-get install -y --no-install-recommends \ @@ -81,7 +81,7 @@ function install_ubuntu_latest_requirements() { } # Install Ubuntu 20.04 packages -function install_ubuntu_lts_requirements() { +function install_ubuntu_focal_requirements() { install_ubuntu_common_requirements sudo apt-get install -y --no-install-recommends \ @@ -93,12 +93,12 @@ function install_ubuntu_lts_requirements() { # Detect OS using /etc/os-release file if [ -f "/etc/os-release" ]; then source /etc/os-release - case "$ID $VERSION_ID" in - "ubuntu 22.04") - install_ubuntu_latest_requirements + case "$VERSION_CODENAME" in + "jammy") + install_ubuntu_jammy_requirements ;; - "ubuntu 20.04") - install_ubuntu_lts_requirements + "focal") + install_ubuntu_focal_requirements ;; *) echo "$ID $VERSION_ID is unsupported. This setup script is written for Ubuntu 20.04." @@ -107,7 +107,7 @@ if [ -f "/etc/os-release" ]; then if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi - install_ubuntu_lts_requirements + install_ubuntu_focal_requirements esac else echo "No /etc/os-release in the system" From d01847e4140b1e31961bae53bea57a5727e9040a Mon Sep 17 00:00:00 2001 From: Greg Hogan Date: Sat, 23 Jul 2022 16:50:47 -0700 Subject: [PATCH 415/436] hyundai: torque lateral controller for palisade (#25262) --- selfdrive/car/hyundai/interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index a32ee2c0ab8638..d5085e322f260f 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -78,8 +78,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl ret.wheelbase = 2.90 ret.steerRatio = 15.6 * 1.15 tire_stiffness_factor = 0.63 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.05]] + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) elif candidate in (CAR.ELANTRA, CAR.ELANTRA_GT_I30): ret.lateralTuning.pid.kf = 0.00006 ret.mass = 1275. + STD_CARGO_KG From 10b212dc7ef126f34150cd1c110b804f8cf0f082 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sat, 23 Jul 2022 23:25:30 -0700 Subject: [PATCH 416/436] Chrysler: add Ram 1500 FW values (#25266) --- selfdrive/car/chrysler/values.py | 79 ++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 5a979776ec569c..56b11652ff3441 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -1,7 +1,6 @@ -import capnp from dataclasses import dataclass from enum import Enum -from typing import Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Union from cereal import car from selfdrive.car import dbc_dict @@ -113,27 +112,67 @@ class ChryslerCarInfo(CarInfo): # Jeep Grand Cherokee 2019, including most 2020 models 55: 8, 168: 8, 179: 8, 181: 8, 256: 4, 257: 5, 258: 8, 264: 8, 268: 8, 272: 6, 273: 6, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 292: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 341: 8, 344: 8, 352: 8, 362: 8, 368: 8, 376: 3, 384: 8, 388: 4, 416: 7, 448: 6, 456: 4, 464: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 530: 8, 532: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 618: 8, 624: 8, 625: 8, 632: 8, 639: 8, 640: 1, 656: 4, 658: 6, 660: 8, 671: 8, 672: 8, 676: 8, 678: 8, 680: 8, 683: 8, 684: 8, 703: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 761: 8, 764: 8, 766: 8, 773: 8, 776: 8, 779: 8, 782: 8, 783: 8, 784: 8, 785: 8, 792: 8, 799: 8, 800: 8, 804: 8, 806: 2, 808: 8, 810: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 831: 6, 832: 8, 838: 2, 840: 8, 844: 5, 847: 1, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 882: 8, 897: 8, 906: 8, 924: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 960: 4, 968: 8, 969: 4, 970: 8, 973: 8, 974: 5, 976: 8, 977: 4, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8, 1216: 8, 1218: 8, 1220: 8, 1223: 8, 1225: 8, 1227: 8, 1235: 8, 1242: 8, 1250: 8, 1251: 8, 1252: 8, 1254: 8, 1264: 8, 1284: 8, 1536: 8, 1537: 8, 1543: 8, 1545: 8, 1562: 8, 1568: 8, 1570: 8, 1572: 8, 1593: 8, 1856: 8, 1858: 8, 1860: 8, 1863: 8, 1865: 8, 1867: 8, 1875: 8, 1882: 8, 1890: 8, 1891: 8, 1892: 8, 1894: 8, 1896: 8, 1904: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8 }], - CAR.RAM_1500: [ - {35: 8, 37: 8, 39: 8, 41: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 278: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 298: 8, 299: 8, 305: 8, 307: 8, 311: 8, 315: 8, 317: 8, 319: 8, 323: 8, 333: 8, 334: 8, 341: 8, 343: 8, 345: 8, 347: 8, 409: 6, 421: 8, 448: 6, 456: 4, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 505: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 611: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 659: 5, 663: 8, 664: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 752: 2, 754: 8, 773: 8, 788: 3, 792: 8, 808: 8, 818: 8, 819: 8, 822: 8, 823: 8, 825: 2, 838: 2, 840: 8, 848: 8, 856: 4, 860: 6, 862: 8, 875: 2, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 956: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8, 2015: 8, 2016: 8, 2017: 8, 2024: 8, 2025: 8}, - {35: 8, 37: 8, 39: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 276: 8, 277: 8, 278: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 299: 8, 301: 8, 302: 8, 305: 8, 307: 8, 311: 8, 317: 8, 319: 8, 323: 8, 327: 8, 333: 8, 334: 8, 341: 8, 343: 8, 345: 8, 347: 8, 421: 8, 448: 6, 456: 4, 457: 8, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 663: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 738: 8, 752: 2, 754: 8, 773: 8, 792: 8, 808: 8, 812: 8, 813: 8, 814: 8, 818: 8, 819: 8, 821: 8, 822: 8, 823: 8, 825: 2, 838: 2, 840: 8, 847: 1, 848: 8, 856: 4, 860: 6, 862: 8, 874: 2, 876: 8, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 975: 8, 976: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1030: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1098: 8, 1100: 8}, - {35: 8, 37: 8, 39: 8, 43: 8, 47: 8, 49: 8, 53: 8, 55: 8, 113: 8, 119: 2, 121: 8, 123: 7, 125: 6, 127: 8, 129: 8, 131: 8, 133: 8, 135: 8, 137: 8, 139: 8, 141: 8, 145: 8, 147: 8, 149: 7, 153: 8, 155: 8, 157: 8, 163: 8, 164: 8, 166: 8, 167: 8, 169: 8, 171: 8, 173: 5, 177: 3, 179: 8, 181: 8, 213: 3, 221: 8, 232: 8, 250: 8, 289: 5, 293: 3, 295: 8, 296: 8, 297: 4, 299: 8, 301: 8, 302: 8, 305: 8, 307: 8, 311: 8, 317: 8, 319: 8, 323: 8, 334: 8, 337: 8, 343: 8, 347: 8, 409: 6, 421: 8, 448: 6, 456: 4, 464: 8, 489: 8, 491: 8, 502: 8, 503: 8, 507: 5, 516: 7, 517: 7, 524: 8, 526: 6, 557: 8, 560: 8, 584: 8, 601: 8, 605: 8, 607: 8, 609: 8, 613: 8, 623: 8, 631: 8, 633: 8, 634: 8, 635: 8, 637: 8, 641: 8, 643: 8, 645: 2, 649: 8, 650: 8, 651: 8, 656: 4, 657: 8, 659: 5, 663: 8, 664: 8, 673: 8, 676: 8, 679: 8, 685: 8, 687: 8, 689: 5, 706: 8, 709: 8, 710: 8, 711: 8, 720: 6, 752: 2, 754: 8, 773: 8, 788: 3, 792: 8, 808: 8, 812: 8, 813: 8, 814: 8, 818: 8, 819: 8, 821: 8, 822: 8, 825: 2, 838: 2, 840: 8, 847: 1, 848: 8, 856: 4, 860: 6, 862: 8, 876: 8, 897: 8, 906: 8, 910: 8, 926: 3, 929: 8, 930: 8, 931: 8, 932: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 956: 8, 961: 8, 962: 8, 969: 4, 971: 8, 972: 8, 973: 8, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1030: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1062: 8, 1098: 8, 1100: 8}, - ], } -FW_VERSIONS: Dict[str, Dict[Tuple[capnp.lib.capnp._EnumModule, int, Optional[int]], List[str]]] = { +FW_VERSIONS = { CAR.RAM_1500: { - (Ecu.combinationMeter, 0x742, None): [], - (Ecu.srs, 0x744, None): [], - (Ecu.esp, 0x747, None): [], - (Ecu.fwdCamera, 0x753, None): [], - (Ecu.fwdCamera, 0x764, None): [], - (Ecu.eps, 0x761, None): [], - (Ecu.fwdRadar, 0x757, None): [], - (Ecu.eps, 0x75A, None): [], - (Ecu.engine, 0x7e0, None): [], - (Ecu.transmission, 0x7e1, None): [], - (Ecu.gateway, 0x18DACBF1, None): [], - } + (Ecu.combinationMeter, 0x742, None): [ + b'68294063AH', + b'68294063AG', + b'68434860AC', + b'68527375AD', + b'68453503AC', + ], + (Ecu.srs, 0x744, None): [ + b'68441329AB', + b'68490898AA', + b'68428609AB', + b'68500728AA', + ], + (Ecu.esp, 0x747, None): [ + b'68432418AD', + b'68432418AB', + b'68436004AE', + b'68438454AD', + b'68436004AD', + b'68535469AB', + b'68438454AC', + ], + (Ecu.fwdCamera, 0x753, None): [ + b'68320950AL', + b'68320950AJ', + b'68454268AB', + b'68475160AG', + b'04672892AB', + b'68475160AE', + ], + (Ecu.eps, 0x75A, None): [ + b'68273275AG', + b'68469901AA', + b'68552788AA', + ], + (Ecu.engine, 0x7e0, None): [ + b'68448163AJ', + b'68500630AD', + b'68539650AD', + ], + (Ecu.transmission, 0x7e1, None): [ + b'68360078AL', + b'68384328AD', + b'68360085AL', + b'68360081AM', + b'68502994AD', + b'68445533AB', + b'68540431AB', + b'68484467AC', + ], + (Ecu.gateway, 0x18DACBF1, None): [ + b'68402660AB', + b'68445283AB', + b'68533631AB', + b'68500483AB', + ], + }, } DBC = { From d462a08056d54a957084d594286626a2509c94aa Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 24 Jul 2022 14:56:55 -0700 Subject: [PATCH 417/436] remove CS.steeringRateLimited (#25251) * remove CS.steeringRateLimited * bump cereal * update refs --- cereal | 2 +- selfdrive/car/chrysler/carcontroller.py | 2 -- selfdrive/car/chrysler/interface.py | 2 -- selfdrive/car/ford/carcontroller.py | 2 -- selfdrive/car/ford/interface.py | 2 -- selfdrive/car/gm/carcontroller.py | 2 -- selfdrive/car/gm/interface.py | 2 -- selfdrive/car/hyundai/carcontroller.py | 2 -- selfdrive/car/hyundai/interface.py | 1 - selfdrive/car/mazda/carcontroller.py | 3 --- selfdrive/car/subaru/carcontroller.py | 2 -- selfdrive/car/subaru/interface.py | 2 -- selfdrive/car/toyota/carcontroller.py | 2 -- selfdrive/car/toyota/interface.py | 2 -- selfdrive/car/volkswagen/carcontroller.py | 3 --- selfdrive/car/volkswagen/interface.py | 1 - selfdrive/controls/controlsd.py | 4 ++- selfdrive/controls/lib/latcontrol.py | 6 ++--- selfdrive/controls/lib/latcontrol_angle.py | 4 +-- selfdrive/controls/lib/latcontrol_indi.py | 4 +-- selfdrive/controls/lib/latcontrol_pid.py | 4 +-- selfdrive/controls/lib/latcontrol_torque.py | 6 ++--- .../controls/lib/tests/test_latcontrol.py | 3 +-- selfdrive/test/process_replay/ref_commit | 2 +- .../test/process_replay/test_processes.py | 26 ++++++++++++++----- tools/tuning/measure_steering_accuracy.py | 2 +- 26 files changed, 39 insertions(+), 54 deletions(-) diff --git a/cereal b/cereal index bc999518e73c2b..9ae66d07129dcf 160000 --- a/cereal +++ b/cereal @@ -1 +1 @@ -Subproject commit bc999518e73c2b04fee3b8207b3c28a8644233ea +Subproject commit 9ae66d07129dcf1b5d6fc5167bbc89691a56d24c diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 344a2f1623cb7c..22abf1b677d5fa 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -10,7 +10,6 @@ def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 self.frame = 0 - self.steer_rate_limited = False self.hud_count = 0 self.last_lkas_falling_edge = 0 @@ -67,7 +66,6 @@ def update(self, CC, CS): apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorqueEps, self.params) if not lkas_active: apply_steer = 0 - self.steer_rate_limited = new_steer != apply_steer self.apply_steer_last = apply_steer can_sends.append(create_lkas_command(self.packer, self.CP, int(apply_steer), lkas_control_bit)) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index acc08954a844dd..56cece47bc9e8b 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -75,8 +75,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - # events events = self.create_common_events(ret, extra_gears=[car.CarState.GearShifter.low]) diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index d7666c1d6522f7..11d9bb3c79dc16 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -25,7 +25,6 @@ def __init__(self, dbc_name, CP, VM): self.frame = 0 self.apply_steer_last = 0 - self.steer_rate_limited = False self.main_on_last = False self.lkas_enabled_last = False self.steer_alert_last = False @@ -46,7 +45,6 @@ def update(self, CC, CS): # apply rate limits new_steer = actuators.steeringAngleDeg apply_steer = apply_ford_steer_angle_limits(new_steer, self.apply_steer_last, CS.out.vEgo) - self.steer_rate_limited = new_steer != apply_steer # send steering commands at 20Hz if (self.frame % CarControllerParams.LKAS_STEER_STEP) == 0: diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index 1af04bee41d6b5..857ccbab8bedc6 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -70,8 +70,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - events = self.create_common_events(ret) ret.events = events.to_msg() diff --git a/selfdrive/car/gm/carcontroller.py b/selfdrive/car/gm/carcontroller.py index f763a58532067c..727c05efdd53ba 100644 --- a/selfdrive/car/gm/carcontroller.py +++ b/selfdrive/car/gm/carcontroller.py @@ -22,7 +22,6 @@ def __init__(self, dbc_name, CP, VM): self.lka_steering_cmd_counter_last = -1 self.lka_icon_status_last = (False, False) - self.steer_rate_limited = False self.params = CarControllerParams() @@ -51,7 +50,6 @@ def update(self, CC, CS): if lkas_enabled: new_steer = int(round(actuators.steer * self.params.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params) - self.steer_rate_limited = new_steer != apply_steer else: apply_steer = 0 diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index e0dd10d4a8a841..bdb42f97e2ba13 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -156,8 +156,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa def _update(self, c): ret = self.CS.update(self.cp, self.cp_loopback) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT: be = create_button_event(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS) diff --git a/selfdrive/car/hyundai/carcontroller.py b/selfdrive/car/hyundai/carcontroller.py index 981a3309d28edf..0bdb7d99fd7c29 100644 --- a/selfdrive/car/hyundai/carcontroller.py +++ b/selfdrive/car/hyundai/carcontroller.py @@ -43,7 +43,6 @@ def __init__(self, dbc_name, CP, VM): self.apply_steer_last = 0 self.car_fingerprint = CP.carFingerprint - self.steer_rate_limited = False self.last_button_frame = 0 self.accel = 0 @@ -59,7 +58,6 @@ def update(self, CC, CS): steer = clip(steer, -0.7, 0.7) new_steer = int(round(steer * self.params.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.params) - self.steer_rate_limited = new_steer != apply_steer if not CC.latActive: apply_steer = 0 diff --git a/selfdrive/car/hyundai/interface.py b/selfdrive/car/hyundai/interface.py index d5085e322f260f..969540ad90e463 100644 --- a/selfdrive/car/hyundai/interface.py +++ b/selfdrive/car/hyundai/interface.py @@ -314,7 +314,6 @@ def init(CP, logcan, sendcan): def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False # On some newer model years, the CANCEL button acts as a pause/resume button based on the PCM state # To avoid re-engaging when openpilot cancels, check user engagement intention via buttons diff --git a/selfdrive/car/mazda/carcontroller.py b/selfdrive/car/mazda/carcontroller.py index a83cef508a0c9a..2add59ccb05884 100644 --- a/selfdrive/car/mazda/carcontroller.py +++ b/selfdrive/car/mazda/carcontroller.py @@ -12,7 +12,6 @@ def __init__(self, dbc_name, CP, VM): self.CP = CP self.apply_steer_last = 0 self.packer = CANPacker(dbc_name) - self.steer_rate_limited = False self.brake_counter = 0 self.frame = 0 @@ -20,14 +19,12 @@ def update(self, CC, CS): can_sends = [] apply_steer = 0 - self.steer_rate_limited = False if CC.latActive: # calculate steer and also set limits due to driver torque new_steer = int(round(CC.actuators.steer * CarControllerParams.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, CarControllerParams) - self.steer_rate_limited = new_steer != apply_steer if CC.cruiseControl.cancel: # If brake is pressed, let us wait >70ms before trying to disable crz to avoid diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index 6cb754ac6fa107..a336572051cfd6 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -11,7 +11,6 @@ def __init__(self, dbc_name, CP, VM): self.es_distance_cnt = -1 self.es_lkas_cnt = -1 self.cruise_button_prev = 0 - self.steer_rate_limited = False self.frame = 0 self.p = CarControllerParams(CP) @@ -33,7 +32,6 @@ def update(self, CC, CS): new_steer = int(round(apply_steer)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, self.p) - self.steer_rate_limited = new_steer != apply_steer if not CC.latActive: apply_steer = 0 diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index 952885a7511220..f820cf42ba743a 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -111,8 +111,6 @@ def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - ret.events = self.create_common_events(ret).to_msg() return ret diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index 390e5f81702caa..858a3815b40f2b 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -20,7 +20,6 @@ def __init__(self, dbc_name, CP, VM): self.alert_active = False self.last_standstill = False self.standstill_req = False - self.steer_rate_limited = False self.packer = CANPacker(dbc_name) self.gas = 0 @@ -52,7 +51,6 @@ def update(self, CC, CS): # steer torque new_steer = int(round(actuators.steer * CarControllerParams.STEER_MAX)) apply_steer = apply_toyota_steer_torque_limits(new_steer, self.last_steer, CS.out.steeringTorqueEps, self.torque_rate_limits) - self.steer_rate_limited = new_steer != apply_steer if not CC.latActive: apply_steer = 0 diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index 21a5d47f62557c..642c39952e3367 100644 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -243,8 +243,6 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=[], disabl def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - # events events = self.create_common_events(ret) diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index e38cd9705fb6f2..5543d4f05ce719 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -22,8 +22,6 @@ def __init__(self, dbc_name, CP, VM): self.graMsgStartFramePrev = 0 self.graMsgBusCounterPrev = 0 - self.steer_rate_limited = False - def update(self, CC, CS, ext_bus): actuators = CC.actuators hud_control = CC.hudControl @@ -46,7 +44,6 @@ def update(self, CC, CS, ext_bus): if CC.latActive: new_steer = int(round(actuators.steer * P.STEER_MAX)) apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.out.steeringTorque, P) - self.steer_rate_limited = new_steer != apply_steer if apply_steer == 0: hcaEnabled = False self.hcaEnabledFrameCount = 0 diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index c2b077b6db493d..225a6a32ca4e32 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -163,7 +163,6 @@ def _update(self, c): buttonEvents = [] ret = self.CS.update(self.cp, self.cp_cam, self.cp_ext, self.CP.transmissionType) - ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False # Check for and process state-change events (button press or release) from # the turn stalk switch or ACC steering wheel/control stalk buttons. diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index f0927b9c743b49..47a15c6ec1f07f 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -176,6 +176,7 @@ def __init__(self, sm=None, pm=None, can_sock=None, CI=None): self.logged_comm_issue = None self.button_timers = {ButtonEvent.Type.decelCruise: 0, ButtonEvent.Type.accelCruise: 0} self.last_actuators = car.CarControl.Actuators.new_message() + self.steer_limited = False self.desired_curvature = 0.0 self.desired_curvature_rate = 0.0 @@ -594,7 +595,7 @@ def state_control(self, CS): lat_plan.curvatures, lat_plan.curvatureRates) actuators.steer, actuators.steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, params, - self.last_actuators, self.desired_curvature, + self.last_actuators, self.steer_limited, self.desired_curvature, self.desired_curvature_rate, self.sm['liveLocationKalman']) else: lac_log = log.ControlsState.LateralDebugState.new_message() @@ -721,6 +722,7 @@ def publish_logs(self, CS, start_time, CC, lac_log): self.last_actuators, can_sends = self.CI.apply(CC) self.pm.send('sendcan', can_list_to_can_capnp(can_sends, msgtype='sendcan', valid=CS.canValid)) CC.actuatorsOutput = self.last_actuators + self.steer_limited = abs(CC.actuators.steer - CC.actuatorsOutput.steer) > 1e-2 force_decel = (self.sm['driverMonitoringState'].awarenessStatus < 0.) or \ (self.state == State.softDisabling) diff --git a/selfdrive/controls/lib/latcontrol.py b/selfdrive/controls/lib/latcontrol.py index 785c8faa8cb378..78b59fda591f29 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/selfdrive/controls/lib/latcontrol.py @@ -16,14 +16,14 @@ def __init__(self, CP, CI): self.steer_max = 1.0 @abstractmethod - def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): + def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): pass def reset(self): self.sat_count = 0. - def _check_saturation(self, saturated, CS): - if saturated and CS.vEgo > 10. and not CS.steeringRateLimited and not CS.steeringPressed: + def _check_saturation(self, saturated, CS, steer_limited): + if saturated and CS.vEgo > 10. and not steer_limited and not CS.steeringPressed: self.sat_count += self.sat_count_rate else: self.sat_count -= self.sat_count_rate diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py index 2eee71c703b2b5..0e5be4a97705ae 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/selfdrive/controls/lib/latcontrol_angle.py @@ -7,7 +7,7 @@ class LatControlAngle(LatControl): - def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): + def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): angle_log = log.ControlsState.LateralAngleState.new_message() if CS.vEgo < MIN_STEER_SPEED or not active: @@ -19,7 +19,7 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi angle_steers_des += params.angleOffsetDeg angle_control_saturated = abs(angle_steers_des - CS.steeringAngleDeg) > STEER_ANGLE_SATURATION_THRESHOLD - angle_log.saturated = self._check_saturation(angle_control_saturated, CS) + angle_log.saturated = self._check_saturation(angle_control_saturated, CS, steer_limited) angle_log.steeringAngleDeg = float(CS.steeringAngleDeg) angle_log.steeringAngleDesiredDeg = angle_steers_des return 0, float(angle_steers_des), angle_log diff --git a/selfdrive/controls/lib/latcontrol_indi.py b/selfdrive/controls/lib/latcontrol_indi.py index 79c881d11be37e..2bc3cef76bf7b2 100644 --- a/selfdrive/controls/lib/latcontrol_indi.py +++ b/selfdrive/controls/lib/latcontrol_indi.py @@ -63,7 +63,7 @@ def reset(self): self.steer_filter.x = 0. self.speed = 0. - def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): + def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): self.speed = CS.vEgo # Update Kalman filter y = np.array([[math.radians(CS.steeringAngleDeg)], [math.radians(CS.steeringRateDeg)]]) @@ -115,6 +115,6 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi indi_log.delayedOutput = float(self.steer_filter.x) indi_log.delta = float(delta_u) indi_log.output = float(output_steer) - indi_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS) + indi_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited) return float(output_steer), float(steers_des), indi_log diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index 5dd1b20ae074f7..6bd678073e70aa 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -17,7 +17,7 @@ def reset(self): super().reset() self.pid.reset() - def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): + def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): pid_log = log.ControlsState.LateralPIDState.new_message() pid_log.steeringAngleDeg = float(CS.steeringAngleDeg) pid_log.steeringRateDeg = float(CS.steeringRateDeg) @@ -43,6 +43,6 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi pid_log.i = self.pid.i pid_log.f = self.pid.f pid_log.output = output_steer - pid_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS) + pid_log.saturated = self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited) return output_steer, angle_steers_des, pid_log diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 46caa41a90fc86..c4604d90e1614d 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -33,7 +33,7 @@ def __init__(self, CP, CI): self.kf = CP.lateralTuning.torque.kf self.steering_angle_deadzone_deg = CP.lateralTuning.torque.steeringAngleDeadzoneDeg - def update(self, active, CS, VM, params, last_actuators, desired_curvature, desired_curvature_rate, llk): + def update(self, active, CS, VM, params, last_actuators, steer_limited, desired_curvature, desired_curvature_rate, llk): pid_log = log.ControlsState.LateralTorqueState.new_message() if CS.vEgo < MIN_STEER_SPEED or not active: @@ -66,7 +66,7 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi # convert friction into lateral accel units for feedforward friction_compensation = interp(apply_deadzone(error, lateral_accel_deadzone), [-FRICTION_THRESHOLD, FRICTION_THRESHOLD], [-self.friction, self.friction]) ff += friction_compensation / self.kf - freeze_integrator = CS.steeringRateLimited or CS.steeringPressed or CS.vEgo < 5 + freeze_integrator = steer_limited or CS.steeringPressed or CS.vEgo < 5 output_torque = self.pid.update(error, feedforward=ff, speed=CS.vEgo, @@ -78,7 +78,7 @@ def update(self, active, CS, VM, params, last_actuators, desired_curvature, desi pid_log.d = self.pid.d pid_log.f = self.pid.f pid_log.output = -output_torque - pid_log.saturated = self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS) + pid_log.saturated = self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited) pid_log.actualLateralAccel = actual_lateral_accel pid_log.desiredLateralAccel = desired_lateral_accel diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/lib/tests/test_latcontrol.py index 503eaaa6a465a2..f15ab2fa5662bf 100755 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/lib/tests/test_latcontrol.py @@ -26,7 +26,6 @@ def test_saturation(self, car_name, controller): controller = controller(CP, CI) - CS = car.CarState.new_message() CS.vEgo = 30 @@ -35,7 +34,7 @@ def test_saturation(self, car_name, controller): params = log.LiveParametersData.new_message() for _ in range(1000): - _, _, lac_log = controller.update(True, CS, CP, VM, params, last_actuators, 1, 0) + _, _, lac_log = controller.update(True, CS, CP, VM, params, last_actuators, True, 1, 0) self.assertTrue(lac_log.saturated) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index b144852714c843..ce4f5c1711df2b 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2bfa96aad8b951988cf9d0dd95d3c9d9019a1c0e \ No newline at end of file +8f918a54cfefcaada91a7eca0c14ca4d4b6f11c8 \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 08933d143ffa18..9e6fee0de0c26f 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -5,7 +5,7 @@ import sys from collections import defaultdict from tqdm import tqdm -from typing import Any, Dict +from typing import Any, DefaultDict, Dict from selfdrive.car.car_helpers import interface_names from selfdrive.test.openpilotci import get_url, upload_file @@ -105,7 +105,7 @@ def test_process(cfg, lr, ref_log_path, new_log_path, ignore_fields=None, ignore return str(e), log_msgs -def format_diff(results, ref_commit): +def format_diff(results, log_paths, ref_commit): diff1, diff2 = "", "" diff2 += f"***** tested against commit {ref_commit} *****\n" @@ -115,13 +115,22 @@ def format_diff(results, ref_commit): diff2 += f"***** differences for segment {segment} *****\n" for proc, diff in list(result.items()): - diff1 += f"\t{proc}\n" + # long diff diff2 += f"*** process: {proc} ***\n" + diff2 += f"\tref: {log_paths[segment]['ref']}\n\n" + diff2 += f"\tnew: {log_paths[segment]['new']}\n\n" + # short diff + diff1 += f" {proc}\n" if isinstance(diff, str): - diff1 += f"\t\t{diff}\n" + diff1 += f" ref: {log_paths[segment]['ref']}\n" + diff1 += f" new: {log_paths[segment]['new']}\n\n" + diff1 += f" {diff}\n" failed = True elif len(diff): + diff1 += f" ref: {log_paths[segment]['ref']}\n" + diff1 += f" new: {log_paths[segment]['new']}\n\n" + cnt: Dict[str, int] = {} for d in diff: diff2 += f"\t{str(d)}\n" @@ -130,7 +139,7 @@ def format_diff(results, ref_commit): cnt[k] = 1 if k not in cnt else cnt[k] + 1 for k, v in sorted(cnt.items()): - diff1 += f"\t\t{k}: {v}\n" + diff1 += f" {k}: {v}\n" failed = True return diff1, diff2, failed @@ -187,6 +196,8 @@ def format_diff(results, ref_commit): untested = (set(interface_names) - set(excluded_interfaces)) - {c.lower() for c in tested_cars} assert len(untested) == 0, f"Cars missing routes: {str(untested)}" + + log_paths: DefaultDict[str, Dict[str, str]] = defaultdict(dict) with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as pool: if not args.upload_only: download_segments = [seg for car, seg in segments if car in tested_cars] @@ -214,13 +225,16 @@ def format_diff(results, ref_commit): dat = None if args.upload_only else log_data[segment] pool_args.append((segment, cfg, args, cur_log_fn, ref_log_path, dat)) + log_paths[segment]['ref'] = ref_log_path + log_paths[segment]['new'] = cur_log_fn + results: Any = defaultdict(dict) p2 = pool.map(run_test_process, pool_args) for (segment, proc, subtest_name, result) in tqdm(p2, desc="Running Tests", total=len(pool_args)): if not args.upload_only: results[segment][proc + subtest_name] = result - diff1, diff2, failed = format_diff(results, ref_commit) + diff1, diff2, failed = format_diff(results, log_paths, ref_commit) if not upload: with open(os.path.join(PROC_REPLAY_DIR, "diff.txt"), "w") as f: f.write(diff2) diff --git a/tools/tuning/measure_steering_accuracy.py b/tools/tuning/measure_steering_accuracy.py index 5084247cb0c54b..7abfb6358ae357 100755 --- a/tools/tuning/measure_steering_accuracy.py +++ b/tools/tuning/measure_steering_accuracy.py @@ -71,7 +71,7 @@ def sigint_handler(signal, frame): active = sm['controlsState'].active steer = sm['carControl'].actuatorsOutput.steer standstill = sm['carState'].standstill - steer_limited = sm['carState'].steeringRateLimited + steer_limited = abs(sm['carControl'].actuators.steer - sm['carControl'].actuatorsOutput.steer) > 1e-2 overriding = sm['carState'].steeringPressed changing_lanes = sm['lateralPlan'].laneChangeState != 0 d_path_points = sm['lateralPlan'].dPathPoints From ef00bf76c1db2ac85f8033e48e89213f1f846d44 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 00:01:16 -0700 Subject: [PATCH 418/436] model_replay: update format_diff call --- selfdrive/test/process_replay/model_replay.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/model_replay.py b/selfdrive/test/process_replay/model_replay.py index bccad5d92cbc88..06720d4a1a3935 100755 --- a/selfdrive/test/process_replay/model_replay.py +++ b/selfdrive/test/process_replay/model_replay.py @@ -176,8 +176,9 @@ def model_replay(lr, frs): # TODO this tolerence is absurdly large tolerance = 5e-1 if PC else None results: Any = {TEST_ROUTE: {}} + log_paths: Any = {TEST_ROUTE: {'ref': BASE_URL + log_fn, 'new': log_fn}} results[TEST_ROUTE]["models"] = compare_logs(cmp_log, log_msgs, tolerance=tolerance, ignore_fields=ignore) - diff1, diff2, failed = format_diff(results, ref_commit) + diff1, diff2, failed = format_diff(results, log_paths, ref_commit) print(diff2) print('-------------\n'*5) From f72c988415a2a1c4299df67cb8e02eee20e5ba77 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 25 Jul 2022 16:20:52 +0200 Subject: [PATCH 419/436] map_renderer.py, output monochrome images (#25272) * map renderer, output monochrome images * pass token/url as argument * greyscale --- selfdrive/navd/map_renderer.cc | 17 +++++++++++------ selfdrive/navd/map_renderer.py | 12 ++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/selfdrive/navd/map_renderer.cc b/selfdrive/navd/map_renderer.cc index cb24f2087e4358..d0770cfb48ef69 100644 --- a/selfdrive/navd/map_renderer.cc +++ b/selfdrive/navd/map_renderer.cc @@ -153,10 +153,15 @@ void MapRenderer::sendVipc() { uint8_t* MapRenderer::getImage() { QImage cap = fbo->toImage().convertToFormat(QImage::Format_RGB888, Qt::AutoColor); - uint8_t* buf = new uint8_t[cap.sizeInBytes()]; - memcpy(buf, cap.bits(), cap.sizeInBytes()); - return buf; + uint8_t* src = cap.bits(); + uint8_t* dst = new uint8_t[WIDTH * HEIGHT]; + + for (int i = 0; i < WIDTH * HEIGHT; i++) { + dst[i] = src[i * 3]; + } + + return dst; } void MapRenderer::updateRoute(QList coordinates) { @@ -189,7 +194,7 @@ MapRenderer::~MapRenderer() { } extern "C" { - MapRenderer* map_renderer_init() { + MapRenderer* map_renderer_init(char *maps_host = nullptr, char *token = nullptr) { char *argv[] = { (char*)"navd", nullptr @@ -199,8 +204,8 @@ extern "C" { assert(app); QMapboxGLSettings settings; - settings.setApiBaseUrl(MAPS_HOST); - settings.setAccessToken(get_mapbox_token()); + settings.setApiBaseUrl(maps_host == nullptr ? MAPS_HOST : maps_host); + settings.setAccessToken(token == nullptr ? get_mapbox_token() : token); return new MapRenderer(settings, false); } diff --git a/selfdrive/navd/map_renderer.py b/selfdrive/navd/map_renderer.py index dc39f335c7945c..90006229282ea0 100755 --- a/selfdrive/navd/map_renderer.py +++ b/selfdrive/navd/map_renderer.py @@ -3,6 +3,7 @@ import os import time +import numpy as np from cffi import FFI from common.ffi_wrapper import suffix @@ -16,7 +17,7 @@ def get_ffi(): ffi = FFI() ffi.cdef(""" -void* map_renderer_init(); +void* map_renderer_init(char *maps_host, char *token); void map_renderer_update_position(void *inst, float lat, float lon, float bearing); void map_renderer_update_route(void *inst, char *polyline); void map_renderer_update(void *inst); @@ -40,20 +41,19 @@ def wait_ready(lib, renderer): def get_image(lib, renderer): buf = lib.map_renderer_get_image(renderer) - r = list(buf[0:3 * WIDTH * HEIGHT]) + r = list(buf[0:WIDTH * HEIGHT]) lib.map_renderer_free_image(renderer, buf) # Convert to numpy r = np.asarray(r) - return r.reshape((WIDTH, HEIGHT, 3)) + return r.reshape((WIDTH, HEIGHT)) if __name__ == "__main__": import matplotlib.pyplot as plt - import numpy as np ffi, lib = get_ffi() - renderer = lib.map_renderer_init() + renderer = lib.map_renderer_init(ffi.NULL, ffi.NULL) wait_ready(lib, renderer) geometry = r"{yxk}@|obn~Eg@@eCFqc@J{RFw@?kA@gA?q|@Riu@NuJBgi@ZqVNcRBaPBkG@iSD{I@_H@cH?gG@mG@gG?aD@{LDgDDkVVyQLiGDgX@q_@@qI@qKhS{R~[}NtYaDbGoIvLwNfP_b@|f@oFnF_JxHel@bf@{JlIuxAlpAkNnLmZrWqFhFoh@jd@kX|TkJxH_RnPy^|[uKtHoZ~Um`DlkCorC``CuShQogCtwB_ThQcr@fk@sVrWgRhVmSb\\oj@jxA{Qvg@u]tbAyHzSos@xjBeKbWszAbgEc~@~jCuTrl@cYfo@mRn\\_m@v}@ij@jp@om@lk@y|A`pAiXbVmWzUod@xj@wNlTw}@|uAwSn\\kRfYqOdS_IdJuK`KmKvJoOhLuLbHaMzGwO~GoOzFiSrEsOhD}PhCqw@vJmnAxSczA`Vyb@bHk[fFgl@pJeoDdl@}}@zIyr@hG}X`BmUdBcM^aRR}Oe@iZc@mR_@{FScHxAn_@vz@zCzH~GjPxAhDlB~DhEdJlIbMhFfG|F~GlHrGjNjItLnGvQ~EhLnBfOn@p`@AzAAvn@CfC?fc@`@lUrArStCfSxEtSzGxM|ElFlBrOzJlEbDnC~BfDtCnHjHlLvMdTnZzHpObOf^pKla@~G|a@dErg@rCbj@zArYlj@ttJ~AfZh@r]LzYg@`TkDbj@gIdv@oE|i@kKzhA{CdNsEfOiGlPsEvMiDpLgBpHyB`MkB|MmArPg@|N?|P^rUvFz~AWpOCdAkB|PuB`KeFfHkCfGy@tAqC~AsBPkDs@uAiAcJwMe@s@eKkPMoXQux@EuuCoH?eI?Kas@}Dy@wAUkMOgDL" @@ -73,6 +73,6 @@ def get_image(lib, renderer): print(f"{pos} took {time.time() - t:.2f} s") plt.subplot(2, 2, i + 1) - plt.imshow(get_image(lib, renderer)) + plt.imshow(get_image(lib, renderer), cmap='gray') plt.show() From 4d8f40aa2d60400c808fa276ef40f8f3d6c10fc5 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 25 Jul 2022 17:00:42 +0200 Subject: [PATCH 420/436] docs: add svg images (#25273) * docs: add svg images * important whitespace --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 02db7db2d3e809..d0aa841c4ddcc4 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -32,7 +32,7 @@ clean: @echo "Copying docs & config to build folder..." cp -a "$(DOCSDIR)" "$(BUILDDIR)" cd "$(OPENPILOT_ROOT)" && \ - find . -type f \( -name "*.md" -o -name "*.rst" -o -name "*.png" -o -name "*.jpg" \) \ + find . -type f \( -name "*.md" -o -name "*.rst" -o -name "*.png" -o -name "*.jpg" -o -name "*.svg" \) \ -not -path "*/.*" \ -not -path "./build/*" \ -not -path "./docs/*" \ From dc1590a9d0a23217ecddf4a6e4d704977d25b181 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Jul 2022 10:44:26 -0700 Subject: [PATCH 421/436] Ram 1500: remove harness (#25275) Update values.py --- selfdrive/car/chrysler/values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 56b11652ff3441..48ea6bd14a3138 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -56,7 +56,7 @@ class ChryslerCarInfo(CarInfo): ], CAR.JEEP_CHEROKEE: ChryslerCarInfo("Jeep Grand Cherokee 2016-18", video_link="https://www.youtube.com/watch?v=eLR9o2JkuRk"), CAR.JEEP_CHEROKEE_2019: ChryslerCarInfo("Jeep Grand Cherokee 2019-21", video_link="https://www.youtube.com/watch?v=jBe4lWnRSu4"), - CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-22"), + CAR.RAM_1500: ChryslerCarInfo("Ram 1500 2019-22", harness=Harness.none), } # Unique CAN messages: From cff4cdc44b73921b5ef4a6df5c9eeb440d8a7f36 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Jul 2022 12:55:21 -0700 Subject: [PATCH 422/436] 2022 Palisade: add missing FW versions (#25258) * We support the 2022 Palisade * Add fingerprints to exact match --- docs/CARS.md | 2 +- selfdrive/car/hyundai/values.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 35cd0353116cea..4d83d2c2eeb88e 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -78,7 +78,7 @@ A supported vehicle is one that just works when you install a comma device. Ever |Hyundai|Kona 2020|SCC + LKAS||||| |Hyundai|Kona Electric 2018-21|SCC + LKAS||||| |Hyundai|Kona Hybrid 2020|SCC + LKAS||||| -|Hyundai|Palisade 2020-21|All||||| +|Hyundai|Palisade 2020-22|All||||| |Hyundai|Santa Fe 2019-20|All||||| |Hyundai|Santa Fe 2021-22|All||||| |Hyundai|Santa Fe Hybrid 2022|All||||| diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 791d84305f7472..45985d2a451597 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -119,7 +119,7 @@ class HyundaiCarInfo(CarInfo): HyundaiCarInfo("Hyundai Tucson Diesel 2019", harness=Harness.hyundai_l), ], CAR.PALISADE: [ - HyundaiCarInfo("Hyundai Palisade 2020-21", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), + HyundaiCarInfo("Hyundai Palisade 2020-22", "All", video_link="https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), HyundaiCarInfo("Kia Telluride 2020", harness=Harness.hyundai_h), ], CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e), @@ -712,7 +712,7 @@ class Buttons: }, CAR.PALISADE: { (Ecu.fwdRadar, 0x7d0, None): [ - b'\xf1\000LX2_ SCC F-CUP 1.00 1.05 99110-S8100 ', + b'\xf1\x00LX2_ SCC F-CUP 1.00 1.05 99110-S8100 ', b'\xf1\x00LX2 SCC FHCUP 1.00 1.04 99110-S8100 ', b'\xf1\x00LX2_ SCC FHCU- 1.00 1.05 99110-S8100 ', b'\xf1\x00LX2_ SCC FHCUP 1.00 1.00 99110-S8110 ', @@ -722,7 +722,7 @@ class Buttons: ], (Ecu.esp, 0x7d1, None): [ b'\xf1\x00LX ESC \x01 103\x19\t\x10 58910-S8360', - b'\xf1\x00LX ESC \x01 103\x31\t\020 58910-S8360', + b'\xf1\x00LX ESC \x01 1031\t\x10 58910-S8360', b'\xf1\x00LX ESC \x0b 101\x19\x03\x17 58910-S8330', b'\xf1\x00LX ESC \x0b 102\x19\x05\x07 58910-S8330', b'\xf1\x00LX ESC \x0b 103\x19\t\x07 58910-S8330', @@ -737,7 +737,7 @@ class Buttons: b'\xf1\x81640S1051\x00\x00\x00\x00\x00\x00\x00\x00', ], (Ecu.eps, 0x7d4, None): [ - b'\xf1\x00LX2 MDPS C 1,00 1,03 56310-S8020 4LXDC103', # modified firmware + b'\xf1\x00LX2 MDPS C 1,00 1,03 56310-S8020 4LXDC103', b'\xf1\x00LX2 MDPS C 1.00 1.03 56310-S8020 4LXDC103', b'\xf1\x00LX2 MDPS C 1.00 1.04 56310-S8020 4LXDC104', b'\xf1\x00ON MDPS C 1.00 1.00 56340-S9000 8B13', @@ -750,6 +750,7 @@ class Buttons: b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.08 99211-S8100 200903', b'\xf1\x00ON MFC AT USA LHD 1.00 1.01 99211-S9100 181105', b'\xf1\x00ON MFC AT USA LHD 1.00 1.03 99211-S9100 200720', + b'\xf1\x00LX2 MFC AT USA LHD 1.00 1.00 99211-S8110 210226', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x00bcsh8p54 U872\x00\x00\x00\x00\x00\x00TON4G38NB1\x96z28', @@ -757,7 +758,7 @@ class Buttons: b'\xf1\x87LBLUFN591307KF25vgvw\x97wwwy\x99\xa7\x99\x99\xaa\xa9\x9af\x88\x96h\x95o\xf7\xff\x99f/\xff\xe4c\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB2\xd7\xc1/\xd1', b'\xf1\x87LBLUFN650868KF36\xa9\x98\x89\x88\xa8\x88\x88\x88h\x99\xa6\x89fw\x86gw\x88\x97x\xaa\x7f\xf6\xff\xbb\xbb\x8f\xff+\x82\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB3\xd1\xc3\xf8\xa8', b'\xf1\x87LBLUFN655162KF36\x98\x88\x88\x88\x98\x88\x88\x88x\x99\xa7\x89x\x99\xa7\x89x\x99\x97\x89g\x7f\xf7\xffwU_\xff\xe9!\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB3\xd1\xc3\xf8\xa8', - b'\xf1\x87LBLUFN731381KF36\xb9\x99\x89\x98\x98\x88\x88\x88\x89\x99\xa8\x99\x88\x99\xa8\x89\x88\x88\x98\x88V\177\xf6\xff\x99w\x8f\xff\xad\xd8\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\000bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB3\xd1\xc3\xf8\xa8', + b'\xf1\x87LBLUFN731381KF36\xb9\x99\x89\x98\x98\x88\x88\x88\x89\x99\xa8\x99\x88\x99\xa8\x89\x88\x88\x98\x88V\x7f\xf6\xff\x99w\x8f\xff\xad\xd8\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB3\xd1\xc3\xf8\xa8', b'\xf1\x87LDKVAA0028604HH1\xa8\x88x\x87vgvw\x88\x99\xa8\x89gw\x86ww\x88\x97x\x97o\xf9\xff\x97w\x7f\xffo\x02\xf1\x81U872\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U872\x00\x00\x00\x00\x00\x00TON4G38NB1\x96z28', b'\xf1\x87LDKVAA3068374HH1wwww\x87xw\x87y\x99\xa7\x99w\x88\x87xw\x88\x97x\x85\xaf\xfa\xffvU/\xffU\xdc\xf1\x81U872\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U872\x00\x00\x00\x00\x00\x00TON4G38NB1\x96z28', b'\xf1\x87LDKVBN382172KF26\x98\x88\x88\x88\xa8\x88\x88\x88x\x99\xa7\x89\x87\x88\x98x\x98\x99\xa9\x89\xa5_\xf6\xffDDO\xff\xcd\x16\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX4G38NB2\xafL]\xe7', @@ -798,6 +799,7 @@ class Buttons: b'\xf1\x87LDMVBN895969KF37vefV\x87vgfx\x99\xa7\x89\x99\x99\xb9\x99f\x88\x96he_\xf7\xffxwo\xff\x14\xf9\xf1\x81U922\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U922\x00\x00\x00\x00\x00\x00SLX4G38NB5\xb9\x94\xe8\x89', b'\xf1\x87LDMVBN899222KF37\xa8\x88x\x87\x97www\x98\x99\x99\x89\x88\x99\x98\x89f\x88\x96hdo\xf7\xff\xbb\xaa\x9f\xff\xe2U\xf1\x81U922\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U922\x00\x00\x00\x00\x00\x00SLX4G38NB5\xb9\x94\xe8\x89', b"\xf1\x87LBLUFN622950KF36\xa8\x88\x88\x88\x87w\x87xh\x99\x96\x89\x88\x99\x98\x89\x88\x99\x98\x89\x87o\xf6\xff\x98\x88o\xffx'\xf1\x81U891\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U891\x00\x00\x00\x00\x00\x00SLX2G38NB3\xd1\xc3\xf8\xa8", + b'\xf1\x87LDMVBN950669KF37\x97www\x96fffy\x99\xa7\x99\xa9\x99\xaa\x99g\x88\x96x\xb8\x8f\xf9\xffTD/\xff\xa7\xcb\xf1\x81U922\x00\x00\x00\x00\x00\x00\xf1\x00bcsh8p54 U922\x00\x00\x00\x00\x00\x00SLX4G38NB5\xb9\x94\xe8\x89', ], }, CAR.VELOSTER: { From d6105f2d1f2315e208a9e7bd42acd90a2e48ce12 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Mon, 25 Jul 2022 22:00:38 +0200 Subject: [PATCH 423/436] cars.md: use markdown images for stars (#25274) * cars.md: use markdown images for stars * See if this works * smaller svg Co-authored-by: Shane Smiskol --- docs/CARS.md | 404 ++++++++++++++++---------------- docs/assets/icon-star-empty.svg | 57 ++++- docs/assets/icon-star-full.svg | 57 ++++- docs/assets/icon-star-half.svg | 69 +++++- selfdrive/car/CARS_template.md | 2 +- 5 files changed, 378 insertions(+), 211 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 4d83d2c2eeb88e..3ee05960e0b253 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -7,218 +7,218 @@ A supported vehicle is one that just works when you install a comma device. Ever ## How We Rate The Cars ### Stop and Go -- - openpilot operates down to 0 mph. -- - openpilot operates only above a minimum speed. See your car's manual for the minimum speed. +- [![star](assets/icon-star-full.svg)](##) - openpilot operates down to 0 mph. +- [![star](assets/icon-star-empty.svg)](##) - openpilot operates only above a minimum speed. See your car's manual for the minimum speed. ### Steer to 0 -- - openpilot can control the steering wheel down to 0 mph. -- - No steering control below certain speeds. See your car's manual for the minimum speed. +- [![star](assets/icon-star-full.svg)](##) - openpilot can control the steering wheel down to 0 mph. +- [![star](assets/icon-star-empty.svg)](##) - No steering control below certain speeds. See your car's manual for the minimum speed. ### Steering Torque -- - Car has enough steering torque to comfortably take most highway turns. -- - Limited ability to make tighter turns. +- [![star](assets/icon-star-full.svg)](##) - Car has enough steering torque to comfortably take most highway turns. +- [![star](assets/icon-star-empty.svg)](##) - Limited ability to make tighter turns. # 196 Supported Cars |Make|Model|Supported Package|openpilot ACC|Stop and Go|Steer to 0|Steering Torque| |---|---|---|:---:|:---:|:---:|:---:| -|Acura|ILX 2016-19|AcuraWatch Plus||||| -|Acura|RDX 2016-18|AcuraWatch Plus||||| -|Acura|RDX 2019-22|All||||| -|Audi|A3 2014-19|ACC + Lane Assist||||| -|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist||||| -|Audi|Q2 2018|ACC + Lane Assist||||| -|Audi|Q3 2020-21|ACC + Lane Assist||||| -|Audi|RS3 2018|ACC + Lane Assist||||| -|Audi|S3 2015-17|ACC + Lane Assist||||| -|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS||||| -|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise||||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise||||| -|Chrysler|Pacifica 2019-20|Adaptive Cruise||||| -|Chrysler|Pacifica 2021|All||||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise||||| -|Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise||||| -|comma|body|All||||| -|Genesis|G70 2018-19|All||||| -|Genesis|G70 2020|All||||| -|Genesis|G80 2017-19|All||||| -|Genesis|G90 2017-18|All||||| -|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise||||| -|Honda|Accord 2018-22|All||||| -|Honda|Accord Hybrid 2018-22|All||||| -|Honda|Civic 2016-18|Honda Sensing||||| -|Honda|Civic 2019-21|All|||[2](#footnotes)|| -|Honda|Civic 2022|All||||| -|Honda|Civic Hatchback 2017-21|Honda Sensing||||| -|Honda|Civic Hatchback 2022|All||||| -|Honda|CR-V 2015-16|Touring||||| -|Honda|CR-V 2017-22|Honda Sensing||||| -|Honda|CR-V Hybrid 2017-19|Honda Sensing||||| -|Honda|e 2020|All||||| -|Honda|Fit 2018-20|Honda Sensing||||| -|Honda|Freed 2020|Honda Sensing||||| -|Honda|HR-V 2019-22|Honda Sensing||||| -|Honda|Insight 2019-22|All||||| -|Honda|Inspire 2018|All||||| -|Honda|Odyssey 2018-22|Honda Sensing||||| -|Honda|Passport 2019-21|All||||| -|Honda|Pilot 2016-22|Honda Sensing||||| -|Honda|Ridgeline 2017-22|Honda Sensing||||| -|Hyundai|Elantra 2017-19|SCC + LKAS||||| -|Hyundai|Elantra 2021-22|SCC + LKAS||||| -|Hyundai|Elantra Hybrid 2021-22|SCC + LKAS||||| -|Hyundai|Genesis 2015-16|SCC + LKAS||||| -|Hyundai|Ioniq Electric 2019|SCC + LKAS||||| -|Hyundai|Ioniq Electric 2020|SCC + LKAS||||| -|Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS||||| -|Hyundai|Ioniq Hybrid 2020-22|SCC + LFA||||| -|Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS||||| -|Hyundai|Ioniq Plug-in Hybrid 2020-21|SCC + LKAS||||| -|Hyundai|Kona 2020|SCC + LKAS||||| -|Hyundai|Kona Electric 2018-21|SCC + LKAS||||| -|Hyundai|Kona Hybrid 2020|SCC + LKAS||||| -|Hyundai|Palisade 2020-22|All||||| -|Hyundai|Santa Fe 2019-20|All||||| -|Hyundai|Santa Fe 2021-22|All||||| -|Hyundai|Santa Fe Hybrid 2022|All||||| -|Hyundai|Santa Fe Plug-in Hybrid 2022|All||||| -|Hyundai|Sonata 2018-19|SCC + LKAS||||| -|Hyundai|Sonata 2020-22|All||||| -|Hyundai|Sonata Hybrid 2020-22|All||||| -|Hyundai|Tucson 2021|SCC + LKAS||||| -|Hyundai|Tucson Diesel 2019|SCC + LKAS||||| -|Hyundai|Veloster 2019-20|SCC + LKAS||||| -|Jeep|Grand Cherokee 2016-18|Adaptive Cruise||||| -|Jeep|Grand Cherokee 2019-21|Adaptive Cruise||||| -|Kia|Ceed 2019|SCC + LKAS||||| -|Kia|EV6 2022|All||||| -|Kia|Forte 2018|SCC + LKAS||||| -|Kia|Forte 2019-21|SCC + LKAS||||| -|Kia|K5 2021-22|SCC||||| -|Kia|Niro Electric 2019-20|All||||| -|Kia|Niro Electric 2021|All||||| -|Kia|Niro Electric 2022|All||||| -|Kia|Niro Hybrid 2021|SCC + LKAS||||| -|Kia|Niro Hybrid 2022|SCC + LKAS||||| -|Kia|Niro Plug-in Hybrid 2018-19|SCC + LKAS||||| -|Kia|Optima 2017|SCC + LKAS||||| -|Kia|Optima 2019|SCC + LKAS||||| -|Kia|Seltos 2021|SCC + LKAS||||| -|Kia|Sorento 2018|SCC + LKAS||||| -|Kia|Sorento 2019|SCC + LKAS||||| -|Kia|Stinger 2018-20|SCC + LKAS||||| -|Kia|Telluride 2020|SCC + LKAS||||| -|Lexus|CT Hybrid 2017-18|LSS|[3](#footnotes)|||| -|Lexus|ES 2019-22|All||||| -|Lexus|ES Hybrid 2017-18|LSS|[3](#footnotes)|||| -|Lexus|ES Hybrid 2019-22|All||||| -|Lexus|IS 2017-19|All||||| -|Lexus|NX 2018-19|All|[3](#footnotes)|||| -|Lexus|NX 2020-21|All||||| -|Lexus|NX Hybrid 2018-19|All|[3](#footnotes)|||| -|Lexus|NX Hybrid 2020-21|All||||| -|Lexus|RC 2017-20|All||||| -|Lexus|RX 2016-18|All|[3](#footnotes)|||| -|Lexus|RX 2020-22|All||||| -|Lexus|RX Hybrid 2016-19|All|[3](#footnotes)|||| -|Lexus|RX Hybrid 2020-21|All||||| -|Lexus|UX Hybrid 2019-22|All||||| -|Mazda|CX-5 2022|All||||| -|Mazda|CX-9 2021-22|All||||| -|Nissan|Altima 2019-20|ProPILOT||||| -|Nissan|Leaf 2018-22|ProPILOT||||| -|Nissan|Rogue 2018-20|ProPILOT||||| -|Nissan|X-Trail 2017|ProPILOT||||| -|Ram|1500 2019-22|Adaptive Cruise||||| -|SEAT|Ateca 2018|Driver Assistance||||| -|SEAT|Leon 2014-20|Driver Assistance||||| -|Subaru|Ascent 2019-21|All||||| -|Subaru|Crosstrek 2018-19|EyeSight||||| -|Subaru|Crosstrek 2020-21|EyeSight||||| -|Subaru|Forester 2019-21|All||||| -|Subaru|Impreza 2017-19|EyeSight||||| -|Subaru|Impreza 2020-22|EyeSight||||| -|Subaru|XV 2018-19|EyeSight||||| -|Subaru|XV 2020-21|EyeSight||||| -|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance||||| -|Škoda|Karoq 2019|Driver Assistance||||| -|Škoda|Kodiaq 2018-19|Driver Assistance||||| -|Škoda|Octavia 2015, 2018-19|Driver Assistance||||| -|Škoda|Octavia RS 2016|Driver Assistance||||| -|Škoda|Scala 2020|Driver Assistance||||| -|Škoda|Superb 2015-18|Driver Assistance||||| -|Toyota|Alphard 2019-20|All||||| -|Toyota|Alphard Hybrid 2021|All||||| -|Toyota|Avalon 2016|TSS-P|[3](#footnotes)|||| -|Toyota|Avalon 2017-18|All|[3](#footnotes)|||| -|Toyota|Avalon 2019-21|All|[3](#footnotes)|||| -|Toyota|Avalon 2022|All||||| -|Toyota|Avalon Hybrid 2019-21|All|[3](#footnotes)|||| -|Toyota|Avalon Hybrid 2022|All||||| -|Toyota|C-HR 2017-21|All||||| -|Toyota|C-HR Hybrid 2017-19|All||||| -|Toyota|Camry 2018-20|All||[4](#footnotes)||| -|Toyota|Camry 2021-22|All||[4](#footnotes)||| -|Toyota|Camry Hybrid 2018-20|All||[4](#footnotes)||| -|Toyota|Camry Hybrid 2021-22|All||||| -|Toyota|Corolla 2017-19|All|[3](#footnotes)|||| -|Toyota|Corolla 2020-22|All||||| -|Toyota|Corolla Cross (Non-US only) 2020-21|All||||| -|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All||||| -|Toyota|Corolla Hatchback 2019-22|All||||| -|Toyota|Corolla Hybrid 2020-22|All||||| -|Toyota|Highlander 2017-19|All|[3](#footnotes)|||| -|Toyota|Highlander 2020-22|All||||| -|Toyota|Highlander Hybrid 2017-19|All|[3](#footnotes)|||| -|Toyota|Highlander Hybrid 2020-22|All||||| -|Toyota|Mirai 2021|All||||| -|Toyota|Prius 2016|TSS-P|[3](#footnotes)|||| -|Toyota|Prius 2017-20|All|[3](#footnotes)|||| -|Toyota|Prius 2021-22|All||||| -|Toyota|Prius Prime 2017-20|All|[3](#footnotes)|||| -|Toyota|Prius Prime 2021-22|All||||| -|Toyota|Prius v 2017|TSS-P|[3](#footnotes)|||| -|Toyota|RAV4 2016|TSS-P|[3](#footnotes)|||| -|Toyota|RAV4 2017-18|All|[3](#footnotes)|||| -|Toyota|RAV4 2019-21|All||||| -|Toyota|RAV4 2022|All||||| -|Toyota|RAV4 Hybrid 2016|TSS-P|[3](#footnotes)|||| -|Toyota|RAV4 Hybrid 2017-18|All|[3](#footnotes)|||| -|Toyota|RAV4 Hybrid 2019-21|All||||| -|Toyota|RAV4 Hybrid 2022|All||||| -|Toyota|Sienna 2018-20|All|[3](#footnotes)|||| -|Volkswagen|Arteon 2018-22[7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|Arteon eHybrid 2020-22[7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|Arteon R 2020-22[7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|California 2021[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance||||| -|Volkswagen|CC 2018-22[7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|e-Golf 2014-20|Driver Assistance||||| -|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance||||| -|Volkswagen|Golf Alltrack 2015-19|Driver Assistance||||| -|Volkswagen|Golf GTD 2015-20|Driver Assistance||||| -|Volkswagen|Golf GTE 2015-20|Driver Assistance||||| -|Volkswagen|Golf GTI 2015-21|Driver Assistance||||| -|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance||||| -|Volkswagen|Golf SportsVan 2015-20|Driver Assistance||||| -|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Passat 2015-22[6,7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Passat GTE 2015-22[7,8](#footnotes)|Driver Assistance||||| -|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance||||| -|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance||||| -|Volkswagen|Touran 2017|Driver Assistance||||| +|Acura|ILX 2016-19|AcuraWatch Plus|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Acura|RDX 2016-18|AcuraWatch Plus|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Acura|RDX 2019-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Audi|A3 2014-19|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Audi|A3 Sportback e-tron 2017-18|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Audi|Q2 2018|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Audi|Q3 2020-21|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Audi|RS3 2018|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Audi|S3 2015-17|ACC + Lane Assist|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Cadillac|Escalade ESV 2016[1](#footnotes)|ACC + LKAS|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chevrolet|Volt 2017-18[1](#footnotes)|Adaptive Cruise|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chrysler|Pacifica 2017-18|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chrysler|Pacifica 2019-20|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chrysler|Pacifica 2021|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Chrysler|Pacifica Hybrid 2019-22|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|comma|body|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Genesis|G70 2018-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Genesis|G70 2020|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Genesis|G80 2017-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Genesis|G90 2017-18|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|GMC|Acadia 2018[1](#footnotes)|Adaptive Cruise|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Honda|Accord 2018-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Accord Hybrid 2018-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Civic 2016-18|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Civic 2019-21|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)[2](#footnotes)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Civic 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Honda|Civic Hatchback 2017-21|Honda Sensing|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Civic Hatchback 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Honda|CR-V 2015-16|Touring|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|CR-V 2017-22|Honda Sensing|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|CR-V Hybrid 2017-19|Honda Sensing|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|e 2020|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Fit 2018-20|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Freed 2020|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|HR-V 2019-22|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Insight 2019-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Inspire 2018|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Odyssey 2018-22|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Passport 2019-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Pilot 2016-22|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Honda|Ridgeline 2017-22|Honda Sensing|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Hyundai|Elantra 2017-19|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Elantra 2021-22|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Elantra Hybrid 2021-22|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Genesis 2015-16|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Electric 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Electric 2020|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Hybrid 2017-19|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Hybrid 2020-22|SCC + LFA|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Plug-in Hybrid 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Ioniq Plug-in Hybrid 2020-21|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Kona 2020|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Kona Electric 2018-21|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Kona Hybrid 2020|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Palisade 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Santa Fe 2019-20|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Santa Fe 2021-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Santa Fe Hybrid 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Santa Fe Plug-in Hybrid 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Sonata 2018-19|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Sonata 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Sonata Hybrid 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Tucson 2021|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Tucson Diesel 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Hyundai|Veloster 2019-20|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Jeep|Grand Cherokee 2016-18|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Ceed 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|EV6 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Forte 2018|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Forte 2019-21|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|K5 2021-22|SCC|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Electric 2019-20|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Electric 2021|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Electric 2022|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Hybrid 2021|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Hybrid 2022|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Niro Plug-in Hybrid 2018-19|SCC + LKAS|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Optima 2017|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Optima 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Seltos 2021|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Sorento 2018|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Sorento 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Stinger 2018-20|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|Telluride 2020|SCC + LKAS|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|CT Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|ES 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|ES Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|ES Hybrid 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|IS 2017-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX 2018-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX Hybrid 2018-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX Hybrid 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RC 2017-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX 2016-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX Hybrid 2016-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX Hybrid 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|UX Hybrid 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Mazda|CX-5 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Mazda|CX-9 2021-22|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Nissan|Altima 2019-20|ProPILOT|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Nissan|Leaf 2018-22|ProPILOT|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Nissan|Rogue 2018-20|ProPILOT|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Nissan|X-Trail 2017|ProPILOT|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Ram|1500 2019-22|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|SEAT|Ateca 2018|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|SEAT|Leon 2014-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Subaru|Ascent 2019-21|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Subaru|Crosstrek 2018-19|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Subaru|Crosstrek 2020-21|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Subaru|Forester 2019-21|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Subaru|Impreza 2017-19|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Subaru|Impreza 2020-22|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Subaru|XV 2018-19|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| +|Subaru|XV 2020-21|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Karoq 2019|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Kodiaq 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Octavia 2015, 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Octavia RS 2016|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Scala 2020|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Superb 2015-18|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Alphard 2019-20|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Alphard Hybrid 2021|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2019-21|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2022|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon Hybrid 2019-21|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon Hybrid 2022|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|C-HR 2017-21|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|C-HR Hybrid 2017-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry Hybrid 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry Hybrid 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla Cross (Non-US only) 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla Hatchback 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla Hybrid 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander Hybrid 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander Hybrid 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Mirai 2021|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius 2017-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius Prime 2017-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius Prime 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius v 2017|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2019-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2019-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Sienna 2018-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon 2018-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon eHybrid 2020-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon R 2020-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|CC 2018-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|e-Golf 2014-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf Alltrack 2015-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf GTD 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf GTE 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf GTI 2015-21|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf SportsVan 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat 2015-22[6,7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat GTE 2015-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Touran 2017|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| 1Requires an OBD-II car harness and community built ASCM harness. NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).
    diff --git a/docs/assets/icon-star-empty.svg b/docs/assets/icon-star-empty.svg index b12b426622529e..5d3c32d6711aba 100644 --- a/docs/assets/icon-star-empty.svg +++ b/docs/assets/icon-star-empty.svg @@ -1,3 +1,56 @@ - - + + + + + + image/svg+xml + + + + + + + diff --git a/docs/assets/icon-star-full.svg b/docs/assets/icon-star-full.svg index 8211fdcc2b2f6b..294db2b7f27c01 100644 --- a/docs/assets/icon-star-full.svg +++ b/docs/assets/icon-star-full.svg @@ -1,3 +1,56 @@ - - + + + + + + image/svg+xml + + + + + + + diff --git a/docs/assets/icon-star-half.svg b/docs/assets/icon-star-half.svg index 7f64bc87e55dda..ab905fddcb4de2 100644 --- a/docs/assets/icon-star-half.svg +++ b/docs/assets/icon-star-half.svg @@ -1,5 +1,66 @@ - - - - + + + + + + image/svg+xml + + + + + + + + + diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md index db05c793775759..6a3a41f41ca7d4 100644 --- a/selfdrive/car/CARS_template.md +++ b/selfdrive/car/CARS_template.md @@ -1,5 +1,5 @@ {% set footnote_tag = '[{}](#footnotes)' -%} -{% set star_icon = '' -%} +{% set star_icon = '[![star](assets/icon-star-{}.svg)](##)' -%} From 2351ae48b932eb7942178954811695b1a98e66e1 Mon Sep 17 00:00:00 2001 From: Cameron Clough Date: Mon, 25 Jul 2022 21:02:27 +0100 Subject: [PATCH 424/436] Ford: fix gear parsing (#25267) * Ford: fix gear shifter state * Ford: manumatic is drive * add full gear names to CarStateBase * fix for unrecognised gears * Update interfaces.py Co-authored-by: Adeeb Shihadeh --- selfdrive/car/ford/carstate.py | 14 ++------------ selfdrive/car/ford/interface.py | 4 ++-- selfdrive/car/ford/values.py | 1 + selfdrive/car/interfaces.py | 21 +++++++++++++++------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index 67a72fd67c12bc..aaa3af612f05d8 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -1,5 +1,3 @@ -from typing import Dict - from cereal import car from common.conversions import Conversions as CV from opendbc.can.can_define import CANDefine @@ -51,8 +49,8 @@ def update(self, cp, cp_cam): # gear if self.CP.transmissionType == TransmissionType.automatic: - gear = int(cp.vl["Gear_Shift_by_Wire_FD1"]["TrnGear_D_RqDrv"]) - ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(gear, None)) + gear = self.shifter_values.get(cp.vl["Gear_Shift_by_Wire_FD1"]["TrnGear_D_RqDrv"], None) + ret.gearShifter = self.parse_gear_shifter(gear) elif self.CP.transmissionType == TransmissionType.manual: ret.clutchPressed = cp.vl["Engine_Clutch_Data"]["CluPdlPos_Pc_Meas"] > 0 if bool(cp.vl["BCM_Lamp_Stat_FD1"]["RvrseLghtOn_B_Stat"]): @@ -85,14 +83,6 @@ def update(self, cp, cp_cam): return ret - @staticmethod - def parse_gear_shifter(gear: str) -> car.CarState.GearShifter: - d: Dict[str, car.CarState.GearShifter] = { - 'Park': GearShifter.park, 'Reverse': GearShifter.reverse, 'Neutral': GearShifter.neutral, - 'Manual': GearShifter.manumatic, 'Drive': GearShifter.drive, - } - return d.get(gear, GearShifter.unknown) - @staticmethod def get_can_parser(CP): signals = [ diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index 857ccbab8bedc6..405579fa6001d8 100644 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -2,7 +2,7 @@ from cereal import car from common.conversions import Conversions as CV from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint -from selfdrive.car.ford.values import TransmissionType, CAR +from selfdrive.car.ford.values import CAR, TransmissionType, GearShifter from selfdrive.car.interfaces import CarInterfaceBase @@ -70,7 +70,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa def _update(self, c): ret = self.CS.update(self.cp, self.cp_cam) - events = self.create_common_events(ret) + events = self.create_common_events(ret, extra_gears=[GearShifter.manumatic]) ret.events = events.to_msg() return ret diff --git a/selfdrive/car/ford/values.py b/selfdrive/car/ford/values.py index aae011ad3dd9c8..6b8396cf072ae6 100644 --- a/selfdrive/car/ford/values.py +++ b/selfdrive/car/ford/values.py @@ -7,6 +7,7 @@ Ecu = car.CarParams.Ecu TransmissionType = car.CarParams.TransmissionType +GearShifter = car.CarState.GearShifter AngleRateLimit = namedtuple('AngleRateLimit', ['speed_points', 'max_angle_diff_points']) diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index a451ef2ae14373..38a613b0e7c652 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -2,7 +2,7 @@ import os import time from abc import abstractmethod, ABC -from typing import Any, Dict, Tuple, List +from typing import Any, Dict, Optional, Tuple, List from cereal import car from common.basedir import BASEDIR @@ -318,13 +318,22 @@ def update_blinker_from_stalk(self, blinker_time: int, left_blinker_stalk: bool, return bool(left_blinker_stalk or self.left_blinker_cnt > 0), bool(right_blinker_stalk or self.right_blinker_cnt > 0) @staticmethod - def parse_gear_shifter(gear: str) -> car.CarState.GearShifter: + def parse_gear_shifter(gear: Optional[str]) -> car.CarState.GearShifter: + if gear is None: + return GearShifter.unknown + d: Dict[str, car.CarState.GearShifter] = { - 'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, - 'E': GearShifter.eco, 'T': GearShifter.manumatic, 'D': GearShifter.drive, - 'S': GearShifter.sport, 'L': GearShifter.low, 'B': GearShifter.brake + 'P': GearShifter.park, 'PARK': GearShifter.park, + 'R': GearShifter.reverse, 'REVERSE': GearShifter.reverse, + 'N': GearShifter.neutral, 'NEUTRAL': GearShifter.neutral, + 'E': GearShifter.eco, 'ECO': GearShifter.eco, + 'T': GearShifter.manumatic, 'MANUAL': GearShifter.manumatic, + 'D': GearShifter.drive, 'DRIVE': GearShifter.drive, + 'S': GearShifter.sport, 'SPORT': GearShifter.sport, + 'L': GearShifter.low, 'LOW': GearShifter.low, + 'B': GearShifter.brake, 'BRAKE': GearShifter.brake, } - return d.get(gear, GearShifter.unknown) + return d.get(gear.upper(), GearShifter.unknown) @staticmethod def get_cam_can_parser(CP): From 6b74be53ae83d255e3513e336b8ed9efbd52a182 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 13:31:39 -0700 Subject: [PATCH 425/436] check vin validity (#25199) --- selfdrive/car/car_helpers.py | 4 ++-- selfdrive/car/vin.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 1a9a5f50f3bced..aa4b37d572e537 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -7,7 +7,7 @@ from system.version import is_comma_remote, is_tested_branch from selfdrive.car.interfaces import get_interface_attr from selfdrive.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars -from selfdrive.car.vin import get_vin, VIN_UNKNOWN +from selfdrive.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN from selfdrive.car.fw_versions import get_fw_versions_ordered, match_fw_to_car, get_present_ecus from system.swaglog import cloudlog import cereal.messaging as messaging @@ -106,7 +106,7 @@ def fingerprint(logcan, sendcan): vin, vin_rx_addr = VIN_UNKNOWN, 0 exact_fw_match, fw_candidates, car_fw = True, set(), [] - if len(vin) != 17: + if not is_valid_vin(vin): cloudlog.event("Malformed VIN", vin=vin, error=True) vin = VIN_UNKNOWN cloudlog.warning("VIN %s", vin) diff --git a/selfdrive/car/vin.py b/selfdrive/car/vin.py index 007c10e7728b01..5ace68649b9c31 100755 --- a/selfdrive/car/vin.py +++ b/selfdrive/car/vin.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import re import struct import traceback @@ -15,6 +16,11 @@ UDS_VIN_RESPONSE = bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + struct.pack("!H", uds.DATA_IDENTIFIER_TYPE.VIN) VIN_UNKNOWN = "0" * 17 +VIN_RE = "[A-HJ-NPR-Z0-9]{17}" + + +def is_valid_vin(vin: str): + return re.fullmatch(VIN_RE, vin) is not None def get_vin(logcan, sendcan, bus, timeout=0.1, retry=5, debug=False): From f4d9573ff05ce246f17306e7ffe736cfad06e4b9 Mon Sep 17 00:00:00 2001 From: Erich Moraga <33645296+ErichMoraga@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:53:26 -0500 Subject: [PATCH 426/436] Add several missing LEXUS_ES_TSS2 firmwares (#25268) `@RTDoc#2994` 2022 ES350 (ICE) DongleID/route 6ed275339186a130|2022-07-24--18-21-35 --- selfdrive/car/toyota/values.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 0f29ae1ee2d2ef..ca4172f24cbc50 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -1466,6 +1466,7 @@ class ToyotaCarInfo(CarInfo): }, CAR.LEXUS_ES_TSS2: { (Ecu.engine, 0x700, None): [ + b'\x018966306U6000\x00\x00\x00\x00', b'\x01896630EC9100\x00\x00\x00\x00', b'\x018966333T5000\x00\x00\x00\x00', b'\x018966333T5100\x00\x00\x00\x00', @@ -1482,18 +1483,21 @@ class ToyotaCarInfo(CarInfo): b'8965B33252\x00\x00\x00\x00\x00\x00', b'8965B33590\x00\x00\x00\x00\x00\x00', b'8965B33690\x00\x00\x00\x00\x00\x00', + b'8965B33721\x00\x00\x00\x00\x00\x00', b'8965B48271\x00\x00\x00\x00\x00\x00', ], (Ecu.fwdRadar, 0x750, 0xf): [ b'\x018821F3301100\x00\x00\x00\x00', b'\x018821F3301200\x00\x00\x00\x00', b'\x018821F3301400\x00\x00\x00\x00', + b'\x018821F6201300\x00\x00\x00\x00', ], (Ecu.fwdCamera, 0x750, 0x6d): [ b'\x028646F33030D0\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00', b'\x028646F3303200\x00\x00\x00\x008646G26011A0\x00\x00\x00\x00', b'\x028646F3304100\x00\x00\x00\x008646G2601200\x00\x00\x00\x00', b'\x028646F3304300\x00\x00\x00\x008646G2601500\x00\x00\x00\x00', + b'\x028646F3309100\x00\x00\x00\x008646G3304000\x00\x00\x00\x00', b'\x028646F4810200\x00\x00\x00\x008646G2601400\x00\x00\x00\x00', ], }, From 60eab24e0c0b4ab372ef5aa30191fe4c18d0ddf0 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 26 Jul 2022 06:12:38 +0900 Subject: [PATCH 427/436] Skoda: add FW versions for Karoq 2021 (#25271) * fingerprint Skoda Karoq 2021 dongle: 2c4292a5cd10536c route: 2022-07-24--09-44-25 * Update docs Co-authored-by: Shane Smiskol --- docs/CARS.md | 2 +- selfdrive/car/volkswagen/values.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 3ee05960e0b253..099ff16b4769ac 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -142,7 +142,7 @@ A supported vehicle is one that just works when you install a comma device. Ever |Subaru|XV 2018-19|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| |Subaru|XV 2020-21|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Škoda|Karoq 2019|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Karoq 2019-21|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Kodiaq 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Octavia 2015, 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Octavia RS 2016|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 34eb75e53904fa..80cd421d64cc82 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -177,7 +177,7 @@ class VWCarInfo(CarInfo): CAR.SEAT_ATECA_MK1: VWCarInfo("SEAT Ateca 2018"), CAR.SEAT_LEON_MK3: VWCarInfo("SEAT Leon 2014-20"), CAR.SKODA_KAMIQ_MK1: VWCarInfo("Škoda Kamiq 2021", footnotes=[Footnote.KAMIQ]), - CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019"), + CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019-21"), CAR.SKODA_KODIAQ_MK1: VWCarInfo("Škoda Kodiaq 2018-19"), CAR.SKODA_SCALA_MK1: VWCarInfo("Škoda Scala 2020"), CAR.SKODA_SUPERB_MK3: VWCarInfo("Škoda Superb 2015-18"), @@ -821,18 +821,23 @@ class VWCarInfo(CarInfo): CAR.SKODA_KAROQ_MK1: { (Ecu.engine, 0x7e0, None): [ b'\xf1\x8705E906018P \xf1\x896020', + b'\xf1\x8705L906022BS\xf1\x890913', ], (Ecu.transmission, 0x7e1, None): [ b'\xf1\x870CW300041S \xf1\x891615', + b'\xf1\x870GC300014L \xf1\x892802', ], (Ecu.srs, 0x715, None): [ b'\xf1\x873Q0959655BH\xf1\x890712\xf1\x82\0161213001211001101131122012100', + b'\xf1\x873Q0959655DE\xf1\x890731\xf1\x82\x0e1213001211001101131121012J00', ], (Ecu.eps, 0x712, None): [ b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\00567T6100500', + b'\xf1\x875Q0910143C \xf1\x892211\xf1\x82\x0567T6100700', ], (Ecu.fwdRadar, 0x757, None): [ b'\xf1\x872Q0907572M \xf1\x890233', + b'\xf1\x872Q0907572T \xf1\x890383', ], }, CAR.SKODA_KODIAQ_MK1: { From 0574b8504f85132bfbe5d8a41913e37ad030401e Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Mon, 25 Jul 2022 16:15:14 -0500 Subject: [PATCH 428/436] VW MQB: Cleanup stock ACC button handling (#25168) * VW MQB: Cleanup stock ACC button handling * use controls resume output as trigger * these can wait until taco bell * pass through of previously fixed value * retry CI * checks already done in carcontroller * don't need these anymore * reduce diff for now * remove parenthesis * update refs * fix packer exception * update refs Co-authored-by: Shane Smiskol --- selfdrive/car/volkswagen/carcontroller.py | 36 +++++------------------ selfdrive/car/volkswagen/carstate.py | 36 ++++++++++------------- selfdrive/car/volkswagen/interface.py | 19 +----------- selfdrive/car/volkswagen/values.py | 24 +++++++-------- selfdrive/car/volkswagen/volkswagencan.py | 22 +++++--------- selfdrive/test/process_replay/ref_commit | 2 +- 6 files changed, 44 insertions(+), 95 deletions(-) diff --git a/selfdrive/car/volkswagen/carcontroller.py b/selfdrive/car/volkswagen/carcontroller.py index 5543d4f05ce719..99a3ce55f4621b 100644 --- a/selfdrive/car/volkswagen/carcontroller.py +++ b/selfdrive/car/volkswagen/carcontroller.py @@ -2,7 +2,7 @@ from opendbc.can.packer import CANPacker from selfdrive.car import apply_std_steer_torque_limits from selfdrive.car.volkswagen import volkswagencan -from selfdrive.car.volkswagen.values import DBC_FILES, CANBUS, MQB_LDW_MESSAGES, BUTTON_STATES, CarControllerParams as P +from selfdrive.car.volkswagen.values import DBC_FILES, CANBUS, MQB_LDW_MESSAGES, CarControllerParams as P VisualAlert = car.CarControl.HUDControl.VisualAlert @@ -17,10 +17,6 @@ def __init__(self, dbc_name, CP, VM): self.hcaSameTorqueCount = 0 self.hcaEnabledFrameCount = 0 - self.graButtonStatesToSend = None - self.graMsgSentCount = 0 - self.graMsgStartFramePrev = 0 - self.graMsgBusCounterPrev = 0 def update(self, CC, CS, ext_bus): actuators = CC.actuators @@ -83,30 +79,12 @@ def update(self, CC, CS, ext_bus): # **** ACC Button Controls ********************************************** # - # FIXME: this entire section is in desperate need of refactoring - - if self.CP.pcmCruise: - if self.frame > self.graMsgStartFramePrev + P.GRA_VBP_STEP: - if CC.cruiseControl.cancel: - # Cancel ACC if it's engaged with OP disengaged. - self.graButtonStatesToSend = BUTTON_STATES.copy() - self.graButtonStatesToSend["cancel"] = True - elif CC.cruiseControl.resume: - # Send Resume button when planner wants car to move - self.graButtonStatesToSend = BUTTON_STATES.copy() - self.graButtonStatesToSend["resumeCruise"] = True - - if CS.graMsgBusCounter != self.graMsgBusCounterPrev: - self.graMsgBusCounterPrev = CS.graMsgBusCounter - if self.graButtonStatesToSend is not None: - if self.graMsgSentCount == 0: - self.graMsgStartFramePrev = self.frame - idx = (CS.graMsgBusCounter + 1) % 16 - can_sends.append(volkswagencan.create_mqb_acc_buttons_control(self.packer_pt, ext_bus, self.graButtonStatesToSend, CS, idx)) - self.graMsgSentCount += 1 - if self.graMsgSentCount >= P.GRA_VBP_COUNT: - self.graButtonStatesToSend = None - self.graMsgSentCount = 0 + if self.CP.pcmCruise and self.frame % P.GRA_ACC_STEP == 0: + idx = (CS.gra_stock_values["COUNTER"] + 1) % 16 + if CC.cruiseControl.cancel: + can_sends.append(volkswagencan.create_mqb_acc_buttons_control(self.packer_pt, ext_bus, CS.gra_stock_values, idx, cancel=True)) + elif CC.cruiseControl.resume: + can_sends.append(volkswagencan.create_mqb_acc_buttons_control(self.packer_pt, ext_bus, CS.gra_stock_values, idx, resume=True)) new_actuators = actuators.copy() new_actuators.steer = self.apply_steer_last / P.STEER_MAX diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index 9c4a111e4e8cf7..052a1262e43d7c 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -4,18 +4,19 @@ from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine -from selfdrive.car.volkswagen.values import DBC_FILES, CANBUS, NetworkLocation, TransmissionType, GearShifter, BUTTON_STATES, CarControllerParams +from selfdrive.car.volkswagen.values import DBC_FILES, CANBUS, NetworkLocation, TransmissionType, GearShifter, \ + CarControllerParams, MQB_BUTTONS class CarState(CarStateBase): def __init__(self, CP): super().__init__(CP) + self.button_states = {button.event_type: False for button in MQB_BUTTONS} can_define = CANDefine(DBC_FILES.mqb) if CP.transmissionType == TransmissionType.automatic: self.shifter_values = can_define.dv["Getriebe_11"]["GE_Fahrstufe"] elif CP.transmissionType == TransmissionType.direct: self.shifter_values = can_define.dv["EV_Gearshift"]["GearPosition"] self.hca_status_values = can_define.dv["LH_EPS_03"]["EPS_HCA_Status"] - self.buttonStates = BUTTON_STATES.copy() def update(self, pt_cp, cam_cp, ext_cp, trans_type): ret = car.CarState.new_message() @@ -114,26 +115,20 @@ def update(self, pt_cp, cam_cp, ext_cp, trans_type): if ret.cruiseState.speed > 90: ret.cruiseState.speed = 0 - # Update control button states for turn signals and ACC controls. - self.buttonStates["accelCruise"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Tip_Hoch"]) - self.buttonStates["decelCruise"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Tip_Runter"]) - self.buttonStates["cancel"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Abbrechen"]) - self.buttonStates["setCruise"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Tip_Setzen"]) - self.buttonStates["resumeCruise"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Tip_Wiederaufnahme"]) - self.buttonStates["gapAdjustCruise"] = bool(pt_cp.vl["GRA_ACC_01"]["GRA_Verstellung_Zeitluecke"]) + # Update button states for turn signals and ACC controls, capture all ACC button state/config for passthrough ret.leftBlinker = bool(pt_cp.vl["Blinkmodi_02"]["Comfort_Signal_Left"]) ret.rightBlinker = bool(pt_cp.vl["Blinkmodi_02"]["Comfort_Signal_Right"]) - - # Read ACC hardware button type configuration info that has to pass thru - # to the radar. Ends up being different for steering wheel buttons vs - # third stalk type controls. - self.graHauptschalter = pt_cp.vl["GRA_ACC_01"]["GRA_Hauptschalter"] - self.graTypHauptschalter = pt_cp.vl["GRA_ACC_01"]["GRA_Typ_Hauptschalter"] - self.graButtonTypeInfo = pt_cp.vl["GRA_ACC_01"]["GRA_ButtonTypeInfo"] - self.graTipStufe2 = pt_cp.vl["GRA_ACC_01"]["GRA_Tip_Stufe_2"] - # Pick up the GRA_ACC_01 CAN message counter so we can sync to it for - # later cruise-control button spamming. - self.graMsgBusCounter = pt_cp.vl["GRA_ACC_01"]["COUNTER"] + self.gra_stock_values = pt_cp.vl["GRA_ACC_01"] + buttonEvents = [] + for button in MQB_BUTTONS: + state = pt_cp.vl[button.can_addr][button.can_msg] in button.values + if self.button_states[button.event_type] != state: + event = car.CarState.ButtonEvent.new_message() + event.type = button.event_type + event.pressed = state + buttonEvents.append(event) + self.button_states[button.event_type] = state + ret.buttonEvents = buttonEvents # Additional safety checks performed in CarInterface. ret.espDisabled = pt_cp.vl["ESP_21"]["ESP_Tastung_passiv"] != 0 @@ -181,6 +176,7 @@ def get_can_parser(CP): ("GRA_Tip_Wiederaufnahme", "GRA_ACC_01"), # ACC button, resume ("GRA_Verstellung_Zeitluecke", "GRA_ACC_01"), # ACC button, time gap adj ("GRA_Typ_Hauptschalter", "GRA_ACC_01"), # ACC main button type + ("GRA_Codierung", "GRA_ACC_01"), # ACC button configuration/coding ("GRA_Tip_Stufe_2", "GRA_ACC_01"), # unknown related to stalk type ("GRA_ButtonTypeInfo", "GRA_ACC_01"), # unknown related to stalk type ("COUNTER", "GRA_ACC_01"), # GRA_ACC_01 CAN message counter diff --git a/selfdrive/car/volkswagen/interface.py b/selfdrive/car/volkswagen/interface.py index 225a6a32ca4e32..8bab93d4a3dac5 100644 --- a/selfdrive/car/volkswagen/interface.py +++ b/selfdrive/car/volkswagen/interface.py @@ -1,5 +1,5 @@ from cereal import car -from selfdrive.car.volkswagen.values import CAR, BUTTON_STATES, CANBUS, NetworkLocation, TransmissionType, GearShifter +from selfdrive.car.volkswagen.values import CAR, CANBUS, NetworkLocation, TransmissionType, GearShifter from selfdrive.car import STD_CARGO_KG, scale_rot_inertia, scale_tire_stiffness, gen_empty_fingerprint, get_safety_config from selfdrive.car.interfaces import CarInterfaceBase @@ -10,8 +10,6 @@ class CarInterface(CarInterfaceBase): def __init__(self, CP, CarController, CarState): super().__init__(CP, CarController, CarState) - self.buttonStatesPrev = BUTTON_STATES.copy() - if CP.networkLocation == NetworkLocation.fwdCamera: self.ext_bus = CANBUS.pt self.cp_ext = self.cp @@ -160,19 +158,8 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa # returns a car.CarState def _update(self, c): - buttonEvents = [] - ret = self.CS.update(self.cp, self.cp_cam, self.cp_ext, self.CP.transmissionType) - # Check for and process state-change events (button press or release) from - # the turn stalk switch or ACC steering wheel/control stalk buttons. - for button in self.CS.buttonStates: - if self.CS.buttonStates[button] != self.buttonStatesPrev[button]: - be = car.CarState.ButtonEvent.new_message() - be.type = button - be.pressed = self.CS.buttonStates[button] - buttonEvents.append(be) - events = self.create_common_events(ret, extra_gears=[GearShifter.eco, GearShifter.sport, GearShifter.manumatic]) # Low speed steer alert hysteresis logic @@ -184,10 +171,6 @@ def _update(self, c): events.add(EventName.belowSteerSpeed) ret.events = events.to_msg() - ret.buttonEvents = buttonEvents - - # update previous car states - self.buttonStatesPrev = self.CS.buttonStates.copy() return ret diff --git a/selfdrive/car/volkswagen/values.py b/selfdrive/car/volkswagen/values.py index 80cd421d64cc82..52a03c320c2ae9 100755 --- a/selfdrive/car/volkswagen/values.py +++ b/selfdrive/car/volkswagen/values.py @@ -1,4 +1,4 @@ -from collections import defaultdict +from collections import defaultdict, namedtuple from dataclasses import dataclass from enum import Enum from typing import Dict, List, Union @@ -11,6 +11,7 @@ NetworkLocation = car.CarParams.NetworkLocation TransmissionType = car.CarParams.TransmissionType GearShifter = car.CarState.GearShifter +Button = namedtuple('Button', ['event_type', 'can_addr', 'can_msg', 'values']) class CarControllerParams: @@ -18,9 +19,6 @@ class CarControllerParams: LDW_STEP = 10 # LDW_02 message frequency 10Hz GRA_ACC_STEP = 3 # GRA_ACC_01 message frequency 33Hz - GRA_VBP_STEP = 100 # Send ACC virtual button presses once a second - GRA_VBP_COUNT = 16 # Send VBP messages for ~0.5s (GRA_ACC_STEP * 16) - # Observed documented MQB limits: 3.00 Nm max, rate of change 5.00 Nm/sec. # Limiting rate-of-change based on real-world testing and Comma's safety # requirements for minimum time to lane departure. @@ -43,14 +41,16 @@ class DBC_FILES: DBC: Dict[str, Dict[str, str]] = defaultdict(lambda: dbc_dict(DBC_FILES.mqb, None)) -BUTTON_STATES = { - "accelCruise": False, - "decelCruise": False, - "cancel": False, - "setCruise": False, - "resumeCruise": False, - "gapAdjustCruise": False -} + +MQB_BUTTONS = [ + Button(car.CarState.ButtonEvent.Type.setCruise, "GRA_ACC_01", "GRA_Tip_Setzen", [1]), + Button(car.CarState.ButtonEvent.Type.resumeCruise, "GRA_ACC_01", "GRA_Tip_Wiederaufnahme", [1]), + Button(car.CarState.ButtonEvent.Type.accelCruise, "GRA_ACC_01", "GRA_Tip_Hoch", [1]), + Button(car.CarState.ButtonEvent.Type.decelCruise, "GRA_ACC_01", "GRA_Tip_Runter", [1]), + Button(car.CarState.ButtonEvent.Type.cancel, "GRA_ACC_01", "GRA_Abbrechen", [1]), + Button(car.CarState.ButtonEvent.Type.gapAdjustCruise, "GRA_ACC_01", "GRA_Verstellung_Zeitluecke", [1]), +] + MQB_LDW_MESSAGES = { "none": 0, # Nothing to display diff --git a/selfdrive/car/volkswagen/volkswagencan.py b/selfdrive/car/volkswagen/volkswagencan.py index 1b42d1a405a626..f4c21eeed4410a 100644 --- a/selfdrive/car/volkswagen/volkswagencan.py +++ b/selfdrive/car/volkswagen/volkswagencan.py @@ -32,19 +32,11 @@ def create_mqb_hud_control(packer, bus, enabled, steering_pressed, hud_alert, le }) return packer.make_can_msg("LDW_02", bus, values) -def create_mqb_acc_buttons_control(packer, bus, buttonStatesToSend, CS, idx): - values = { - "COUNTER": idx, - "GRA_Hauptschalter": CS.graHauptschalter, - "GRA_Abbrechen": buttonStatesToSend["cancel"], - "GRA_Tip_Setzen": buttonStatesToSend["setCruise"], - "GRA_Tip_Hoch": buttonStatesToSend["accelCruise"], - "GRA_Tip_Runter": buttonStatesToSend["decelCruise"], - "GRA_Tip_Wiederaufnahme": buttonStatesToSend["resumeCruise"], - "GRA_Verstellung_Zeitluecke": 3 if buttonStatesToSend["gapAdjustCruise"] else 0, - "GRA_Typ_Hauptschalter": CS.graTypHauptschalter, - "GRA_Codierung": 2, - "GRA_Tip_Stufe_2": CS.graTipStufe2, - "GRA_ButtonTypeInfo": CS.graButtonTypeInfo - } +def create_mqb_acc_buttons_control(packer, bus, gra_stock_values, idx, cancel=False, resume=False): + values = gra_stock_values.copy() + + values["COUNTER"] = idx + values["GRA_Abbrechen"] = cancel + values["GRA_Tip_Wiederaufnahme"] = resume + return packer.make_can_msg("GRA_ACC_01", bus, values) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index ce4f5c1711df2b..0c1fa408e7e24e 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -8f918a54cfefcaada91a7eca0c14ca4d4b6f11c8 \ No newline at end of file +dfacf4d807e04b855832218803b5c025d06569b8 \ No newline at end of file From 49bd53549d3e4ee16d9fcfc9f7a3fa27957d8ddb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 14:46:21 -0700 Subject: [PATCH 429/436] Chrysler: increase steering angle resolution (#25278) --- opendbc | 2 +- selfdrive/car/chrysler/carstate.py | 5 +++-- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/opendbc b/opendbc index 4195e8f4c9998c..67cf76f2524209 160000 --- a/opendbc +++ b/opendbc @@ -1 +1 @@ -Subproject commit 4195e8f4c9998c0a1a6084c3cc71499307bbd81e +Subproject commit 67cf76f2524209ea19f6f867cb4671c88a3347ac diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 47b28f9e05dbc2..fefd351a232741 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -63,7 +63,7 @@ def update(self, cp, cp_cam): ret.genericToggle = cp.vl["STEERING_LEVERS"]["HIGH_BEAM_PRESSED"] == 1 # steering wheel - ret.steeringAngleDeg = cp.vl["STEERING"]["STEER_ANGLE"] + ret.steeringAngleDeg = cp.vl["STEERING"]["STEERING_ANGLE"] + cp.vl["STEERING"]["STEERING_ANGLE_HP"] ret.steeringRateDeg = cp.vl["STEERING"]["STEERING_RATE"] ret.steeringTorque = cp.vl["EPS_2"]["COLUMN_TORQUE"] ret.steeringTorqueEps = cp.vl["EPS_2"]["EPS_TORQUE_MOTOR"] @@ -126,7 +126,8 @@ def get_can_parser(CP): ("WHEEL_SPEED_RR", "ESP_6"), ("WHEEL_SPEED_RL", "ESP_6"), ("WHEEL_SPEED_FR", "ESP_6"), - ("STEER_ANGLE", "STEERING"), + ("STEERING_ANGLE", "STEERING"), + ("STEERING_ANGLE_HP", "STEERING"), ("STEERING_RATE", "STEERING"), ("TURN_SIGNALS", "STEERING_LEVERS"), ("HIGH_BEAM_PRESSED", "STEERING_LEVERS"), diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 0c1fa408e7e24e..0d057476448be7 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -dfacf4d807e04b855832218803b5c025d06569b8 \ No newline at end of file +ee36966d20fa63ed4b9488bbc75d61e95e464cb7 \ No newline at end of file From 347455325e5cd67e0697708ac980782aec2c3b33 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 15:16:38 -0700 Subject: [PATCH 430/436] Mazda: use torque controller (#25265) * Mazda: use torque controller * update refs * update refs --- selfdrive/car/mazda/interface.py | 14 ++------------ selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/selfdrive/car/mazda/interface.py b/selfdrive/car/mazda/interface.py index 35e3c1bb01f259..8640903444cbf7 100755 --- a/selfdrive/car/mazda/interface.py +++ b/selfdrive/car/mazda/interface.py @@ -28,34 +28,24 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.steerLimitTimer = 0.8 tire_stiffness_factor = 0.70 # not optimized yet + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + if candidate in (CAR.CX5, CAR.CX5_2022): ret.mass = 3655 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.7 ret.steerRatio = 15.5 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.19], [0.019]] - ret.lateralTuning.pid.kf = 0.00006 elif candidate in (CAR.CX9, CAR.CX9_2021): ret.mass = 4217 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 3.1 ret.steerRatio = 17.6 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.19], [0.019]] - ret.lateralTuning.pid.kf = 0.00006 elif candidate == CAR.MAZDA3: ret.mass = 2875 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.7 ret.steerRatio = 14.0 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.19], [0.019]] - ret.lateralTuning.pid.kf = 0.00006 elif candidate == CAR.MAZDA6: ret.mass = 3443 * CV.LB_TO_KG + STD_CARGO_KG ret.wheelbase = 2.83 ret.steerRatio = 15.5 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.19], [0.019]] - ret.lateralTuning.pid.kf = 0.00006 if candidate not in (CAR.CX5_2022, ): ret.minSteerSpeed = LKAS_LIMITS.DISABLE_SPEED * CV.KPH_TO_MS diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 0d057476448be7..5067208d0ecc53 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -ee36966d20fa63ed4b9488bbc75d61e95e464cb7 \ No newline at end of file +2da0446f3cc0c4fd705f7b199ed1d9e6341fc466 \ No newline at end of file From 5101a7cf8889750e1ed8269f0e456a29f9f444eb Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 15:21:58 -0700 Subject: [PATCH 431/436] Chrylser: some Ram 1500 can steer to 0 (#25279) --- selfdrive/car/chrysler/interface.py | 7 +++++-- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index 56cece47bc9e8b..3c2bc3131a4b3c 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -51,10 +51,13 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.wheelbase = 3.88 ret.steerRatio = 16.3 ret.mass = 2493. + STD_CARGO_KG - ret.maxLateralAccel = 2.4 - ret.minSteerSpeed = 14.5 CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) + ret.minSteerSpeed = 14.5 + if car_fw is not None: + for fw in car_fw: + if fw.ecu == 'eps' and fw.fwVersion in (b"68312176AE", b"68312176AG", b"68273275AG"): + ret.minSteerSpeed = 0. else: raise ValueError(f"Unsupported car: {candidate}") diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 5067208d0ecc53..01e762d336f014 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -2da0446f3cc0c4fd705f7b199ed1d9e6341fc466 \ No newline at end of file +66790e176b98244bb76ce19fb1aa943b36c87dec \ No newline at end of file From 5d4d2c3b0dfd8607144066317d0bb60962975c5f Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 15:55:43 -0700 Subject: [PATCH 432/436] Subaru: move all pre-global cars to torque tune (#25281) * Subaru: move all pre-global cars to torque tune * pid init --- selfdrive/car/subaru/interface.py | 34 ++++++++++++------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/selfdrive/car/subaru/interface.py b/selfdrive/car/subaru/interface.py index f820cf42ba743a..d246ab2022b79e 100644 --- a/selfdrive/car/subaru/interface.py +++ b/selfdrive/car/subaru/interface.py @@ -13,6 +13,7 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.carName = "subaru" ret.radarOffCan = True + ret.dashcamOnly = candidate in PREGLOBAL_CARS if candidate in PREGLOBAL_CARS: ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.subaruLegacy)] @@ -21,9 +22,9 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.safetyConfigs = [get_safety_config(car.CarParams.SafetyModel.subaru)] ret.enableBsm = 0x228 in fingerprint[0] - ret.dashcamOnly = candidate in PREGLOBAL_CARS - ret.steerLimitTimer = 0.4 + ret.steerActuatorDelay = 0.1 + CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) if candidate == CAR.ASCENT: ret.mass = 2031. + STD_CARGO_KG @@ -31,70 +32,61 @@ def get_params(candidate, fingerprint=gen_empty_fingerprint(), car_fw=None, disa ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 13.5 ret.steerActuatorDelay = 0.3 # end-to-end angle controller + ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00003 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.0025, 0.1], [0.00025, 0.01]] - if candidate == CAR.IMPREZA: + elif candidate == CAR.IMPREZA: ret.mass = 1568. + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 15 ret.steerActuatorDelay = 0.4 # end-to-end angle controller + ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2, 0.3], [0.02, 0.03]] - if candidate == CAR.IMPREZA_2020: + elif candidate == CAR.IMPREZA_2020: ret.mass = 1480. + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 17 # learned, 14 stock - ret.steerActuatorDelay = 0.1 + ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.00005 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.045, 0.042, 0.20], [0.04, 0.035, 0.045]] - if candidate == CAR.FORESTER: + elif candidate == CAR.FORESTER: ret.mass = 1568. + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 17 # learned, 14 stock - ret.steerActuatorDelay = 0.1 + ret.lateralTuning.init('pid') ret.lateralTuning.pid.kf = 0.000038 ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 14., 23.], [0., 14., 23.]] ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.065, 0.2], [0.001, 0.015, 0.025]] - if candidate in (CAR.FORESTER_PREGLOBAL, CAR.OUTBACK_PREGLOBAL_2018): + elif candidate in (CAR.FORESTER_PREGLOBAL, CAR.OUTBACK_PREGLOBAL_2018): ret.safetyConfigs[0].safetyParam = 1 # Outback 2018-2019 and Forester have reversed driver torque signal ret.mass = 1568 + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 20 # learned, 14 stock - ret.steerActuatorDelay = 0.1 - ret.lateralTuning.pid.kf = 0.000039 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 10., 20.], [0., 10., 20.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.05, 0.2], [0.003, 0.018, 0.025]] - if candidate == CAR.LEGACY_PREGLOBAL: + elif candidate == CAR.LEGACY_PREGLOBAL: ret.mass = 1568 + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 12.5 # 14.5 stock ret.steerActuatorDelay = 0.15 - ret.lateralTuning.pid.kf = 0.00005 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 20.], [0., 20.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.1, 0.2], [0.01, 0.02]] - if candidate == CAR.OUTBACK_PREGLOBAL: + elif candidate == CAR.OUTBACK_PREGLOBAL: ret.mass = 1568 + STD_CARGO_KG ret.wheelbase = 2.67 ret.centerToFront = ret.wheelbase * 0.5 ret.steerRatio = 20 # learned, 14 stock - ret.steerActuatorDelay = 0.1 - ret.lateralTuning.pid.kf = 0.000039 - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0., 10., 20.], [0., 10., 20.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.01, 0.05, 0.2], [0.003, 0.018, 0.025]] # TODO: get actual value, for now starting with reasonable value for # civic and scaling by mass and wheelbase From 5a6b0a9f4ca065a14abe085c47bac0ef36455c9b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Jul 2022 19:08:56 -0700 Subject: [PATCH 433/436] fw_versions: fix ECU scanning (#25283) Fix when scanning --- selfdrive/car/fw_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 9c6f6e44f03e53..1c696b8ed3ae5c 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -467,7 +467,7 @@ def get_fw_versions(logcan, sendcan, query_brand=None, extra=None, timeout=0.1, for (addr, rx_addr), version in query.get_data(timeout).items(): f = car.CarParams.CarFw.new_message() - f.ecu = ecu_types[(r.brand, addr[0], addr[1])] + f.ecu = ecu_types.get((r.brand, addr[0], addr[1]), Ecu.unknown) f.fwVersion = version f.address = addr[0] f.responseAddress = rx_addr From 260171fcd8a5659ed2501cc6b425d037323f2fa8 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 25 Jul 2022 19:48:30 -0700 Subject: [PATCH 434/436] EV6: can-fd footnote (#25284) --- docs/CARS.md | 115 ++++++++++++++++---------------- selfdrive/car/hyundai/values.py | 11 ++- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index 099ff16b4769ac..c84008bd630bf8 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -92,7 +92,7 @@ A supported vehicle is one that just works when you install a comma device. Ever |Jeep|Grand Cherokee 2016-18|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Jeep|Grand Cherokee 2019-21|Adaptive Cruise|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|Ceed 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Kia|EV6 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Kia|EV6 2022[3](#footnotes)|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|Forte 2018|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|Forte 2019-21|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|K5 2021-22|SCC|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| @@ -109,19 +109,19 @@ A supported vehicle is one that just works when you install a comma device. Ever |Kia|Sorento 2019|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|Stinger 2018-20|SCC + LKAS|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Kia|Telluride 2020|SCC + LKAS|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|CT Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|CT Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|ES 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|ES Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|ES Hybrid 2017-18|LSS|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|ES Hybrid 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|IS 2017-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|NX 2018-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX 2018-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|NX 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|NX Hybrid 2018-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|NX Hybrid 2018-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|NX Hybrid 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|RC 2017-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|RX 2016-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX 2016-18|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|RX 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Lexus|RX Hybrid 2016-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Lexus|RX Hybrid 2016-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|RX Hybrid 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Lexus|UX Hybrid 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Mazda|CX-5 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| @@ -141,7 +141,7 @@ A supported vehicle is one that just works when you install a comma device. Ever |Subaru|Impreza 2020-22|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Subaru|XV 2018-19|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)| |Subaru|XV 2020-21|EyeSight|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Škoda|Kamiq 2021[5](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Škoda|Kamiq 2021[6](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Karoq 2019-21|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Kodiaq 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Škoda|Octavia 2015, 2018-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| @@ -150,85 +150,86 @@ A supported vehicle is one that just works when you install a comma device. Ever |Škoda|Superb 2015-18|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Alphard 2019-20|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Alphard Hybrid 2021|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Avalon 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Avalon 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Avalon 2019-21|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2017-18|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon 2019-21|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Avalon 2022|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Avalon Hybrid 2019-21|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Avalon Hybrid 2019-21|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Avalon Hybrid 2022|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|C-HR 2017-21|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|C-HR Hybrid 2017-19|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Camry 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Camry 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Camry Hybrid 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[5](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)[5](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Camry Hybrid 2018-20|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)[5](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Camry Hybrid 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Corolla 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Corolla 2017-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Corolla 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Corolla Cross (Non-US only) 2020-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Corolla Hatchback 2019-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Corolla Hybrid 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Highlander 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander 2017-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Highlander 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Highlander Hybrid 2017-19|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Highlander Hybrid 2017-19|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Highlander Hybrid 2020-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Mirai 2021|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Prius 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Prius 2017-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius 2017-20|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Prius 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Prius Prime 2017-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius Prime 2017-20|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|Prius Prime 2021-22|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Prius v 2017|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|RAV4 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|RAV4 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Prius v 2017|TSS-P|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 2017-18|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|RAV4 2019-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|RAV4 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|RAV4 Hybrid 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|RAV4 Hybrid 2017-18|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2016|TSS-P|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|RAV4 Hybrid 2017-18|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|RAV4 Hybrid 2019-21|All|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Toyota|RAV4 Hybrid 2022|All|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Toyota|Sienna 2018-20|All|[![star](assets/icon-star-half.svg)](##)[3](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Arteon 2018-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Arteon eHybrid 2020-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Arteon R 2020-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Atlas 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Atlas Cross Sport 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|California 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Caravelle 2020[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|CC 2018-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Toyota|Sienna 2018-20|All|[![star](assets/icon-star-half.svg)](##)[4](#footnotes)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon 2018-22[8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon eHybrid 2020-22[8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Arteon R 2020-22[8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Atlas 2018-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Atlas Cross Sport 2021-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|California 2021[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Caravelle 2020[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|CC 2018-22[8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|e-Golf 2014-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Golf 2015-20[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf 2015-20[9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Golf Alltrack 2015-19|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Golf GTD 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Golf GTE 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Golf GTI 2015-21|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Golf R 2015-19[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Golf R 2015-19[9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Golf SportsVan 2015-20|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Jetta 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Jetta GLI 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Passat 2015-22[6,7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Passat Alltrack 2015-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Passat GTE 2015-22[7,8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Polo 2020-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Polo GTI 2020-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|T-Cross 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|T-Roc 2021[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Taos 2022[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Teramont 2018-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Teramont Cross Sport 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Teramont X 2021-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| -|Volkswagen|Tiguan 2019-22[7](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Jetta 2018-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Jetta GLI 2021-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat 2015-22[7,8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat Alltrack 2015-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Passat GTE 2015-22[8,9](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Polo 2020-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Polo GTI 2020-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|T-Cross 2021[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|T-Roc 2021[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Taos 2022[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont 2018-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont Cross Sport 2021-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Teramont X 2021-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| +|Volkswagen|Tiguan 2019-22[8](#footnotes)|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| |Volkswagen|Touran 2017|Driver Assistance|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)| 1Requires an OBD-II car harness and community built ASCM harness. NOTE: disconnecting the ASCM disables Automatic Emergency Braking (AEB).
    22019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
    -3When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
    -428mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
    -5Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
    -6Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.
    -7Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.
    -8Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)
    +3Requires a red panda and additional harness box.
    +4When disconnecting the Driver Support Unit (DSU), openpilot Adaptive Cruise Control (ACC) will replace stock Adaptive Cruise Control (ACC). NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
    +528mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
    +6Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
    +7Not including the USA/China market Passat, which is based on the (currently) unsupported PQ35/NMS platform.
    +8Model-years 2021 and beyond may have a new camera harness design, which isn't yet available from the comma store. Before ordering, remove the Lane Assist camera cover and check to see if the connector is black (older design) or light brown (newer design). For the newer design, in the interim, choose "VW J533 Development" from the vehicle drop-down for a harness that integrates at the CAN gateway inside the dashboard.
    +9Includes versions with extra rear cargo space (may be called Variant, Estate, SportWagen, Shooting Brake, etc.)
    ## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). diff --git a/selfdrive/car/hyundai/values.py b/selfdrive/car/hyundai/values.py index 45985d2a451597..7a25188e87b2aa 100644 --- a/selfdrive/car/hyundai/values.py +++ b/selfdrive/car/hyundai/values.py @@ -1,10 +1,11 @@ +from enum import Enum from dataclasses import dataclass from typing import Dict, List, Optional, Union from cereal import car from common.conversions import Conversions as CV from selfdrive.car import dbc_dict -from selfdrive.car.docs_definitions import CarInfo, Harness +from selfdrive.car.docs_definitions import CarFootnote, CarInfo, Column, Harness Ecu = car.CarParams.Ecu @@ -86,6 +87,12 @@ class CAR: GENESIS_G90 = "GENESIS G90 2017" +class Footnote(Enum): + CANFD = CarFootnote( + "Requires a red panda and additional harness box.", + Column.MODEL) + + @dataclass class HyundaiCarInfo(CarInfo): # TODO: we can probably remove LKAS. LKAS is standard on many @@ -153,7 +160,7 @@ class HyundaiCarInfo(CarInfo): ], CAR.KIA_STINGER: HyundaiCarInfo("Kia Stinger 2018-20", video_link="https://www.youtube.com/watch?v=MJ94qoofYw0", harness=Harness.hyundai_c), CAR.KIA_CEED: HyundaiCarInfo("Kia Ceed 2019", harness=Harness.hyundai_e), - CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.hyundai_p), + CAR.KIA_EV6: HyundaiCarInfo("Kia EV6 2022", "All", harness=Harness.hyundai_p, footnotes=[Footnote.CANFD,]), # Genesis CAR.GENESIS_G70: HyundaiCarInfo("Genesis G70 2018-19", "All", harness=Harness.hyundai_f), From f648fd83c3547dd5ca840a81af1717ece8abc0c8 Mon Sep 17 00:00:00 2001 From: sshane Date: Sun, 14 Aug 2022 18:27:04 -0700 Subject: [PATCH 435/436] fix import --- common/data_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data_collector.py b/common/data_collector.py index 4cdb4c35b8a8a5..d35a4959435d9a 100644 --- a/common/data_collector.py +++ b/common/data_collector.py @@ -1,5 +1,5 @@ from common.travis_checker import travis -from selfdrive.swaglog import cloudlog +from system.swaglog import cloudlog from common.realtime import sec_since_boot from common.op_params import opParams import threading From c2444719092c50494914bb40b2275d292cc74190 Mon Sep 17 00:00:00 2001 From: sshane Date: Sun, 14 Aug 2022 18:35:22 -0700 Subject: [PATCH 436/436] Fix class variables --- selfdrive/ui/qt/widgets/prime.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/qt/widgets/prime.cc b/selfdrive/ui/qt/widgets/prime.cc index 1ae39d7f5b30c2..c10ec6190ac606 100644 --- a/selfdrive/ui/qt/widgets/prime.cc +++ b/selfdrive/ui/qt/widgets/prime.cc @@ -124,13 +124,13 @@ PrimeUserWidget::PrimeUserWidget(QWidget* parent) : QWidget(parent) { primeLayout->setMargin(0); primeWidget->setContentsMargins(60, 50, 60, 50); - QLabel* subscribed = new QLabel(tr("✕ NOT SUBSCRIBED")); + subscribed = new QLabel(tr("✕ NOT SUBSCRIBED")); subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #ff4e4e;"); primeLayout->addWidget(subscribed, 0, Qt::AlignTop); primeLayout->addSpacing(60); - QLabel* commaPrime = new QLabel(tr("comma prime")); + commaPrime = new QLabel(tr("comma prime")); commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;"); primeLayout->addWidget(commaPrime, 0, Qt::AlignTop);