diff --git a/.gitignore b/.gitignore index f3a632ff..a1c7bcc6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ a.out .cache/ .comma_sysroot/ .venv-linux-arm64/ +compiledmodels/ /docs_site/ diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py index 05e698b2..7f97de33 100644 --- a/frogpilot/common/frogpilot_variables.py +++ b/frogpilot/common/frogpilot_variables.py @@ -249,11 +249,18 @@ def get_frogpilot_toggles(sm=messaging.SubMaster(["frogpilotPlan"])): return toggles @cache +def _process_frogpilot_plan_toggles(toggles): + return SimpleNamespace(**json.loads(toggles)) + + def process_frogpilot_toggles(toggles): if toggles: - return SimpleNamespace(**json.loads(toggles)) + return _process_frogpilot_plan_toggles(toggles) return FrogPilotVariables().frogpilot_toggles + +process_frogpilot_toggles.cache_clear = _process_frogpilot_plan_toggles.cache_clear + def update_frogpilot_toggles(): if not hasattr(update_frogpilot_toggles, "_params_memory"): update_frogpilot_toggles._params_memory = Params(memory=True) diff --git a/frogpilot/navigation/mapd_wrapper.py b/frogpilot/navigation/mapd_wrapper.py new file mode 100644 index 00000000..ff469d8a --- /dev/null +++ b/frogpilot/navigation/mapd_wrapper.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +import json +import os +import signal +import subprocess +import sys +import time + +from collections import defaultdict, deque +from pathlib import Path + +from openpilot.common.basedir import BASEDIR +from openpilot.common.swaglog import cloudlog + +MAPD_DIR = Path(BASEDIR) / "frogpilot/navigation" +MAPD_BIN = MAPD_DIR / "mapd" +OFFLINE_ROOT = Path("/data/media/0/osm/offline") +RESTART_DELAY_S = 0.25 +FAILURE_WINDOW_S = 3.0 +FAILURE_THRESHOLD = 3 + + +def extract_bounds_filename(line: str) -> str | None: + try: + payload = json.loads(line) + except json.JSONDecodeError: + return None + + if payload.get("msg") != "Loading bounds file": + return None + + filename = payload.get("filename") + return filename if isinstance(filename, str) else None + + +def is_offline_read_error(line: str) -> bool: + try: + payload = json.loads(line) + except json.JSONDecodeError: + return False + + return payload.get("msg") == "could not unmarshal offline data" + + +class CorruptTileMonitor: + def __init__(self, threshold: int = FAILURE_THRESHOLD, window_s: float = FAILURE_WINDOW_S): + self.threshold = threshold + self.window_s = window_s + self.current_filename: str | None = None + self.failures: dict[str, deque[float]] = defaultdict(deque) + + def observe(self, line: str, now: float | None = None) -> str | None: + filename = extract_bounds_filename(line) + if filename is not None: + self.current_filename = filename + return None + + if not is_offline_read_error(line) or self.current_filename is None: + return None + + ts = time.monotonic() if now is None else now + failures = self.failures[self.current_filename] + failures.append(ts) + + cutoff = ts - self.window_s + while failures and failures[0] < cutoff: + failures.popleft() + + if len(failures) >= self.threshold: + return self.current_filename + return None + + +def quarantine_offline_tile(filename: str) -> Path | None: + tile_path = Path(filename) + try: + tile_path.relative_to(OFFLINE_ROOT) + except ValueError: + cloudlog.warning(f"mapd_wrapper refusing to quarantine unexpected path: {filename}") + return None + + if not tile_path.exists(): + return None + + quarantined = tile_path.with_name(f"{tile_path.name}.corrupt.{int(time.time())}") + tile_path.rename(quarantined) + return quarantined + + +def terminate_child(proc: subprocess.Popen[str]) -> None: + if proc.poll() is not None: + return + + proc.terminate() + try: + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=2) + + +def run_mapd_once() -> int: + proc = subprocess.Popen( + [MAPD_BIN.as_posix()], + cwd=MAPD_DIR, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + assert proc.stdout is not None + + def _handle_signal(signum, _frame): + terminate_child(proc) + raise SystemExit(128 + signum) + + signal.signal(signal.SIGTERM, _handle_signal) + signal.signal(signal.SIGINT, _handle_signal) + + monitor = CorruptTileMonitor() + + for line in proc.stdout: + print(line, end="") + bad_tile = monitor.observe(line) + if bad_tile is None: + continue + + quarantined = quarantine_offline_tile(bad_tile) + if quarantined is None: + cloudlog.warning(f"mapd_wrapper detected repeated offline read failures for {bad_tile}, but could not quarantine it") + else: + message = f"mapd_wrapper quarantined corrupt offline tile: {bad_tile} -> {quarantined}" + print(message, flush=True) + cloudlog.warning(message) + + terminate_child(proc) + return 1 + + return proc.wait() + + +def main() -> None: + while True: + exit_code = run_mapd_once() + if exit_code == 1: + time.sleep(RESTART_DELAY_S) + continue + raise SystemExit(exit_code) + + +if __name__ == "__main__": + main() diff --git a/frogpilot/navigation/test_mapd_wrapper.py b/frogpilot/navigation/test_mapd_wrapper.py new file mode 100644 index 00000000..e1c591c6 --- /dev/null +++ b/frogpilot/navigation/test_mapd_wrapper.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import json + +from pathlib import Path + +from openpilot.frogpilot.navigation.mapd_wrapper import CorruptTileMonitor, quarantine_offline_tile + + +def _loading_line(filename: str) -> str: + return json.dumps({"msg": "Loading bounds file", "filename": filename}) + + +def _error_line() -> str: + return json.dumps({"msg": "could not unmarshal offline data", "error": "EOF"}) + + +def test_corrupt_tile_monitor_triggers_after_repeated_failures(): + filename = "/data/media/0/osm/offline/36/-98/37.500000_-98.000000_37.750000_-97.750000" + monitor = CorruptTileMonitor(threshold=3, window_s=3.0) + + assert monitor.observe(_loading_line(filename), now=0.0) is None + assert monitor.observe(_error_line(), now=0.1) is None + assert monitor.observe(_loading_line(filename), now=0.2) is None + assert monitor.observe(_error_line(), now=0.3) is None + assert monitor.observe(_loading_line(filename), now=0.4) is None + assert monitor.observe(_error_line(), now=0.5) == filename + + +def test_quarantine_offline_tile_renames_file(tmp_path, monkeypatch): + offline_root = tmp_path / "offline" + tile = offline_root / "36/-98/37.500000_-98.000000_37.750000_-97.750000" + tile.parent.mkdir(parents=True) + tile.write_text("bad") + + monkeypatch.setattr("openpilot.frogpilot.navigation.mapd_wrapper.OFFLINE_ROOT", offline_root) + + quarantined = quarantine_offline_tile(tile.as_posix()) + + assert quarantined is not None + assert not tile.exists() + assert Path(quarantined).exists() + assert Path(quarantined).name.startswith(f"{tile.name}.corrupt.") diff --git a/models b/models new file mode 100755 index 00000000..1e648cdc --- /dev/null +++ b/models @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eo pipefail + +DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +set +u +source "$DIR/launch_env.sh" +exec python3 "$DIR/scripts/model_compiler.py" "$@" diff --git a/opendbc_repo/opendbc/car/gm/interface.py b/opendbc_repo/opendbc/car/gm/interface.py index 4d280a11..1d1dadd8 100755 --- a/opendbc_repo/opendbc/car/gm/interface.py +++ b/opendbc_repo/opendbc/car/gm/interface.py @@ -485,7 +485,6 @@ class CarInterface(CarInterfaceBase): elif candidate == CAR.GMC_YUKON: ret.steerActuatorDelay = 0.5 CarInterfaceBase.configure_torque_tune(candidate, ret.lateralTuning) - ret.dashcamOnly = True # Needs steerRatio, tireStiffness, and lat accel factor tuning # OPGM variables elif candidate in (CAR.CHEVROLET_MALIBU, CAR.CHEVROLET_MALIBU_CC, CAR.CHEVROLET_MALIBU_HYBRID_CC): diff --git a/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_volt.dbc b/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_volt.dbc new file mode 100644 index 00000000..9361c756 --- /dev/null +++ b/opendbc_repo/opendbc/dbc/gm_global_a_powertrain_volt.dbc @@ -0,0 +1,375 @@ +CM_ "AUTOGENERATED FILE, DO NOT EDIT"; + + +CM_ "Imported file _comma.dbc starts here"; +BO_ 512 GAS_COMMAND: 6 NEO + SG_ GAS_COMMAND : 7|16@0+ (0.2777778,-96.111115) [0|1] "" INTERCEPTOR + SG_ GAS_COMMAND2 : 23|16@0+ (0.120853074,-79.15877) [0|1] "" INTERCEPTOR + SG_ ENABLE : 39|1@0+ (1,0) [0|1] "" INTERCEPTOR + SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" INTERCEPTOR + SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" INTERCEPTOR + +BO_ 513 GAS_SENSOR: 6 INTERCEPTOR + SG_ INTERCEPTOR_GAS : 7|16@0+ (0.2777778,-96.111115) [0|1] "" NEO + SG_ INTERCEPTOR_GAS2 : 23|16@0+ (0.120853074,-79.15877) [0|1] "" NEO + SG_ STATE : 39|4@0+ (1,0) [0|15] "" NEO + SG_ COUNTER_PEDAL : 35|4@0+ (1,0) [0|15] "" NEO + SG_ CHECKSUM_PEDAL : 47|8@0+ (1,0) [0|255] "" NEO + +VAL_ 513 STATE 5 "FAULT_TIMEOUT" 4 "FAULT_STARTUP" 3 "FAULT_SCE" 2 "FAULT_SEND" 1 "FAULT_BAD_CHECKSUM" 0 "NO_FAULT" ; + +CM_ "gm_global_a_powertrain.dbc starts here"; + +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BS_: + +BU_: K16_BECM K73_TCIC K9_BCM K43_PSCM K17_EBCM K20_ECM K114B_HPCM NEO K124_ASCM EPB +VAL_TABLE_ TurnSignals 2 "Right Turn" 1 "Left Turn" 0 "None" ; +VAL_TABLE_ Intellibeam 1 "Active" 0 "Inactive" ; +VAL_TABLE_ HighBeamsActive 1 "Active" 0 "Inactive" ; +VAL_TABLE_ HighBeamsTemporary 1 "Active" 0 "Inactive" ; +VAL_TABLE_ ACCLeadCar 1 "Present" 0 "Not Present" ; +VAL_TABLE_ ACCCmdActive 1 "Active" 0 "Inactive" ; +VAL_TABLE_ BrakePedalPressed 1 "Pressed" 0 "Depressed" ; +VAL_TABLE_ DistanceButton 1 "Active" 0 "Inactive" ; +VAL_TABLE_ LKAButton 1 "Active" 0 "Inactive" ; +VAL_TABLE_ ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ; +VAL_TABLE_ DriveModeButton 1 "Active" 0 "Inactive" ; +VAL_TABLE_ PRNDL 3 "Reverse" 2 "Drive" 1 "Neutral" 0 "Park" ; +VAL_TABLE_ ESPButton 1 "Active" 0 "Inactive" ; +VAL_TABLE_ DoorStatus 1 "Opened" 0 "Closed" ; +VAL_TABLE_ SeatBeltStatus 1 "Latched" 0 "Unlatched" ; +VAL_TABLE_ LKASteeringCmdActive 1 "Active" 0 "Inactive" ; +VAL_TABLE_ ACCGapLevel 3 "Far" 2 "Med" 1 "Near" 0 "Inactive" ; +VAL_TABLE_ GasRegenCmdActiveInv 1 "Inactive" 0 "Active" ; +VAL_TABLE_ GasRegenCmdActive 1 "Active" 0 "Inactive" ; +VAL_TABLE_ LKATorqueDeliveredStatus 3 "Failed" 2 "Temp. Limited" 1 "Active" 0 "Inactive" ; +VAL_TABLE_ HandsOffSWDetectionStatus 1 "Hands On" 0 "Hands Off" ; +VAL_TABLE_ HandsOffSWDetectionMode 2 "Failed" 1 "Enabled" 0 "Disabled" ; + + +BO_ 189 EBCMRegenPaddle: 7 K17_EBCM + SG_ RegenPaddle : 7|4@0+ (1,0) [0|0] "" NEO + +BO_ 190 ECMAcceleratorPos: 6 K20_ECM + SG_ BrakePedalPos : 15|8@0+ (1,0) [0|0] "sticky" NEO + SG_ GasPedalAndAcc : 23|8@0+ (1,0) [0|0] "" NEO + +BO_ 201 ECMEngineStatus: 8 K20_ECM + SG_ EngineTPS : 39|8@0+ (0.392156863,0) [0|100.000000065] "%" NEO + SG_ EngineRPM : 15|16@0+ (0.25,0) [0|0] "RPM" NEO + SG_ CruiseMainOn : 29|1@0+ (1,0) [0|1] "" NEO + SG_ BrakePressed : 40|1@0+ (1,0) [0|1] "" NEO + SG_ Standstill : 2|1@0+ (1,0) [0|1] "" NEO + SG_ CruiseActive : 31|2@0+ (1,0) [0|3] "" NEO + +BO_ 209 EBCMBrakePedalSensors: 7 K17_EBCM + SG_ Counter1 : 7|2@0+ (1,0) [0|3] "" XXX + SG_ Counter2 : 23|2@0+ (1,0) [0|3] "" XXX + SG_ BrakePedalPosition1 : 5|14@0+ (1,0) [0|16383] "" XXX + SG_ BrakePedalPosition2 : 21|14@0- (-1,0) [0|16383] "" XXX + SG_ BrakeNormalized1 : 39|8@0+ (1,0) [0|255] "" XXX + SG_ BrakeNormalized2 : 47|8@0- (-1,0) [0|255] "" XXX + +BO_ 241 EBCMBrakePedalPosition: 6 K17_EBCM + SG_ BrakePressed : 1|1@0+ (1,0) [0|1] "" XXX + SG_ BrakePedalPosition : 15|8@0+ (1,0) [0|255] "" NEO + +BO_ 298 BCMDoorBeltStatus: 8 K9_BCM + SG_ RearLeftDoor : 8|1@0+ (1,0) [0|0] "" NEO + SG_ FrontLeftDoor : 9|1@0+ (1,0) [0|0] "" NEO + SG_ FrontRightDoor : 10|1@0+ (1,0) [0|0] "" NEO + SG_ RearRightDoor : 23|1@0+ (1,0) [0|0] "" NEO + SG_ LeftSeatBelt : 12|1@0+ (1,0) [0|0] "" NEO + SG_ RightSeatBelt : 53|1@0+ (1,0) [0|0] "" NEO + +BO_ 309 ECMPRDNL: 8 K20_ECM + SG_ PRNDL : 2|3@0+ (1,0) [0|0] "" NEO + SG_ ESPButton : 4|1@0+ (1,0) [0|1] "" XXX + +BO_ 320 BCMTurnSignals: 3 K9_BCM + SG_ TurnSignals : 19|2@0+ (1,0) [0|0] "" NEO + SG_ Intellibeam : 13|1@0+ (1,0) [0|1] "" XXX + SG_ HighBeamsActive : 7|1@0+ (1,0) [0|1] "" XXX + SG_ HighBeamsTemporary : 5|1@0+ (1,0) [0|1] "" XXX + +BO_ 322 BCMBlindSpotMonitor: 7 K9_BCM + SG_ LeftBSM : 6|1@0+ (1,0) [0|1] "" XXX + SG_ RightBSM : 7|1@0+ (1,0) [0|1] "" XXX + +BO_ 328 PSCM_148: 1 K43_PSCM + +BO_ 381 ESPStatus: 6 K20_ECM + SG_ TractionControlOn : 5|1@0+ (1,0) [0|0] "" NEO + SG_ MSG17D_AccPower : 35|12@0- (1,0) [0|0] "" NEO + +BO_ 384 ASCMLKASteeringCmd: 4 NEO + SG_ RollingCounter : 5|2@0+ (1,0) [0|0] "" NEO + SG_ LKASteeringCmdChecksum : 19|12@0+ (1,0) [0|0] "" NEO + SG_ LKASteeringCmdActive : 3|1@0+ (1,0) [0|0] "" NEO + SG_ LKASteeringCmd : 2|11@0- (1,0) [0|0] "" NEO + +BO_ 388 PSCMStatus: 8 K43_PSCM + SG_ HandsOffSWDetectionMode : 20|2@0+ (1,0) [0|3] "" NEO + SG_ HandsOffSWlDetectionStatus : 21|1@0+ (1,0) [0|1] "" NEO + SG_ LKATorqueDeliveredStatus : 5|3@0+ (1,0) [0|7] "" NEO + SG_ LKADriverAppldTrq : 50|11@0- (0.01,0) [-10.24|10.23] "Nm" NEO + SG_ LKATorqueDelivered : 18|11@0- (0.01,0) [0|1] "" NEO + SG_ LKATotalTorqueDelivered : 2|11@0- (0.01,0) [-10.24|10.23] "Nm" NEO + SG_ RollingCounter : 38|4@0+ (1,0) [0|15] "" XXX + SG_ PSCMStatusChecksum : 33|10@0+ (1,0) [0|1023] "" XXX + +BO_ 417 AcceleratorPedal: 7 XXX + SG_ AcceleratorPedal : 55|8@0+ (1,0) [0|0] "" NEO + +BO_ 451 GasAndAcc: 8 XXX + SG_ GasPedalAndAcc2 : 55|8@0+ (1,0) [0|0] "" NEO + +BO_ 452 AcceleratorPedal2: 8 XXX + SG_ CruiseState : 15|3@0+ (1,0) [0|7] "" NEO + SG_ AcceleratorPedal2 : 47|8@0+ (1,0) [0|0] "" NEO + +BO_ 481 ASCMSteeringButton: 7 K124_ASCM + SG_ DistanceButton : 22|1@0+ (1,0) [0|0] "" NEO + SG_ LKAButton : 23|1@0+ (1,0) [0|0] "" NEO + SG_ ACCAlwaysOne : 24|1@0+ (1,0) [0|1] "" XXX + SG_ ACCButtons : 46|3@0+ (1,0) [0|0] "" NEO + SG_ DriveModeButton : 39|1@0+ (1,0) [0|1] "" XXX + SG_ RollingCounter : 33|2@0+ (1,0) [0|3] "" NEO + SG_ SteeringButtonChecksum : 43|12@0+ (1,0) [0|255] "" NEO + +BO_ 485 PSCMSteeringAngle: 8 K43_PSCM + SG_ SteeringWheelAngle : 15|16@0- (0.0625,0) [-2047|2047] "deg" NEO + SG_ SteeringWheelRate : 27|12@0- (1,0) [-2047|2047] "deg/s" NEO + +BO_ 489 EBCMVehicleDynamic: 8 K17_EBCM + SG_ BrakePedalPressed : 6|1@0+ (1,0) [0|0] "" NEO + SG_ LateralAcceleration : 3|10@0- (0.161,0) [-2047|2047] "m/s2" NEO + SG_ YawRate : 35|12@0- (0.625,0) [0|1] "" NEO + SG_ YawRate2 : 51|12@0- (0.0625,0) [-2047|2047] "grad/s" NEO + +BO_ 352 BCMImmobilizer: 5 K9_BCM + SG_ ImmobilizerInfo : 7|32@0+ (1,0) [0|4294967295] "" XXX + +BO_ 497 BCMGeneralPlatformStatus: 8 K9_BCM + SG_ SystemPowerMode : 1|2@0+ (1,0) [0|3] "" XXX + SG_ SystemBackUpPowerMode : 5|2@0+ (1,0) [0|3] "" XXX + SG_ ParkBrakeSwActive : 36|1@0+ (1,0) [0|3] "" XXX + +BO_ 500 SportMode: 6 XXX + SG_ SportMode : 15|1@0+ (1,0) [0|1] "" XXX + +BO_ 501 ECMPRDNL2: 8 K20_ECM + SG_ TransmissionState : 48|4@1+ (1,0) [0|7] "" NEO + SG_ PRNDL2 : 27|4@0+ (1,0) [0|255] "" NEO + SG_ ManualMode : 41|1@0+ (1,0) [0|1] "" NEO + +BO_ 532 BRAKE_RELATED: 6 XXX + SG_ UserBrakePressure : 0|9@0+ (1,0) [0|511] "" XXX + +BO_ 560 EPBStatus: 8 EPB + SG_ EPBClosed : 12|1@0+ (1,0) [0|1] "" NEO + +BO_ 562 EBCMFrictionBrakeStatus: 8 K17_EBCM + SG_ FrictionBrakeUnavailable : 46|1@0+ (1,0) [0|1] "" XXX + +BO_ 608 SPEED_RELATED: 8 XXX + SG_ RollingCounter : 5|2@0+ (1,0) [0|0] "" XXX + SG_ ClusterSpeed : 31|8@0+ (1,0) [0|0] "" XXX + +BO_ 711 BECMBatteryVoltageCurrent: 6 K17_EBCM + SG_ HVBatteryVoltage : 31|12@0+ (0.125,0) [0|511.875] "V" NEO + SG_ HVBatteryCurrent : 12|13@0- (0.15,0) [-614.4|614.25] "A" NEO + +BO_ 715 ASCMGasRegenCmd: 8 K124_ASCM + SG_ GasRegenAlwaysOne2 : 9|1@0+ (1,0) [0|1] "" NEO + SG_ GasRegenAlwaysOne : 14|1@0+ (1,0) [0|1] "" NEO + SG_ GasRegenChecksum : 47|24@0+ (1,0) [0|0] "" NEO + SG_ GasRegenCmdActiveInv : 32|1@0+ (1,0) [0|0] "" NEO + SG_ GasRegenFullStopActive : 13|1@0+ (1,0) [0|0] "" NEO + SG_ GasRegenCmdActive : 0|1@0+ (1,0) [0|0] "" NEO + SG_ RollingCounter : 7|2@0+ (1,0) [0|0] "" NEO + SG_ GasRegenAlwaysOne3 : 23|1@0+ (1,0) [0|1] "" NEO + SG_ GasRegenCmd : 22|12@0+ (1,0) [0|0] "" NEO + +BO_ 717 ASCM_2CD: 5 K124_ASCM + +BO_ 761 BRAKE_RELATED_2: 7 XXX + SG_ UserBrakePressure2 : 47|9@0+ (1,0) [0|511] "" XXX + +BO_ 789 EBCMFrictionBrakeCmd: 5 K124_ASCM + SG_ RollingCounter : 33|2@0+ (1,0) [0|0] "" NEO + SG_ FrictionBrakeMode : 7|4@0+ (1,0) [0|0] "" NEO + SG_ FrictionBrakeChecksum : 23|16@0+ (1,0) [0|0] "" NEO + SG_ FrictionBrakeCmd : 3|12@0- (1,0) [0|0] "" NEO + +BO_ 800 AEBCmd: 6 K124_ASCM + SG_ RollingCounter : 5|2@0+ (1,0) [0|3] "" NEO + SG_ AEBChecksum : 27|20@0+ (1,0) [0|0] "" NEO + SG_ AEBCmdActive : 3|1@1+ (1,0) [0|1] "" NEO + SG_ AEBCmd : 2|11@0+ (1,0) [0|0] "" NEO + SG_ AEBCmd2 : 23|8@0+ (1,0) [0|0] "" NEO + +BO_ 810 TCICOnStarGPSPosition: 8 K73_TCIC + SG_ GPSLongitude : 39|32@0+ (1,-2147483648) [0|0] "milliarcsecond" NEO + SG_ GPSLatitude : 7|32@0+ (1,0) [0|0] "milliarcsecond" NEO + +BO_ 840 EBCMWheelSpdFront: 5 K17_EBCM + SG_ FLWheelSpd : 7|16@0+ (0.0311,0) [0|255] "km/h" NEO + SG_ FRWheelSpd : 23|16@0+ (0.0311,0) [0|255] "km/h" NEO + +BO_ 842 EBCMWheelSpdRear: 5 K17_EBCM + SG_ RLWheelSpd : 7|16@0+ (0.0311,0) [0|255] "km/h" NEO + SG_ RRWheelSpd : 23|16@0+ (0.0311,0) [0|255] "km/h" NEO + SG_ RRWheelDir : 34|3@0+ (1,0) [0|7] "" NEO + SG_ RLWheelDir : 37|3@0+ (1,0) [0|7] "" NEO + +BO_ 869 ASCM_365: 4 K124_ASCM + +BO_ 880 ASCMActiveCruiseControlStatus: 6 K124_ASCM + SG_ ACCCruiseState : 8|3@1+ (1,0) [0|7] "" XXX + SG_ ACCLeadCar : 44|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ ACCAlwaysOne2 : 32|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ ACCAlwaysOne : 0|1@0+ (1,0) [0|0] "" Vector__XXX + SG_ ACCSpeedSetpoint : 19|12@0+ (0.0625,0) [0|255.9375] "km/h" NEO + SG_ ACCGapLevel : 21|2@0+ (1,0) [0|0] "" NEO + SG_ ACCResumeButton : 1|1@0+ (1,0) [0|0] "" NEO + SG_ ACCCmdActive : 23|1@0+ (1,0) [0|0] "" NEO + SG_ FCWAlert : 41|2@0+ (1,0) [0|3] "" XXX + +BO_ 967 EVDriveMode: 4 XXX + SG_ SinglePedalModeActive : 7|1@0+ (1,0) [0|1] "" XXX + SG_ SinglePedalModeRisingEdge : 21|1@0+ (1,0) [0|1] "" XXX + SG_ SinglePedalModeFallingEdge : 22|1@0+ (1,0) [0|1] "" XXX + +BO_ 977 ECMCruiseControl: 8 K20_ECM + SG_ CruiseActive : 39|1@0+ (1,0) [0|3] "" NEO + SG_ CruiseSetSpeed : 19|12@0+ (0.0625,0) [0|0] "km/h" NEO + +BO_ 1001 ECMVehicleSpeed: 8 K20_ECM + SG_ VehicleSpeed : 7|16@0+ (0.01,0) [0|0] "mph" NEO + SG_ VehicleSpeedLeft : 39|16@0+ (0.01,0) [0|0] "mph" NEO + +BO_ 1033 ASCMKeepAlive: 7 NEO + SG_ ASCMKeepAliveAllZero : 7|56@0+ (1,0) [0|0] "" NEO + +BO_ 1034 ASCM_40A: 7 K124_ASCM + +BO_ 1217 ECMEngineCoolantTemp: 8 K20_ECM + SG_ EngineCoolantTemp : 23|8@0+ (1,-40) [0|0] "C" NEO + +BO_ 1249 VIN_Part2: 8 K20_ECM + SG_ VINPart2 : 7|64@0+ (1,0) [0|0] "" NEO + +BO_ 1296 ASCM_510: 4 K124_ASCM + +BO_ 1300 VIN_Part1: 8 K20_ECM + SG_ VINPart1 : 7|64@0+ (1,0) [0|0] "" NEO + +BO_ 1912 PSCM_778: 8 K43_PSCM + +BO_ 1930 ASCM_78A: 7 K124_ASCM + +BO_TX_BU_ 384 : K124_ASCM,NEO; +BO_TX_BU_ 880 : NEO,K124_ASCM; +BO_TX_BU_ 1033 : K124_ASCM,NEO; +BO_TX_BU_ 715 : NEO,K124_ASCM; +BO_TX_BU_ 789 : NEO,K124_ASCM; +BO_TX_BU_ 800 : NEO,K124_ASCM; + + +CM_ BU_ K16_BECM "Battery Energy Control Module"; +CM_ BU_ K73_TCIC "Telematics Communication Control Module"; +CM_ BU_ K9_BCM "Body Control Module"; +CM_ BU_ K43_PSCM "Power Steering Control Module"; +CM_ BU_ K17_EBCM "Electronic Brake Control Module"; +CM_ BU_ K20_ECM "Engine Control Module"; +CM_ BU_ K114B_HPCM "Hybrid Powertrain Control Module"; +CM_ BU_ NEO "Comma NEO"; +CM_ BU_ K124_ASCM "Active Safety Control Module"; +CM_ SG_ 381 MSG17D_AccPower "Need to investigate"; +CM_ BO_ 190 "Length varies from 6 to 8 bytes by car"; +CM_ SG_ 190 GasPedalAndAcc "ACC baseline is 62"; +CM_ SG_ 322 LeftBSM "For some cars, this can only be when the blinker is also active"; +CM_ SG_ 322 RightBSM "For some cars, this can only be when the blinker is also active"; +CM_ SG_ 352 ImmobilizerInfo "Non-zero when ignition or accessory mode"; +CM_ SG_ 451 GasPedalAndAcc2 "ACC baseline is 62"; +CM_ SG_ 481 ACCAlwaysOne "Usually 1 if the car is equipped with ACC"; +CM_ SG_ 562 FrictionBrakeUnavailable "1 when ACC brake control is unavailable. Stays high if brake command messages are blocked for a period of time"; +CM_ SG_ 497 SystemPowerMode "Describes ignition"; +CM_ SG_ 497 SystemBackUpPowerMode "Describes ignition + preconditioning mode, noisy"; +CM_ SG_ 501 PRNDL2 "When ManualMode is Active, Value is 13=L1 12=L2 11=L3 ... 4=L10"; +CM_ SG_ 532 UserBrakePressure "can be lower than other brake position signals when the brakes are pre-filled from ACC braking and the user presses on the brakes. user-only pressure?"; +CM_ SG_ 608 ClusterSpeed "Cluster speed signal seems to match dash on newer cars, but is a lower rate and can be noisier."; +CM_ SG_ 761 UserBrakePressure2 "Similar to BRAKE_RELATED->UserBrakePressure"; +CM_ SG_ 1001 VehicleSpeed "Spinouts show here on 2wd. Speed derived from right front wheel (drive tire)"; +BA_DEF_ "UseGMParameterIDs" INT 0 0; +BA_DEF_ "ProtocolType" STRING ; +BA_DEF_ "BusType" STRING ; +BA_DEF_DEF_ "UseGMParameterIDs" 1; +BA_DEF_DEF_ "ProtocolType" "GMLAN"; +BA_DEF_DEF_ "BusType" ""; +BA_ "BusType" "CAN"; +BA_ "ProtocolType" "GMLAN"; +BA_ "UseGMParameterIDs" 0; +VAL_ 497 SystemPowerMode 3 "Crank Request" 2 "Run" 1 "Accessory" 0 "Off"; +VAL_ 497 SystemBackUpPowerMode 3 "Crank Request" 2 "Run" 1 "Accessory" 0 "Off"; +VAL_ 481 DistanceButton 1 "Active" 0 "Inactive" ; +VAL_ 481 LKAButton 1 "Active" 0 "Inactive" ; +VAL_ 481 ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ; +VAL_ 481 DriveModeButton 1 "Active" 0 "Inactive" ; +VAL_ 452 CruiseState 4 "Standstill" 3 "Faulted" 1 "Active" 0 "Off" ; +VAL_ 309 PRNDL 3 "R" 2 "D" 1 "N" 0 "P" ; +VAL_ 309 ESPButton 1 "Active" 0 "Inactive" ; +VAL_ 384 LKASteeringCmdActive 1 "Active" 0 "Inactive" ; +VAL_ 842 RRWheelDir 0 "Stationary" 1 "Forward" 2 "Reverse" 3 "Unsupported" 4 "Fault"; +VAL_ 842 RLWheelDir 0 "Stationary" 1 "Forward" 2 "Reverse" 3 "Unsupported" 4 "Fault"; +VAL_ 880 ACCCruiseState 2 "Adaptive" 3 "Adaptive" 4 "Non-adaptive" 5 "Non-adaptive" ; +VAL_ 880 ACCLeadCar 1 "Present" 0 "Not Present" ; +VAL_ 880 ACCGapLevel 3 "Far" 2 "Med" 1 "Near" 0 "Inactive" ; +VAL_ 880 ACCResumeButton 1 "Pressed" 0 "Depressed" ; +VAL_ 880 ACCCmdActive 1 "Active" 0 "Inactive" ; +VAL_ 388 HandsOffSWDetectionMode 2 "Failed" 1 "Enabled" 0 "Disabled" ; +VAL_ 388 HandsOffSWlDetectionStatus 1 "Hands On" 0 "Hands Off" ; +VAL_ 388 LKATorqueDeliveredStatus 3 "Failed" 2 "Temp. Limited" 1 "Active" 0 "Inactive" ; +VAL_ 489 BrakePedalPressed 1 "Pressed" 0 "Depressed" ; +VAL_ 715 GasRegenCmdActiveInv 1 "Inactive" 0 "Active" ; +VAL_ 715 GasRegenCmdActive 1 "Active" 0 "Inactive" ; +VAL_ 320 Intellibeam 1 "Active" 0 "Inactive" ; +VAL_ 320 HighBeamsActive 1 "Active" 0 "Inactive" ; +VAL_ 320 HighBeamsTemporary 1 "Active" 0 "Inactive" ; +VAL_ 501 PRNDL2 6 "L" 4 "D" 3 "N" 2 "R" 1 "P" 0 "Shifting"; +VAL_ 501 TransmissionState 11 "Shifting" 10 "Reverse" 9 "Forward" 8 "Disengaged"; +VAL_ 501 ManualMode 1 "Active" 0 "Inactive" diff --git a/panda/board/obj/body_h7.bin.signed b/panda/board/obj/body_h7.bin.signed index f8c50c0f..f08d972a 100644 Binary files a/panda/board/obj/body_h7.bin.signed and b/panda/board/obj/body_h7.bin.signed differ diff --git a/panda/board/obj/body_h7/bootstub.elf b/panda/board/obj/body_h7/bootstub.elf index 9bdd11ec..e928f540 100755 Binary files a/panda/board/obj/body_h7/bootstub.elf and b/panda/board/obj/body_h7/bootstub.elf differ diff --git a/panda/board/obj/body_h7/main.bin b/panda/board/obj/body_h7/main.bin index 13afad54..c47a9cf4 100755 Binary files a/panda/board/obj/body_h7/main.bin and b/panda/board/obj/body_h7/main.bin differ diff --git a/panda/board/obj/body_h7/main.elf b/panda/board/obj/body_h7/main.elf index faee7588..26dcdf8c 100755 Binary files a/panda/board/obj/body_h7/main.elf and b/panda/board/obj/body_h7/main.elf differ diff --git a/panda/board/obj/bootstub.body_h7.bin b/panda/board/obj/bootstub.body_h7.bin index 8f8dc686..ccec2665 100755 Binary files a/panda/board/obj/bootstub.body_h7.bin and b/panda/board/obj/bootstub.body_h7.bin differ diff --git a/panda/board/obj/bootstub.panda.bin b/panda/board/obj/bootstub.panda.bin index 4e129a1b..859fdab2 100755 Binary files a/panda/board/obj/bootstub.panda.bin and b/panda/board/obj/bootstub.panda.bin differ diff --git a/panda/board/obj/bootstub.panda_h7.bin b/panda/board/obj/bootstub.panda_h7.bin index d42e08f2..44913911 100755 Binary files a/panda/board/obj/bootstub.panda_h7.bin and b/panda/board/obj/bootstub.panda_h7.bin differ diff --git a/panda/board/obj/bootstub.panda_h7_remote.bin b/panda/board/obj/bootstub.panda_h7_remote.bin index d42e08f2..44913911 100755 Binary files a/panda/board/obj/bootstub.panda_h7_remote.bin and b/panda/board/obj/bootstub.panda_h7_remote.bin differ diff --git a/panda/board/obj/bootstub.panda_jungle_h7.bin b/panda/board/obj/bootstub.panda_jungle_h7.bin index 4f8c37b1..024084d1 100755 Binary files a/panda/board/obj/bootstub.panda_jungle_h7.bin and b/panda/board/obj/bootstub.panda_jungle_h7.bin differ diff --git a/panda/board/obj/bootstub.panda_remote.bin b/panda/board/obj/bootstub.panda_remote.bin index 4e129a1b..859fdab2 100755 Binary files a/panda/board/obj/bootstub.panda_remote.bin and b/panda/board/obj/bootstub.panda_remote.bin differ diff --git a/panda/board/obj/gitversion.h b/panda/board/obj/gitversion.h index d04731af..71ab066b 100644 --- a/panda/board/obj/gitversion.h +++ b/panda/board/obj/gitversion.h @@ -1,2 +1,2 @@ extern const uint8_t gitversion[19]; -const uint8_t gitversion[19] = "DEV-60a7fb11-DEBUG"; +const uint8_t gitversion[19] = "DEV-d17d41fb-DEBUG"; diff --git a/panda/board/obj/panda.bin.signed b/panda/board/obj/panda.bin.signed index 0c650c2a..6195b612 100644 Binary files a/panda/board/obj/panda.bin.signed and b/panda/board/obj/panda.bin.signed differ diff --git a/panda/board/obj/panda/bootstub.elf b/panda/board/obj/panda/bootstub.elf index 06125f5b..3cc7211c 100755 Binary files a/panda/board/obj/panda/bootstub.elf and b/panda/board/obj/panda/bootstub.elf differ diff --git a/panda/board/obj/panda/main.bin b/panda/board/obj/panda/main.bin index 75dd3ae9..f0d1f590 100755 Binary files a/panda/board/obj/panda/main.bin and b/panda/board/obj/panda/main.bin differ diff --git a/panda/board/obj/panda/main.elf b/panda/board/obj/panda/main.elf index fdff9927..6eb51ae3 100755 Binary files a/panda/board/obj/panda/main.elf and b/panda/board/obj/panda/main.elf differ diff --git a/panda/board/obj/panda_h7.bin.signed b/panda/board/obj/panda_h7.bin.signed index 1e8e353f..ec4ad13a 100644 Binary files a/panda/board/obj/panda_h7.bin.signed and b/panda/board/obj/panda_h7.bin.signed differ diff --git a/panda/board/obj/panda_h7/bootstub.elf b/panda/board/obj/panda_h7/bootstub.elf index 40ad4d9f..32b969cf 100755 Binary files a/panda/board/obj/panda_h7/bootstub.elf and b/panda/board/obj/panda_h7/bootstub.elf differ diff --git a/panda/board/obj/panda_h7/main.bin b/panda/board/obj/panda_h7/main.bin index 48fa8308..60d1ae49 100755 Binary files a/panda/board/obj/panda_h7/main.bin and b/panda/board/obj/panda_h7/main.bin differ diff --git a/panda/board/obj/panda_h7/main.elf b/panda/board/obj/panda_h7/main.elf index 4eeb2a64..e95a617f 100755 Binary files a/panda/board/obj/panda_h7/main.elf and b/panda/board/obj/panda_h7/main.elf differ diff --git a/panda/board/obj/panda_h7_remote.bin.signed b/panda/board/obj/panda_h7_remote.bin.signed index 29d819fb..328a1c3d 100644 Binary files a/panda/board/obj/panda_h7_remote.bin.signed and b/panda/board/obj/panda_h7_remote.bin.signed differ diff --git a/panda/board/obj/panda_h7_remote/bootstub.elf b/panda/board/obj/panda_h7_remote/bootstub.elf index 7aa4cab3..fcf532fb 100755 Binary files a/panda/board/obj/panda_h7_remote/bootstub.elf and b/panda/board/obj/panda_h7_remote/bootstub.elf differ diff --git a/panda/board/obj/panda_h7_remote/main.bin b/panda/board/obj/panda_h7_remote/main.bin index d78d074b..004b0148 100755 Binary files a/panda/board/obj/panda_h7_remote/main.bin and b/panda/board/obj/panda_h7_remote/main.bin differ diff --git a/panda/board/obj/panda_h7_remote/main.elf b/panda/board/obj/panda_h7_remote/main.elf index 083bd177..a2dfaf93 100755 Binary files a/panda/board/obj/panda_h7_remote/main.elf and b/panda/board/obj/panda_h7_remote/main.elf differ diff --git a/panda/board/obj/panda_jungle_h7.bin.signed b/panda/board/obj/panda_jungle_h7.bin.signed index 0c359998..52703056 100644 Binary files a/panda/board/obj/panda_jungle_h7.bin.signed and b/panda/board/obj/panda_jungle_h7.bin.signed differ diff --git a/panda/board/obj/panda_jungle_h7/bootstub.elf b/panda/board/obj/panda_jungle_h7/bootstub.elf index 530d99d6..85d7edd3 100755 Binary files a/panda/board/obj/panda_jungle_h7/bootstub.elf and b/panda/board/obj/panda_jungle_h7/bootstub.elf differ diff --git a/panda/board/obj/panda_jungle_h7/main.bin b/panda/board/obj/panda_jungle_h7/main.bin index 9e1033bb..3b5f158d 100755 Binary files a/panda/board/obj/panda_jungle_h7/main.bin and b/panda/board/obj/panda_jungle_h7/main.bin differ diff --git a/panda/board/obj/panda_jungle_h7/main.elf b/panda/board/obj/panda_jungle_h7/main.elf index 3dde733f..4446a9b5 100755 Binary files a/panda/board/obj/panda_jungle_h7/main.elf and b/panda/board/obj/panda_jungle_h7/main.elf differ diff --git a/panda/board/obj/panda_remote.bin.signed b/panda/board/obj/panda_remote.bin.signed index 690cf937..5f6f4a6c 100644 Binary files a/panda/board/obj/panda_remote.bin.signed and b/panda/board/obj/panda_remote.bin.signed differ diff --git a/panda/board/obj/panda_remote/bootstub.elf b/panda/board/obj/panda_remote/bootstub.elf index 9bc68593..bb949282 100755 Binary files a/panda/board/obj/panda_remote/bootstub.elf and b/panda/board/obj/panda_remote/bootstub.elf differ diff --git a/panda/board/obj/panda_remote/main.bin b/panda/board/obj/panda_remote/main.bin index 1b88b64e..7e42e0f8 100755 Binary files a/panda/board/obj/panda_remote/main.bin and b/panda/board/obj/panda_remote/main.bin differ diff --git a/panda/board/obj/panda_remote/main.elf b/panda/board/obj/panda_remote/main.elf index 6ca31b2c..45467650 100755 Binary files a/panda/board/obj/panda_remote/main.elf and b/panda/board/obj/panda_remote/main.elf differ diff --git a/panda/board/obj/version b/panda/board/obj/version index 5c83ce11..57b311e4 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-60a7fb11-DEBUG \ No newline at end of file +DEV-d17d41fb-DEBUG \ No newline at end of file diff --git a/scripts/model_compiler.py b/scripts/model_compiler.py new file mode 100644 index 00000000..18340d06 --- /dev/null +++ b/scripts/model_compiler.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +import argparse +import codecs +import os +import pickle +import re +import shutil +import subprocess +import sys + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_INPUT_ROOT = Path("/data/openpilot/uncompiledmodels") +DEFAULT_OUTPUT_ROOT = Path("/data/openpilot/compiledmodels") +COMPILE_SCRIPT = REPO_ROOT / "tinygrad_repo/examples/openpilot/compile3.py" + +COMPONENT_ALIASES = { + "driving_off_policy": ("driving_off_policy", "off_policy", "offpolicy"), + "driving_policy": ("driving_policy", "policy"), + "driving_vision": ("driving_vision", "vision"), +} +REQUIRED_COMPONENTS = {"driving_policy", "driving_vision"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Compile staged ONNX driving models into tinygrad pkls without touching selfdrive/modeld/models.", + ) + parser.add_argument("--model", help="Output model key, for example sc2.") + parser.add_argument("--input-dir", type=Path, default=DEFAULT_INPUT_ROOT, help="Directory containing staged ONNX files. Flat root files like driving_policy.onnx are preferred.") + parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_ROOT, help="Directory for compiled tinygrad pkls and metadata.") + parser.add_argument("--list", action="store_true", help="List detected staged models and exit.") + parser.add_argument("--force", action="store_true", help="Legacy no-op. Compiled outputs are always cleared before a build.") + + args, unknown = parser.parse_known_args() + dynamic_model_flags = [arg[2:] for arg in unknown if arg.startswith("--")] + invalid = [arg for arg in unknown if not arg.startswith("--")] + if invalid: + parser.error(f"Unexpected arguments: {' '.join(invalid)}") + if len(dynamic_model_flags) > 1: + parser.error("Pass only one dynamic model flag, for example ./models --sc2") + if args.model and dynamic_model_flags and args.model != dynamic_model_flags[0]: + parser.error("Use either --model sc2 or --sc2, not both with different values.") + args.model = args.model or (dynamic_model_flags[0] if dynamic_model_flags else None) + return args + + +def detect_component(path: Path) -> str | None: + stem = path.stem.lower() + for component, aliases in COMPONENT_ALIASES.items(): + if any(alias in stem for alias in aliases): + return component + return None + + +def find_staged_models(input_root: Path) -> dict[str, dict[str, Path]]: + found: dict[str, dict[str, Path]] = {} + if not input_root.is_dir(): + return found + + for child in sorted(input_root.iterdir()): + if not child.is_dir(): + continue + model_files = {} + for onnx_file in sorted(child.glob("*.onnx")): + component = detect_component(onnx_file) + if component: + model_files[component] = onnx_file + if model_files: + found[child.name] = model_files + + flat_root_files = {} + for onnx_file in sorted(input_root.glob("*.onnx")): + component = detect_component(onnx_file) + if component is None: + continue + + model_key = None + lowered = onnx_file.stem.lower() + for alias in COMPONENT_ALIASES[component]: + if lowered == alias: + model_key = None + break + suffix = f"_{alias}" + if lowered.endswith(suffix): + model_key = onnx_file.stem[:-len(suffix)] + break + + if model_key in ("", "driving"): + model_key = None + + if model_key: + found.setdefault(model_key, {})[component] = onnx_file + else: + flat_root_files[component] = onnx_file + + if flat_root_files: + found["_root"] = flat_root_files + + return found + + +def resolve_model_files(input_root: Path, model_key: str) -> dict[str, Path]: + staged = find_staged_models(input_root) + if model_key in staged: + return staged[model_key] + + root_files = staged.get("_root") + if root_files and len(staged) == 1: + return root_files + + prefixed_files = {} + for onnx_file in sorted(input_root.glob(f"{model_key}_*.onnx")): + component = detect_component(onnx_file) + if component: + prefixed_files[component] = onnx_file + return prefixed_files + + +def get_metadata_value_by_name(model, name: str): + for prop in model.metadata_props: + if prop.key == name: + return prop.value + return None + + +def write_metadata(onnx_path: Path, output_path: Path) -> None: + import onnx + + model = onnx.load(str(onnx_path)) + output_slices = get_metadata_value_by_name(model, "output_slices") + if output_slices is None: + raise ValueError(f"output_slices not found in metadata for {onnx_path.name}") + + def get_name_and_shape(value_info) -> tuple[str, tuple[int, ...]]: + shape = tuple(int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim) + return value_info.name, shape + + metadata = { + "model_checkpoint": get_metadata_value_by_name(model, "model_checkpoint"), + "output_slices": pickle.loads(codecs.decode(output_slices.encode(), "base64")), + "input_shapes": dict(get_name_and_shape(x) for x in model.graph.input), + "output_shapes": dict(get_name_and_shape(x) for x in model.graph.output), + } + + with open(output_path, "wb") as f: + pickle.dump(metadata, f) + + +def compile_component(onnx_path: Path, output_path: Path) -> None: + env = os.environ.copy() + existing_pythonpath = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = f"{REPO_ROOT}:{existing_pythonpath}" if existing_pythonpath else str(REPO_ROOT) + subprocess.run( + [sys.executable, str(COMPILE_SCRIPT), str(onnx_path), str(output_path)], + cwd=REPO_ROOT, + env=env, + check=True, + ) + + +def clear_existing_outputs(output_dir: Path) -> list[Path]: + removed = [] + for existing in sorted(output_dir.iterdir()): + if existing.is_file() or existing.is_symlink(): + existing.unlink() + elif existing.is_dir(): + shutil.rmtree(existing) + removed.append(existing) + return removed + + +def list_models(staged: dict[str, dict[str, Path]], input_root: Path) -> int: + if not staged: + print(f"No staged models found in {input_root}") + return 0 + + for model_key, files in sorted(staged.items()): + print(model_key) + for component, path in sorted(files.items()): + print(f" {component}: {path}") + return 0 + + +def main() -> int: + args = parse_args() + staged = find_staged_models(args.input_dir) + + if args.list: + return list_models(staged, args.input_dir) + + if not args.model: + available = ", ".join(sorted(k for k in staged if k != "_root")) + raise SystemExit(f"Choose a model key, for example ./models --sc2. Available staged models: {available or 'none'}") + + model_key = args.model.strip() + files = resolve_model_files(args.input_dir, model_key) + if not files: + raise SystemExit( + f"No staged ONNX files found for {model_key} in {args.input_dir}. " + f"Use {args.input_dir}/driving_policy.onnx and {args.input_dir}/driving_vision.onnx, " + f"or optionally {args.input_dir / model_key}/*.onnx" + ) + + missing = sorted(REQUIRED_COMPONENTS - set(files)) + if missing: + raise SystemExit(f"Missing required ONNX files for {model_key}: {', '.join(missing)}") + + args.output_dir.mkdir(parents=True, exist_ok=True) + print(f"Compiling {model_key} from {args.input_dir} -> {args.output_dir}") + + removed = clear_existing_outputs(args.output_dir) + if removed: + print(f" cleared {len(removed)} existing output entries") + + for component, onnx_path in sorted(files.items()): + output_pkl = args.output_dir / f"{model_key}_{component}_tinygrad.pkl" + output_metadata = args.output_dir / f"{model_key}_{component}_metadata.pkl" + + print(f" compiling {component}: {onnx_path.name}") + compile_component(onnx_path, output_pkl) + write_metadata(onnx_path, output_metadata) + print(f" saved {output_pkl.name}") + print(f" saved {output_metadata.name}") + + print("Done.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png b/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png new file mode 100644 index 00000000..4b848657 Binary files /dev/null and b/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png differ diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 3e357ee0..28190db3 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -25,7 +25,6 @@ MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl' METADATA_PATH = Path(__file__).parent / 'models/dmonitoring_model_metadata.pkl' MODELS_DIR = Path(__file__).parent / 'models' - class ModelState: inputs: dict[str, np.ndarray] output: np.ndarray @@ -46,42 +45,8 @@ class ModelState: self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()} self._blob_cache : dict[int, Tensor] = {} self.image_warp = None - self._warp_rebuild_attempted: set[tuple[int, int]] = set() - self._warp_backend_rebuild_attempted: set[tuple[int, int]] = set() self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH))) - def _load_or_rebuild_dm_warp(self, width: int, height: int): - warp_path = MODELS_DIR / f'dm_warp_{width}x{height}_tinygrad.pkl' - resolution_key = (width, height) - - def load_warp(): - with open(warp_path, "rb") as f: - return pickle.load(f) - - try: - return load_warp() - except Exception as error: - if resolution_key in self._warp_rebuild_attempted: - raise - - self._warp_rebuild_attempted.add(resolution_key) - cloudlog.exception(f"Failed to load DM warp artifact {warp_path}: {error}") - cloudlog.warning(f"Rebuilding DM warp artifact for {width}x{height}") - - try: - warp_path.unlink(missing_ok=True) - except Exception: - pass - - from openpilot.selfdrive.modeld.compile_warp import compile_dm_warp - compile_dm_warp(width, height) - - try: - return load_warp() - except Exception as retry_error: - cloudlog.exception(f"Reload failed after rebuilding {warp_path}: {retry_error}") - raise - def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]: self.numpy_inputs['calib'][0,:] = calib @@ -89,33 +54,16 @@ class ModelState: if self.image_warp is None: self.frame_buf_params = get_nv12_info(buf.width, buf.height) - self.image_warp = self._load_or_rebuild_dm_warp(buf.width, buf.height) + warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl' + with open(warp_path, "rb") as f: + self.image_warp = pickle.load(f) ptr = buf.data.ctypes.data # There is a ringbuffer of imgs, just cache tensors pointing to all of them if ptr not in self._blob_cache: self._blob_cache[ptr] = Tensor.from_blob(ptr, (self.frame_buf_params[3],), dtype='uint8') self.warp_inputs_np['transform'][:] = transform[:] - resolution_key = (buf.width, buf.height) - try: - self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize() - except AssertionError as error: - # Handle runtime backend mismatch (e.g. CPU-captured warp artifact on QCOM device). - if "args mismatch in JIT" not in str(error) or resolution_key in self._warp_backend_rebuild_attempted: - raise - - self._warp_backend_rebuild_attempted.add(resolution_key) - cloudlog.warning(f"DM warp JIT backend mismatch for {buf.width}x{buf.height}; rebuilding artifact for active backend") - warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl' - try: - warp_path.unlink(missing_ok=True) - except Exception: - pass - - from openpilot.selfdrive.modeld.compile_warp import compile_dm_warp - compile_dm_warp(buf.width, buf.height) - self.image_warp = self._load_or_rebuild_dm_warp(buf.width, buf.height) - self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize() + self.tensor_inputs['input_img'] = self.image_warp(self._blob_cache[ptr], self.warp_inputs['transform']).realize() output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy().flatten() diff --git a/selfdrive/modeld/models/dmonitoring_model.onnx b/selfdrive/modeld/models/dmonitoring_model.onnx index 0e08fe94..7d59ce24 100644 Binary files a/selfdrive/modeld/models/dmonitoring_model.onnx and b/selfdrive/modeld/models/dmonitoring_model.onnx differ diff --git a/selfdrive/modeld/models/dmonitoring_model_metadata.pkl b/selfdrive/modeld/models/dmonitoring_model_metadata.pkl index efcdd4c3..71e4b289 100644 Binary files a/selfdrive/modeld/models/dmonitoring_model_metadata.pkl and b/selfdrive/modeld/models/dmonitoring_model_metadata.pkl differ diff --git a/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl b/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl index f80ca5a2..299065b5 100644 Binary files a/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl and b/selfdrive/modeld/models/dmonitoring_model_tinygrad.pkl differ diff --git a/selfdrive/monitoring/dmonitoringd.py b/selfdrive/monitoring/dmonitoringd.py index fe51af1c..d2a6163a 100644 --- a/selfdrive/monitoring/dmonitoringd.py +++ b/selfdrive/monitoring/dmonitoringd.py @@ -48,16 +48,11 @@ def dmonitoringd_thread(): DM.always_on = params.get_bool("AlwaysOnDM") demo_mode = params.get_bool("IsDriverViewEnabled") and sm["carState"].gearShifter != GearShifter.reverse - # save rhd virtual toggle every 5 mins, but only with clear confidence. + # save rhd virtual toggle every 5 mins if (sm['driverStateV2'].frameId % 6000 == 0 and not demo_mode and - DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT): - wheelpos_mean = DM.wheelpos.prob_offseter.filtered_stat.M - save_rhd = DM.settings._WHEELPOS_THRESHOLD_ENTER_RHD + DM.settings._WHEELPOS_SAVE_MARGIN - save_lhd = DM.settings._WHEELPOS_THRESHOLD_ENTER_LHD - DM.settings._WHEELPOS_SAVE_MARGIN - if wheelpos_mean >= save_rhd: - params.put_bool_nonblocking("IsRhdDetected", True) - elif wheelpos_mean <= save_lhd: - params.put_bool_nonblocking("IsRhdDetected", False) + DM.wheelpos.prob_offseter.filtered_stat.n > DM.settings._WHEELPOS_FILTER_MIN_COUNT and + DM.wheel_on_right == (DM.wheelpos.prob_offseter.filtered_stat.M > DM.settings._WHEELPOS_THRESHOLD)): + params.put_bool_nonblocking("IsRhdDetected", DM.wheel_on_right) def main(): dmonitoringd_thread() diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 70945f8c..ca569627 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -35,11 +35,7 @@ class DRIVER_MONITOR_SETTINGS: self._EYE_THRESHOLD = 0.65 self._SG_THRESHOLD = 0.9 self._BLINK_THRESHOLD = 0.865 - - self._PHONE_THRESH = 0.75 if device_type == 'mici' else 0.4 - self._PHONE_THRESH2 = 15.0 - self._PHONE_MAX_OFFSET = 0.06 - self._PHONE_MIN_OFFSET = 0.025 + self._PHONE_THRESH = 0.5 self._POSE_PITCH_THRESHOLD = 0.3133 self._POSE_PITCH_THRESHOLD_SLACK = 0.3237 @@ -50,6 +46,8 @@ class DRIVER_MONITOR_SETTINGS: self._PITCH_NATURAL_OFFSET = 0.011 # initial value before offset is learned self._PITCH_NATURAL_THRESHOLD = 0.449 self._YAW_NATURAL_OFFSET = 0.075 # initial value before offset is learned + self._PITCH_NATURAL_VAR = 3*0.01 + self._YAW_NATURAL_VAR = 3*0.05 self._PITCH_MAX_OFFSET = 0.124 self._PITCH_MIN_OFFSET = -0.0881 self._YAW_MAX_OFFSET = 0.289 @@ -70,18 +68,9 @@ class DRIVER_MONITOR_SETTINGS: self._WHEELPOS_CALIB_MIN_SPEED = 11 self._WHEELPOS_THRESHOLD = 0.5 self._WHEELPOS_FILTER_MIN_COUNT = int(15 / self._DT_DMON) # allow 15 seconds to converge wheel side - self._WHEELPOS_THRESHOLD_ENTER_RHD = self._WHEELPOS_THRESHOLD - self._WHEELPOS_THRESHOLD_ENTER_LHD = self._WHEELPOS_THRESHOLD - self._WHEELPOS_SAVE_MARGIN = 0.0 - self._WHEELPOS_STARTUP_OVERRIDE_RHD = 0.55 - self._WHEELPOS_STARTUP_OVERRIDE_LHD = 0.45 - - # C4 (mici) has shown borderline wheel-side probabilities around 0.5x. - # Use hysteresis and stricter persistence thresholds to avoid false RHD latching. - if device_type == 'mici': - self._WHEELPOS_THRESHOLD_ENTER_RHD = 0.65 - self._WHEELPOS_THRESHOLD_ENTER_LHD = 0.35 - self._WHEELPOS_SAVE_MARGIN = 0.05 + self._WHEELPOS_DATA_AVG = 0.03 + self._WHEELPOS_DATA_VAR = 3*5.5e-5 + self._WHEELPOS_MAX_COUNT = -1 self._RECOVERY_FACTOR_MAX = 5. # relative to minus step change self._RECOVERY_FACTOR_MIN = 1.25 # relative to minus step change @@ -96,24 +85,26 @@ class DistractedType: DISTRACTED_PHONE = 1 << 2 class DriverPose: - def __init__(self, max_trackable): + def __init__(self, settings): + pitch_filter_raw_priors = (settings._PITCH_NATURAL_OFFSET, settings._PITCH_NATURAL_VAR, 2) + yaw_filter_raw_priors = (settings._YAW_NATURAL_OFFSET, settings._YAW_NATURAL_VAR, 2) self.yaw = 0. self.pitch = 0. self.roll = 0. self.yaw_std = 0. self.pitch_std = 0. self.roll_std = 0. - self.pitch_offseter = RunningStatFilter(max_trackable=max_trackable) - self.yaw_offseter = RunningStatFilter(max_trackable=max_trackable) + self.pitch_offseter = RunningStatFilter(raw_priors=pitch_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT) + self.yaw_offseter = RunningStatFilter(raw_priors=yaw_filter_raw_priors, max_trackable=settings._POSE_OFFSET_MAX_COUNT) self.calibrated = False self.low_std = True self.cfactor_pitch = 1. self.cfactor_yaw = 1. class DriverProb: - def __init__(self, max_trackable): + def __init__(self, raw_priors, max_trackable): self.prob = 0. - self.prob_offseter = RunningStatFilter(max_trackable=max_trackable) + self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable) self.prob_calibrated = False class DriverBlink: @@ -152,10 +143,11 @@ class DriverMonitoring: self.settings = settings if settings is not None else DRIVER_MONITOR_SETTINGS(device_type=HARDWARE.get_device_type()) # init driver status - self.wheelpos = DriverProb(-1) - self.pose = DriverPose(self.settings._POSE_OFFSET_MAX_COUNT) - self.phone = DriverProb(self.settings._POSE_OFFSET_MAX_COUNT) + wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2) + self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT) + self.pose = DriverPose(settings=self.settings) self.blink = DriverBlink() + self.phone_prob = 0. self.always_on = always_on self.distracted_types = [] @@ -256,12 +248,7 @@ class DriverMonitoring: if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD: distracted_types.append(DistractedType.DISTRACTED_BLINK) - if self.phone.prob_calibrated: - using_phone = self.phone.prob > max(min(self.phone.prob_offseter.filtered_stat.M, self.settings._PHONE_MAX_OFFSET), self.settings._PHONE_MIN_OFFSET) \ - * self.settings._PHONE_THRESH2 - else: - using_phone = self.phone.prob > self.settings._PHONE_THRESH - if using_phone: + if self.phone_prob > self.settings._PHONE_THRESH: distracted_types.append(DistractedType.DISTRACTED_PHONE) return distracted_types @@ -274,34 +261,8 @@ class DriverMonitoring: self.wheelpos.prob_offseter.push_and_update(rhd_pred) self.wheelpos.prob_calibrated = self.wheelpos.prob_offseter.filtered_stat.n > self.settings._WHEELPOS_FILTER_MIN_COUNT - startup_override = None - if not self.wheelpos.prob_calibrated and not demo_mode and not op_engaged: - left_face_detected = driver_state.leftDriverData.faceProb > self.settings._FACE_THRESHOLD - right_face_detected = driver_state.rightDriverData.faceProb > self.settings._FACE_THRESHOLD - if rhd_pred <= self.settings._WHEELPOS_STARTUP_OVERRIDE_LHD and left_face_detected and not right_face_detected: - startup_override = False - elif rhd_pred >= self.settings._WHEELPOS_STARTUP_OVERRIDE_RHD and right_face_detected and not left_face_detected: - startup_override = True - if self.wheelpos.prob_calibrated or demo_mode: - wheelpos_mean = self.wheelpos.prob_offseter.filtered_stat.M - enter_rhd = self.settings._WHEELPOS_THRESHOLD_ENTER_RHD - enter_lhd = self.settings._WHEELPOS_THRESHOLD_ENTER_LHD - - # Hysteresis: avoid side flapping near 0.5 and preserve last stable side. - if self.wheel_on_right_last is None: - if wheelpos_mean >= enter_rhd: - self.wheel_on_right = True - elif wheelpos_mean <= enter_lhd: - self.wheel_on_right = False - else: - self.wheel_on_right = self.wheel_on_right_default - elif self.wheel_on_right_last: - self.wheel_on_right = wheelpos_mean > enter_lhd - else: - self.wheel_on_right = wheelpos_mean >= enter_rhd - elif startup_override is not None: - self.wheel_on_right = startup_override + self.wheel_on_right = self.wheelpos.prob_offseter.filtered_stat.M > self.settings._WHEELPOS_THRESHOLD else: self.wheel_on_right = self.wheel_on_right_default # use default/saved if calibration is unfinished # make sure no switching when engaged @@ -325,7 +286,7 @@ class DriverMonitoring: * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \ * (driver_data.sunglassesProb < self.settings._SG_THRESHOLD) - self.phone.prob = driver_data.phoneProb + self.phone_prob = driver_data.phoneProb self.distracted_types = self._get_distracted_types() self.driver_distracted = (DistractedType.DISTRACTED_PHONE in self.distracted_types @@ -339,11 +300,9 @@ class DriverMonitoring: if self.face_detected and car_speed > self.settings._POSE_CALIB_MIN_SPEED and self.pose.low_std and (not op_engaged or not self.driver_distracted): self.pose.pitch_offseter.push_and_update(self.pose.pitch) self.pose.yaw_offseter.push_and_update(self.pose.yaw) - self.phone.prob_offseter.push_and_update(self.phone.prob) self.pose.calibrated = self.pose.pitch_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT and \ self.pose.yaw_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT - self.phone.prob_calibrated = self.phone.prob_offseter.filtered_stat.n > self.settings._POSE_OFFSET_MIN_COUNT if self.face_detected and not self.driver_distracted: if model_std_max > self.settings._DCAM_UNCERTAIN_ALERT_THRESHOLD: @@ -449,8 +408,8 @@ class DriverMonitoring: "posePitchValidCount": self.pose.pitch_offseter.filtered_stat.n, "poseYawOffset": self.pose.yaw_offseter.filtered_stat.mean(), "poseYawValidCount": self.pose.yaw_offseter.filtered_stat.n, - "phoneProbOffset": self.phone.prob_offseter.filtered_stat.mean(), - "phoneProbValidCount": self.phone.prob_offseter.filtered_stat.n, + "phoneProbOffset": 0., + "phoneProbValidCount": 0, "stepChange": self.step_change, "awarenessActive": self.awareness_active, "awarenessPassive": self.awareness_passive, diff --git a/selfdrive/monitoring/test_monitoring.py b/selfdrive/monitoring/test_monitoring.py index 46fe72b1..6ea9b802 100644 --- a/selfdrive/monitoring/test_monitoring.py +++ b/selfdrive/monitoring/test_monitoring.py @@ -30,23 +30,6 @@ def make_msg(face_detected, distracted=False, model_uncertain=False): return ds -def make_dual_msg(left_face_prob, right_face_prob, wheel_on_right_prob=0., model_uncertain=False): - ds = log.DriverStateV2.new_message() - ds.wheelOnRightProb = wheel_on_right_prob - for side, face_prob in ((ds.leftDriverData, left_face_prob), (ds.rightDriverData, right_face_prob)): - side.faceOrientation = [0., 0., 0.] - side.facePosition = [0., 0.] - side.faceProb = face_prob - side.leftEyeProb = 1. - side.rightEyeProb = 1. - side.leftBlinkProb = 0. - side.rightBlinkProb = 0. - side.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain] - side.facePositionStd = [1.*model_uncertain, 1.*model_uncertain] - side.phoneProb = 0. - return ds - - # driver state from neural net, 10Hz msg_NO_FACE_DETECTED = make_msg(False) msg_ATTENTIVE = make_msg(True) @@ -88,16 +71,6 @@ class TestMonitoring: events, _ = self._run_seq(always_attentive, always_false, always_true, always_false) self._assert_no_events(events) - def test_saved_rhd_recovers_to_lhd_with_strong_left_face(self): - settings = DRIVER_MONITOR_SETTINGS(device_type='mici') - DM = DriverMonitoring(rhd_saved=True, settings=settings) - msg = make_dual_msg(left_face_prob=0.95, right_face_prob=0.2, wheel_on_right_prob=0.35) - - DM._update_states(msg, [0, 0, 0], 0, False, False) - - assert not DM.wheel_on_right - assert DM.face_detected - # engaged, driver is distracted and does nothing def test_fully_distracted_driver(self): events, d_status = self._run_seq(always_distracted, always_false, always_true, always_false) @@ -230,3 +203,4 @@ class TestMonitoring: events[int((INVISIBLE_SECONDS_TO_ORANGE-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names assert EventName.driverUnresponsive in \ events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names + diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index b1c4cab5..05f99bec 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -91,7 +91,7 @@ class DeveloperLayoutMici(NavWidget): self._long_maneuver_toggle, self._alpha_long_toggle, self._debug_mode_toggle, - ], snap_items=False) + ], snap_items=False, scroll_indicator=True, edge_shadows=True) # Toggle lists self._refresh_toggles = ( diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index e37869ee..70a8e8ca 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -76,6 +76,8 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str): icon = "icons_mici/settings/device/reboot.png" elif action_text == "reset": icon = "icons_mici/settings/device/lkas.png" + elif action_text == "reset driver monitoring": + icon = "icons_mici/settings/device/cameras.png" elif action_text == "uninstall": icon = "icons_mici/settings/device/uninstall.png" else: @@ -83,7 +85,7 @@ def _engaged_confirmation_callback(callback: Callable, action_text: str): icon = "icons_mici/settings/comma_icon.png" dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red, - exit_on_confirm=action_text == "reset", + exit_on_confirm=action_text in {"reset", "reset driver monitoring"}, confirm_callback=confirm_callback) gui_app.set_modal_overlay(dlg) else: @@ -459,9 +461,17 @@ class DeviceLayoutMici(NavWidget): params.remove("LiveDelay") params.put_bool("OnroadCycleRequested", True) + def reset_driver_monitoring_callback(): + params = ui_state.params + params.remove("IsRhdDetected") + params.put_bool("OnroadCycleRequested", True) + def uninstall_openpilot_callback(): ui_state.params.put_bool("DoUninstall", True) + reset_driver_monitoring_btn = BigButton("reset driver monitoring calibration", "", "icons_mici/settings/device/cameras.png") + reset_driver_monitoring_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_driver_monitoring_callback, "reset driver monitoring")) + reset_calibration_btn = BigButton("reset calibration", "", "icons_mici/settings/device/lkas.png") reset_calibration_btn.set_click_callback(lambda: _engaged_confirmation_callback(reset_calibration_callback, "reset")) @@ -508,13 +518,14 @@ class DeviceLayoutMici(NavWidget): PairBigButton(), review_training_guide_btn, driver_cam_btn, + reset_driver_monitoring_btn, # lang_button, reset_calibration_btn, uninstall_openpilot_btn, regulatory_btn, reboot_btn, self._power_off_btn, - ], snap_items=False) + ], snap_items=False, scroll_indicator=True, edge_shadows=True) # Set up back navigation self.set_back_callback(back_callback) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 1faf4931..0326f71b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -3,7 +3,7 @@ from enum import IntEnum from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici +from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigToggle, BigParamControl from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog from openpilot.selfdrive.ui.ui_state import ui_state @@ -75,8 +75,14 @@ class NetworkLayoutMici(NavWidget): self._network_metered_btn = BigMultiToggle("network usage", ["default", "metered", "unmetered"], select_callback=network_metered_callback) self._network_metered_btn.set_enabled(False) - wifi_button = BigButton("wi-fi") + self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 64, 56) + self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 64, 47) + self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 64, 47) + self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) + + wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt) wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + self._wifi_button = wifi_button # ******** Advanced settings ******** # ******** Roaming toggle ******** @@ -101,7 +107,7 @@ class NetworkLayoutMici(NavWidget): self._cellular_metered_btn, # */ self._ip_address_btn, - ], snap_items=False) + ], snap_items=False, scroll_indicator=True, edge_shadows=True) # Set initial config roaming_enabled = ui_state.params.get_bool("GsmRoaming") diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 41267f16..3471b348 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -90,7 +90,7 @@ class SettingsLayout(NavWidget): #BigDialogButton("manual", "", "icons_mici/settings/manual_icon.png", "Check out the mici user\nmanual at comma.ai/setup"), firehose_btn, developer_btn, - ], snap_items=False) + ], snap_items=False, scroll_indicator=True, edge_shadows=True) # Set up back navigation self.set_back_callback(self.close_settings) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index c16504fa..b847fe74 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -35,7 +35,7 @@ class TogglesLayoutMici(NavWidget): record_front, record_mic, enable_openpilot, - ], snap_items=False) + ], snap_items=False, scroll_indicator=True, edge_shadows=True) # Toggle lists self._refresh_toggles = ( diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 75291fb7..1575a2f8 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -214,6 +214,10 @@ class AugmentedRoadView(CameraView): # debug self._pm = messaging.PubMaster(['uiDebug']) + @staticmethod + def _controls_ready() -> bool: + return ui_state.sm.recv_frame["selfdriveState"] >= ui_state.started_frame + def is_swiping_left(self) -> bool: """Check if currently swiping left (for scroller to disable).""" return self._bookmark_icon.is_swiping_left() @@ -224,6 +228,8 @@ class AugmentedRoadView(CameraView): # update offroad label if ui_state.panda_type == log.PandaState.PandaType.unknown: self._offroad_label.set_text("system booting") + elif ui_state.started and not self._controls_ready(): + self._offroad_label.set_text("waiting for\ncontrols to start") else: self._offroad_label.set_text("start the car to\nuse openpilot") @@ -259,6 +265,21 @@ class AugmentedRoadView(CameraView): # Render the base camera view super()._render(self._content_rect) + waiting_for_controls = ui_state.started and not self._controls_ready() + if waiting_for_controls: + rl.draw_rectangle(int(self._content_rect.x), int(self._content_rect.y), + int(self._content_rect.width), int(self._content_rect.height), + rl.Color(0, 0, 0, 145)) + self._offroad_label.render(self._content_rect) + rl.end_scissor_mode() + self._draw_border() + self._bookmark_icon.render(self.rect) + + msg = messaging.new_message('uiDebug') + msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000 + self._pm.send('uiDebug', msg) + return + in_reverse = self._is_in_reverse() # Draw all UI overlays diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index be08e0fe..b500f143 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -110,6 +110,7 @@ class BigButton(Widget): self.text = text self.value = value self.set_icon(icon) + self._label_font_size_override: int | None = None self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) @@ -136,6 +137,18 @@ class BigButton(Widget): def set_icon(self, icon: Union[str, rl.Texture]): self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon + def _refresh_label_metrics(self): + font_size = self._label_font_size_override if self._label_font_size_override is not None else self._get_label_font_size() + self._label.set_font_size(font_size) + self._needs_scroll = measure_text_cached(self._label_font, self.text, font_size).x + 25 > self._rect.width + self._scroll_offset = 0 + self._scroll_timer = 0 + self._scroll_state = ScrollState.PRE_SCROLL + + def _set_label_font_size_override(self, font_size: int | None): + self._label_font_size_override = font_size + self._refresh_label_metrics() + def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: return @@ -165,10 +178,12 @@ class BigButton(Widget): def set_text(self, text: str): self.text = text self._label.set_text(text) + self._refresh_label_metrics() def set_value(self, value: str): self.value = value self._sub_label.set_text(value) + self._refresh_label_metrics() def get_value(self) -> str: return self.value @@ -256,7 +271,7 @@ class BigToggle(BigButton): self._checked = initial_state self._toggle_callback = toggle_callback - self._label.set_font_size(48) + self._set_label_font_size_override(48) def _load_images(self): super()._load_images() @@ -296,8 +311,8 @@ class BigMultiToggle(BigToggle): self._select_callback = select_callback self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)) - # TODO: why isn't this automatic? - self._label.set_font_size(self._get_label_font_size()) + # Keep the title size stable when the selected option changes. + self._set_label_font_size_override(self._get_label_font_size()) self.set_value(self._options[0]) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 34760925..5f201766 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -113,6 +113,14 @@ class BigConfirmationDialogV2(BigDialogBase): self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) self._slider.set_enabled(lambda: not self._swiping_away) + def show_event(self): + super().show_event() + self._slider.show_event() + + def hide_event(self): + super().hide_event() + self._slider.hide_event() + def _on_confirm(self): if self._confirm_callback: self._confirm_callback() @@ -122,7 +130,7 @@ class BigConfirmationDialogV2(BigDialogBase): def _update_state(self): super()._update_state() if self._swiping_away and not self._slider.confirmed: - self._slider.reset() + self._slider.reset(reset_shimmer=False) def _render(self, _) -> DialogResult: self._slider.render(self._rect) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index e54c6f6f..65568e71 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -363,6 +363,26 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); addItem(dcamBtn); + resetDmCalibBtn = new ButtonControl( + tr("Reset Driver Monitoring"), + tr("RESET"), + tr("Clears the saved driver monitoring wheel-side calibration if the device thinks you're seated on the wrong side. " + "Resetting will restart openpilot if the car is powered on.") + ); + connect(resetDmCalibBtn, &ButtonControl::clicked, [&]() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"), this)) { + if (!uiState()->engaged()) { + params.remove("IsRhdDetected"); + params.putBool("OnroadCycleRequested", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reset Driver Monitoring"), this); + } + }); + addItem(resetDmCalibBtn); + resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { @@ -420,7 +440,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { - if (btn != pair_device && btn != resetCalibBtn) { + if (btn != pair_device && btn != resetCalibBtn && btn != resetDmCalibBtn) { btn->setEnabled(offroad); } } diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 438ee0eb..252b5062 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -94,6 +94,7 @@ private: ButtonControl *pair_galaxy; QPushButton *galaxy_qr_btn; ButtonControl *resetCalibBtn; + ButtonControl *resetDmCalibBtn; }; class TogglesPanel : public ListWidget { diff --git a/selfdrive/ui/ui b/selfdrive/ui/ui index 2035f80e..e1077059 100755 Binary files a/selfdrive/ui/ui and b/selfdrive/ui/ui differ diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index b09db0ab..5296cfaa 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -324,6 +324,14 @@ class WifiManager: def ipv4_address(self) -> str: return self._ipv4_address + @property + def networks(self) -> list[Network]: + return list(self._networks) + + @property + def connecting_to_ssid(self) -> str: + return self._connecting_to_ssid + @property def current_network_metered(self) -> MeteredType: return self._current_network_metered diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 3624fd77..079267b6 100644 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -133,11 +133,17 @@ class SoftwareSelectionPage(Widget): super().__init__() self._openpilot_slider = LargerSlider("slide to use\nstarpilot", use_openpilot_callback) - self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) + self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, + green=False, shimmer_offset=0.4) + + def show_event(self): + super().show_event() + self._openpilot_slider.show_event() + self._custom_software_slider.show_event() def reset(self): - self._openpilot_slider.reset() - self._custom_software_slider.reset() + self._openpilot_slider.reset(reset_shimmer=False) + self._custom_software_slider.reset(reset_shimmer=False) def _render(self, rect: rl.Rectangle): self._openpilot_slider.set_opacity(1.0 - self._custom_software_slider.slider_percentage) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 97b29308..7bfbd6dc 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -1,3 +1,4 @@ +import math from enum import IntEnum from collections.abc import Callable from itertools import zip_longest @@ -401,6 +402,12 @@ class UnifiedLabel(Widget): - Proper multiline vertical alignment - Height calculation for layout purposes """ + SHIMMER_BAND_WIDTH = 0.3 + SHIMMER_BLUR_RADIUS = 0.12 + SHIMMER_CYCLE_PERIOD = 2.5 + SHIMMER_SWEEP_FRACTION = 0.9 + SHIMMER_LOW_OPACITY = 0.65 + def __init__(self, text: str | Callable[[], str], font_size: int = DEFAULT_TEXT_SIZE, @@ -414,7 +421,8 @@ class UnifiedLabel(Widget): wrap_text: bool = True, scroll: bool = False, line_height: float = 1.0, - letter_spacing: float = 0.0): + letter_spacing: float = 0.0, + shimmer: bool = False): super().__init__() self._text = text self._font_size = font_size @@ -431,6 +439,8 @@ class UnifiedLabel(Widget): self._line_height = line_height * 0.9 self._letter_spacing = letter_spacing # 0.1 = 10% self._spacing_pixels = font_size * letter_spacing + self._shimmer = shimmer + self._shimmer_start_time = 0.0 # Scroll state self._scroll = scroll @@ -489,6 +499,13 @@ class UnifiedLabel(Widget): self._spacing_pixels = self._font_size * letter_spacing self._cached_text = None # Invalidate cache + def set_line_height(self, line_height: float): + """Update line height (multiplier, e.g., 1.0 = default).""" + new_line_height = line_height * 0.9 + if self._line_height != new_line_height: + self._line_height = new_line_height + self._cached_text = None + def set_font_weight(self, font_weight: FontWeight): """Update the font weight.""" if self._font_weight != font_weight: @@ -510,6 +527,9 @@ class UnifiedLabel(Widget): self._scroll_pause_t = None self._scroll_state = ScrollState.STARTING + def reset_shimmer(self, offset: float = 0.0): + self._shimmer_start_time = rl.get_time() + offset + def set_max_width(self, max_width: int | None): """Set the maximum width constraint for wrapping/eliding.""" if self._max_width != max_width: @@ -627,6 +647,25 @@ class UnifiedLabel(Widget): return self._cached_total_height return 0.0 + def _compute_shimmer_alpha(self, char_center_x: float, text_left: float, text_width: float) -> float: + if text_width <= 0: + return self.SHIMMER_LOW_OPACITY + + elapsed = rl.get_time() - self._shimmer_start_time + sigma = text_width * self.SHIMMER_BLUR_RADIUS + + t_raw = (elapsed % self.SHIMMER_CYCLE_PERIOD) / self.SHIMMER_CYCLE_PERIOD + t_clamped = max(0.0, min(t_raw / self.SHIMMER_SWEEP_FRACTION, 1.0)) + t = t_clamped * t_clamped * (3.0 - 2.0 * t_clamped) + + margin = text_width * self.SHIMMER_BAND_WIDTH + text_right = text_left + text_width + center = text_right + margin - t * (text_width + 2.0 * margin) + + d = char_center_x - center + shimmer = math.exp(-0.5 * d * d / (sigma * sigma)) if sigma > 0 else 0.0 + return self.SHIMMER_LOW_OPACITY + (1.0 - self.SHIMMER_LOW_OPACITY) * shimmer + def _render(self, _): """Render the label.""" if self._rect.width <= 0 or self._rect.height <= 0: @@ -770,6 +809,20 @@ class UnifiedLabel(Widget): line_x = self._rect.x + self._text_padding line_x += self._scroll_offset + x_offset + if self._shimmer and not emojis and line: + base_alpha = self._text_color.a / 255.0 + text_width = max(size.x, 1.0) + cursor_x = line_x + for char in line: + char_width = measure_text_cached(self._font, char, self._font_size, self._spacing_pixels).x + char_center_x = cursor_x + char_width / 2.0 + shimmer_alpha = self._compute_shimmer_alpha(char_center_x, line_x, text_width) + char_alpha = int(255 * base_alpha * shimmer_alpha) + char_color = rl.Color(self._text_color.r, self._text_color.g, self._text_color.b, char_alpha) + rl.draw_text_ex(self._font, char, rl.Vector2(cursor_x, current_y), self._font_size, self._spacing_pixels, char_color) + cursor_x += char_width + return + # Render line with emojis line_pos = rl.Vector2(line_x, current_y) prev_index = 0 diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index f33ba941..e941486a 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -11,6 +11,7 @@ ITEM_SPACING = 20 LINE_COLOR = rl.GRAY LINE_PADDING = 40 ANIMATION_SCALE = 0.6 +EDGE_SHADOW_WIDTH = 20 MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds DO_ZOOM = False @@ -33,9 +34,50 @@ class LineSeparator(Widget): LINE_COLOR) +class ScrollIndicator(Widget): + def __init__(self): + super().__init__() + self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48) + self._scroll_offset: float = 0.0 + self._content_size: float = 0.0 + self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + + def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None: + self._scroll_offset = scroll_offset + self._content_size = content_size + self._viewport = viewport + + def _render(self, _): + if self._viewport.width <= 0 or self._viewport.height <= 0: + return + + indicator_w = min(float(np.interp(self._content_size, [1000, 3000], [300, 100])), self._viewport.width) + max_scroll = self._content_size - self._viewport.width + if max_scroll > 0: + scroll_ratio = -self._scroll_offset / max_scroll + slide_range = max(self._viewport.width - indicator_w, 0.0) + x = self._viewport.x + scroll_ratio * slide_range + else: + x = self._viewport.x + (self._viewport.width - indicator_w) / 2 + y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2 + + dest_left = max(x, self._viewport.x) + dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width) + dest_w = max(indicator_w / 2, dest_right - dest_left) + + dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w) + dest_left = max(dest_left, self._viewport.x) + + src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height) + dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height) + rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0, + rl.Color(255, 255, 255, int(255 * 0.45))) + + class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, - line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING): + line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, + scroll_indicator: bool = False, edge_shadows: bool = False): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal @@ -65,11 +107,18 @@ class Scroller(Widget): self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items) self._scroll_enabled: bool | Callable[[], bool] = True - self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80) + self._txt_vertical_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80) + self._show_scroll_indicator = scroll_indicator and self._horizontal + self._scroll_indicator = ScrollIndicator() + self._edge_shadows = edge_shadows and self._horizontal for item in items: self.add_widget(item) + @property + def items(self) -> list[Widget]: + return self._items + def set_reset_scroll_at_show(self, scroll: bool): self._reset_scroll_at_show = scroll @@ -93,6 +142,14 @@ class Scroller(Widget): self._items.append(item) item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled) + def move_item(self, from_index: int, to_index: int) -> None: + if from_index == to_index: + return + if not (0 <= from_index < len(self._items) and 0 <= to_index < len(self._items)): + return + item = self._items.pop(from_index) + self._items.insert(to_index, item) + def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None: """Set whether scrolling is enabled (does not affect widget enabled state).""" self._scroll_enabled = enabled @@ -243,13 +300,27 @@ class Scroller(Widget): # Draw scroll indicator if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0: - _real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height + _real_content_size = self._content_size - self._rect.height + self._txt_vertical_scroll_indicator.height scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height - scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height) - rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) + scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_vertical_scroll_indicator.height) + rl.draw_texture_ex(self._txt_vertical_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) rl.end_scissor_mode() + if self._edge_shadows: + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.Color(0, 0, 0, 166), rl.BLANK) + + right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) + rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.BLANK, rl.Color(0, 0, 0, 166)) + + if self._show_scroll_indicator and len(self._visible_items) > 0: + self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) + self._scroll_indicator.render() + def show_event(self): super().show_event() if self._reset_scroll_at_show: diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index 455cdeef..e606c5c3 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -5,17 +5,19 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter class SmallSlider(Widget): HORIZONTAL_PADDING = 8 CONFIRM_DELAY = 0.2 + PRESSED_SCALE = 1.07 - def __init__(self, title: str, confirm_callback: Callable | None = None): + def __init__(self, title: str, confirm_callback: Callable | None = None, shimmer_offset: float = 0.0): # TODO: unify this with BigConfirmationDialogV2 super().__init__() self._confirm_callback = confirm_callback + self._shimmer_offset = shimmer_offset self._font = gui_app.font(FontWeight.DISPLAY) @@ -30,29 +32,40 @@ class SmallSlider(Widget): self._start_x_circle = 0.0 self._scroll_x_circle = 0.0 self._scroll_x_circle_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) + self._circle_scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._circle_press_time: float | None = None self._is_dragging_circle = False - self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), + self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9) + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 316 + self.HORIZONTAL_PADDING * 2, 100)) self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg.png", 316, 100) self._circle_bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle.png", 100, 100) + self._circle_bg_pressed_txt = self._circle_bg_txt self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 37, 32) @property def confirmed(self) -> bool: return self._confirmed_time > 0.0 - def reset(self): + def show_event(self): + super().show_event() + self.reset() + + def reset(self, reset_shimmer: bool = True): # reset all slider state self._is_dragging_circle = False self._confirmed_time = 0.0 self._confirm_callback_called = False + self._circle_press_time = None + self._circle_scale_filter.x = 1.0 + if reset_shimmer: + self._label.reset_shimmer(self._shimmer_offset) def set_opacity(self, opacity: float, smooth: bool = False): if smooth: @@ -83,6 +96,7 @@ class SmallSlider(Widget): if rl.check_collision_point_rec(mouse_event.pos, circle_button_rect): self._start_x_circle = mouse_event.pos.x self._is_dragging_circle = True + self._circle_press_time = rl.get_time() elif mouse_event.left_released: # swiped to left @@ -129,8 +143,9 @@ class SmallSlider(Widget): btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2 - if self._confirmed_time == 0.0 or self._scroll_x_circle > 0: - self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) + label_alpha = int(255 * (1.0 - self.slider_percentage) * self._opacity_filter.x) + if label_alpha > 0: + self._label.set_text_color(rl.Color(255, 255, 255, label_alpha)) label_rect = rl.Rectangle( self._rect.x + 20, self._rect.y, @@ -139,18 +154,24 @@ class SmallSlider(Widget): ) self._label.render(label_rect) - # circle and arrow - rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white) + circle_pressed = self._is_dragging_circle or self.confirmed or ( + self._circle_press_time is not None and rl.get_time() - self._circle_press_time < 0.075 + ) + circle_bg_txt = self._circle_bg_pressed_txt if circle_pressed else self._circle_bg_txt + scale = self._circle_scale_filter.update(self.PRESSED_SCALE if circle_pressed else 1.0) + scaled_btn_x = btn_x + (self._circle_bg_txt.width * (1 - scale)) / 2 + scaled_btn_y = btn_y + (self._circle_bg_txt.height * (1 - scale)) / 2 + rl.draw_texture_ex(circle_bg_txt, rl.Vector2(scaled_btn_x, scaled_btn_y), 0.0, scale, white) - arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2 - arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2 + arrow_x = scaled_btn_x + (self._circle_bg_txt.width * scale - self._circle_arrow_txt.width) / 2 + arrow_y = scaled_btn_y + (self._circle_bg_txt.height * scale - self._circle_arrow_txt.height) / 2 rl.draw_texture_ex(self._circle_arrow_txt, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, white) class LargerSlider(SmallSlider): - def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True): + def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True, shimmer_offset: float = 0.0): self._green = green - super().__init__(title, confirm_callback=confirm_callback) + super().__init__(title, confirm_callback=confirm_callback, shimmer_offset=shimmer_offset) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 115)) @@ -158,6 +179,7 @@ class LargerSlider(SmallSlider): self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg_larger.png", 520, 115) circle_fn = "slider_green_rounded_rectangle" if self._green else "slider_black_rounded_rectangle" self._circle_bg_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}.png", 180, 115) + self._circle_bg_pressed_txt = self._circle_bg_txt self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 55) @@ -165,15 +187,16 @@ class BigSlider(SmallSlider): def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable | None = None): self._icon = icon super().__init__(title, confirm_callback=confirm_callback) - self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), + self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - line_height=0.875) + line_height=0.875, shimmer=True) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 180)) self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180) self._circle_arrow_txt = self._icon @@ -183,4 +206,5 @@ class RedBigSlider(BigSlider): self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180) self._circle_arrow_txt = self._icon diff --git a/uncompiledmodels/driving_policy.onnx b/uncompiledmodels/driving_policy.onnx new file mode 100644 index 00000000..706c95db Binary files /dev/null and b/uncompiledmodels/driving_policy.onnx differ diff --git a/uncompiledmodels/driving_vision.onnx b/uncompiledmodels/driving_vision.onnx new file mode 100644 index 00000000..902f1dd3 Binary files /dev/null and b/uncompiledmodels/driving_vision.onnx differ